shadow插件运行原理

其实,用shadow来运行我们的业务插件已经很长一段时间了,但是呢,因为shadow这一块是开源了,而我们使用的版本维护者经常不on call,所以有种受制于人的赶脚。还是想抽出时间自己上手分析一遍,这样遇到问题也可以快速响应解决了。

首先看一下,一般情况下是如何实现插件能力的。 ps.这里,我将不再在文章内部贴太多实现代码,我希望可以先理清整体的思路,当然,所有相关代码我都会放在最后以供取阅。

既然是插件,那么就需要和宿主应用保持一定的距离,不能参与宿主的打包和安装,却要在宿主需要的时候出现。但是如果不参与宿主的编译打包安装,那么系统便无法感知有这么个插件的存在,继而想要通过系统方法调用插件的能力也不太可能,毕竟系统也没有承认过它呀。这也引申出插件化技术的几个关键技术点

1. 类加载/资源 加载

插件和宿主既然是两个独立的apk,那么他们的上下文也是隔离开来的。宿主无法通过自己的classLoader找到插件apk中的信息,反之亦然。同样需要使插件能够加载到插件包内的资源,同时做好与宿主资源的隔离。

2. 与系统、宿主应用的通信

没有在系统中进行安装,也就无法实现安卓中四大组件的功能。插件的activity等信息都没有向系统注册过,也就无法被系统启动从而注册上一系列生命周期了。

shadow接入指引可以看这篇文章,写的很详细

Android Tencent Shadow 插件接入指南 - cps666 - 博客园

那么依次看下,传统插件思路shadow是如何解决这些问题的

1. 类加载/资源 加载

1.1类加载

了解在JVM中,java代码被编译成.class文件,然后交给jvm来执行。 一般的,类加载分为: <math xmlns="http://www.w3.org/1998/Math/MathML"> 加载、链接、初始化 \color{red}{加载、链接、初始化} </math>加载、链接、初始化三个过程

在安卓系统中,提供DexClassLoader,支持 传入apk地址或者dex文件地址,即可以加载类文件。

所以,如果我们需要加载插件的class,那么就需要告诉ClassLoader插件的dex文件地址。

java 复制代码
// 创建DexClassLoader实例
   val dexClassLoader = DexClassLoader(
          apkFile.path,  // APK文件的路径
          optimizedDexOutputPath,  // 优化后的DEX文件存放路径
          null,  // 库文件搜索路径,通常为null
          parentClassLoader // 父ClassLoader
                )

但是,宿主也有自己的classLoader,如何让宿主apk启动插件时用插件的classLoader呢?

我们通过阅读 <math xmlns="http://www.w3.org/1998/Math/MathML"> B a s e C l a s s L o a d e r \color{red}{BaseClassLoader} </math>BaseClassLoader的findClass方法可以得出,classLoader会通过遍历pathList中的dexElements来完成类的查找,那么我们只需要将插件的dexList添加到其中,也就能够通过宿主的classLoader加载到插件的类了。很多安卓的热修复/补丁 方案也是基于这个原理来实现的。

这样也有弊端,比如当插件和宿主依赖了同一个库的不同版本时,可能会存在版本冲突导致异常。

当然,也可以重新实现一份插件的classLoader,实现多classLoader架构,使得插件apk总是使用插件的loader来加载。不过这种方式比较麻烦,需要向系统的packages中添加插件apk信息,而常规情况该信息的添加是由系统来处理的,因此需要hook很多系统操作。

shadow采用的方案也是重新实现一份插件的classloader,并将宿主的loader作为插件的父loader

java 复制代码
//创建插件loader
    PluginClassLoader(
            apk.absolutePath, // dex文件地址
            odexDir, // odex优化后路径
            installedApk.libraryPath, //库文件搜索路径,通常为null
            hostClassLoader, //宿主loader
            hostParentClassLoader,
            loadParameters.hostWhiteList // 宿主配置白名单
                )

在插件的基类中,重写getClassloader修改为自定义实现的classloader

java 复制代码
      @Override
      public ClassLoader getClassLoader() {
        if (hostActivityDelegate != null) {
          return hostActivityDelegate.getClassLoader();
        } else {
          return super.getClassLoader();
        }
      }

1.2资源加载:

处理完上面的问题后,我们已经可以成功在宿主中启动插件apk的actvity了,马上又会发现,展示插件内容时,插件引用的R资源文件会抛出 <math xmlns="http://www.w3.org/1998/Math/MathML"> N o t f o u n d \color{red}{Not found} </math>Not found的错误。这也很好理解,因为我们是用宿主来启动的插件,所以系统加载资源时,会用宿主的resource来进行查找。但是不建议将插件资源放到宿主中去,这样一来耦合过重,二来也会有资源冲突的问题。

一般方案是在插件中,重新定义resource,使得插件apk执行时总是用处理过的resource进行资源加载。反射的做法是调用AssetManager中的addAssetPath传入插件的apk路径

shadow这里采用了一种非反射的做法,即直接获取插件apk的resource,然后修改插件基类的getResource方法。

java 复制代码
        // 获取插件apk的resource
    val archiveFilePath = File(this.getFileStreamPath("tool-debug.apk").path).path
    // getPackageArchiveInfo 它用于从 APK 文件中检索包信息,而不需要安装该 APK。
                val packageArchiveInfo = packageManager.getPackageArchiveInfo( 
                    archiveFilePath,
                    PackageManager.GET_ACTIVITIES
                            or PackageManager.GET_META_DATA
                            or PackageManager.GET_SERVICES
                            or PackageManager.GET_PROVIDERS
                            or PackageManager.GET_SIGNATURES
                )
                mPluginResources = packageArchiveInfo?.let { it1 -> packageManager.getResourcesForApplication(it1.applicationInfo) }

2. 与系统的通信(以activity为例)

常规的我们启动一个activity,需要先实现该activity,然后在AndroidManifest.xml中注册相关信息,然后再通过intent来启动。(又可分显式和隐式)

整个启动流程很复杂,简单概括下就是 通过intent设置要启动的activity,然后由AMS找到该activity的相关信息,反射创建一个实例出来,然后再通过生命周期派发,再由WMS将该activity显示到屏幕界面上

如果没有注册,那么AMS则不会启动该activity。常见的是通过一些hook和代理技术,在通知AMS启动和创建时,用一个已经注册的代理activity替换非法activity,从而达到"狸猫换太子"的效果。

<math xmlns="http://www.w3.org/1998/Math/MathML"> 建议实际实现一次,可以对整个 a c t i v i t y 的启动流程有更深入的了解 . \color{red}{建议实际实现一次,可以对整个activity的启动流程有更深入的了解.} </math>建议实际实现一次,可以对整个activity的启动流程有更深入的了解.

同时这里还涉及到实际需要启动的插件Activity在另外的plugin.apk中,即1. 类加载中的相关问题,不再赘述。

这种方案也是主流的插件hook方案,不过缺点也显而易见,即使用反射无可避免的兼容性问题。

反观shadow在这里的实现,则更为巧妙。同样的也是使用 一个ProxyActivity来让系统启动,但无需图中两次替换操作,即减少两次反射hook,代理的ProxyActivity也真正的被启动起来了。

这时你肯定会想,那插件的activity呢?

当代理activity启动后,再从插件apk中找到入口activity,直接实例化后add到页面上展示。如果你试过直接new activity,会发现实际根本无法在业务层直接创建activity,activity需要由系统来创建,并完成 <math xmlns="http://www.w3.org/1998/Math/MathML"> 状态、资源、上下文等的初始化和分配。 \color{red}{状态、资源、上下文等的初始化和分配。} </math>状态、资源、上下文等的初始化和分配。然而,shadow通过修改插件编译生成的字节码的方式(gradle插件),将插件APK内的业务activity 都修改继承自shadow框架的ShadowActivity,不同于正常的ac,插件内的ac已经不再继承自安卓系统的ac,所以可以直接new出来展示。

自然,ShadowActivity也需要添加系统ac常用的生命周期方法,当我们操作插件ac时,实际上操作的是ShadowActivity,而相关操作又会被 ProxyActivity接收并发送给系统进行处理;当系统通知 ProxyActivity相关回调时, ProxyActivity又会将消息发送给ShadowActivity,进而再通知的插件内的activity。这是一种很好的委托模式。

基于activity梳理的插件加载运行流程基本就是这样。

3. 总结

概括一下,常见的插件通过hook AMS与handler,绕开系统检查,启动了一个非法的activity(未安装/注册),同时通过修改classloader和resource来实现插件类/资源的加载。而shadow与之的一个最主要的区别则是,通过修改字节码的方式,让插件内的activity不再继承安卓系统的activity,而真正成为一个可以被手动create的对象,再利用委托模式使其通过壳容器与系统、宿主进行交互。

--附

hook 启动插件activity时,需要将其替换为 已注册的代理activity

java 复制代码
/**
 * 使用代理的Activity替换需要启动的未注册的Activity
 * 在启动startActivity和AMS检测启动的页面之间,将未注册的Activity替换为已注册的Activity
 */
public static void hookAMS() {
    // Android10之前要适配
    try {
        //反射
        Class<?> clazz = Class.forName("android.app.ActivityTaskManager");
        Field singletonField = clazz.getDeclaredField("IActivityTaskManagerSingleton");

        singletonField.setAccessible(true);
        Object singleton = singletonField.get(null);

        Class<?> singletonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        Method getMethod = singletonClass.getMethod("get");
        Object mInstance = getMethod.invoke(singleton);

        Class IActivityTaskManagerClass = Class.forName("android.app.IActivityTaskManager");
        //动态代理
        Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{IActivityTaskManagerClass}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        if ("startActivity".equals(method.getName())) {
                            Log.i("Cath","拦截到调用startActivity方法");

                            // 获取 Intent 参数在 args 数组中的index值
                            for (int i = 0; i < args.length; i++) {
                                Log.i("Cath","第"+i+"个参数是:"+args[i]);
                                if (args[i] instanceof Intent) {
                                    Intent intent = (Intent) args[i];
                                    // 获取假的activity,在intent中的componentName对象中
                                    ComponentName componentName = intent.getComponent();
                                    Log.i("Cath","要跳转的目标Activity:"+componentName.getClassName());
                                    // 获取代理的activity名称,这个ac是真的有注册过的
                                    ComponentName proxyComponentName =intent.getParcelableExtra("proxyComponentName");
                                    if (proxyComponentName != null) {
                                        Log.i("Cath","拿来伪造的Activity:"+proxyComponentName.getClassName());
                                        intent.putExtra("realComponentName", componentName); // 真正想打开的插件activity也要记录一下
                                        intent.setComponent(proxyComponentName); // 把代理的activity信息设置进去
                                    }


                                    break;
                                }
                            }
                        }

                        // 原来流程
                        return method.invoke(mInstance, args);
                    }
                });

        // 用代理的对象替换系统的对象
        mInstanceField.set(singleton, mInstanceProxy);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

hook AMS通知展示代理activity时,需要再次替换回插件的activity

java 复制代码
/**
 * 需要启动的未注册的Activity 替换回来  ProxyActivity
 * 在AMS检测启动的Activity之后,Activity执行生命周期之前,将已注册页面替换成未注册的页面即可
 */
public static void hookHandler() {
    try {
        Class<?> clazz = Class.forName("android.app.ActivityThread");
        Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
        activityThreadField.setAccessible(true);
        Object activityThread = activityThreadField.get(null);

        Field mHField = clazz.getDeclaredField("mH");
        mHField.setAccessible(true);
        // 获取handler
        final Handler mH = (Handler) mHField.get(activityThread);

        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);

        mCallbackField.set(mH, new Handler.Callback() {

            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == 159) {
                    Log.i("Cath", "拦截在handleMessage中启动Activity的消息 ");
                    // msg.obj = ClientTransaction
                    try {
                        // 获取 List<ClientTransactionItem> mActivityCallbacks 对象
                        Field mActivityCallbacksField = msg.obj.getClass()
                                .getDeclaredField("mActivityCallbacks");
                        mActivityCallbacksField.setAccessible(true);
                        List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj); //根据源码得知,这个msg.obj就是ActivityClientRecord实例

                        for (int i = 0; i < mActivityCallbacks.size(); i++) {
                            // 打印 mActivityCallbacks 的所有item:
                            //android.app.servertransaction.WindowVisibilityItem
                            //android.app.servertransaction.LaunchActivityItem

                            // 如果是 LaunchActivityItem,则获取该类中的 mIntent 值,即 proxyIntent
                            if (mActivityCallbacks.get(i).getClass().getName()
                                    .equals("android.app.servertransaction.LaunchActivityItem")) {
                                Object launchActivityItem = mActivityCallbacks.get(i);
                                Field mIntentField = launchActivityItem.getClass()
                                        .getDeclaredField("mIntent");
                                mIntentField.setAccessible(true);
                                Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);

                                // 获取启动插件的 Intent,并替换回来
                                ComponentName realComponentName = proxyIntent.getParcelableExtra("realComponentName");
                                // java.lang.ClassNotFoundException: Didn't find class "com.example.tool.MainActivity" on path: DexPathList
                                Log.i("Cath", "替换回插件的intent" + realComponentName);
                                if (realComponentName != null) {
                                    proxyIntent.setComponent(realComponentName);
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.i("Cath", " 获取启动activity的int值 " + e.getMessage());
                    }
                }
                return false;
            }
        });
    } catch (Exception e) {
        e.printStackTrace();
        Log.i("Cath", " 获取启动activity的int值 " + e.getMessage());
    }
}

将插件的dex文件合并到宿主中

java 复制代码
    /**
     * 插件中的Element融入到宿主中
     */
    public static void pluginToAppAction(Activity activity, ClassLoader classLoader, ClassLoader parentClassLoader) throws Exception{
        //1. 找到宿主中的dexElements数组
        PathClassLoader pathClassLoader = (PathClassLoader) classLoader;
        Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object dexPathList = pathListField.get(pathClassLoader);

        Field dexElementsField = dexPathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        Object appDexElements = dexElementsField.get(dexPathList);
        //2.  找到插件中的dexElements数组
        File pluginFile = new File(activity.getFileStreamPath("tool-debug.apk").getPath());
        Log.d("Cath", activity.getFileStreamPath("tool-debug.apk").getPath());
        if (!pluginFile.exists()) {
            Log.i("Cath","插件包不存在");
            return;
        }
        String pluginPath = pluginFile.getAbsolutePath();
        File pluginDir = activity.getDir("pluginDir",MODE_PRIVATE);
        DexClassLoader dexClassLoader = new DexClassLoader(pluginPath,pluginDir.getAbsolutePath(),null, parentClassLoader);

        Class<?> pluginBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        Field pluginPathListField = pluginBaseDexClassLoaderClass.getDeclaredField("pathList");
        pluginPathListField.setAccessible(true);
        Object pluginDexPathList = pluginPathListField.get(dexClassLoader);
        Field dexElementsFieldPlugin = pluginDexPathList.getClass().getDeclaredField("dexElements");
        dexElementsFieldPlugin.setAccessible(true);
        Object pluginDexElements = dexElementsFieldPlugin.get(pluginDexPathList);

        //3. 把两个数组合并成一个新的数组 newElement
        int appLength = Array.getLength(appDexElements);
        int pluginLength = Array.getLength(pluginDexElements);
        int sum = appLength+pluginLength;
        //创建一个新的数组 两个参数,一个是类型  一个是长度
        Object newDexElement = Array.newInstance(appDexElements.getClass().getComponentType(), sum);
        //进行融合
        for (int i = 0; i < sum; i++) {
            if(i<appLength){
                Array.set(newDexElement,i,Array.get(appDexElements,i));
            }else {
                Array.set(newDexElement,i,Array.get(pluginDexElements,i-appLength));
            }
        }
        //4. 把新的数组设置回宿主中
        dexElementsField.set(dexPathList,newDexElement);

        //5. 处理布局文件
        handlePluginLayout(activity);
    }
    private static void handlePluginLayout(Activity activity) throws Exception{
        AssetManager assetManager = AssetManager.class.newInstance();

        // 把插件的路径 给 AssetManager
        File pluginFile = new File(activity.getFileStreamPath("tool-debug.apk").getPath());
        if (!pluginFile.exists()) {
            Log.i("Cath","插件包不存在");
            return;
        }
        String pluginPath = pluginFile.getAbsolutePath();

        //执行addAssetPath方法吧路径添加进去 assetManager才能加载资源
        Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath",String.class);
        addAssetPathMethod.setAccessible(true);
        addAssetPathMethod.invoke(assetManager,pluginPath);

//        //宿主的Resources
//        Resources r = getResources();
//        //创建新的resources用来加载插件中的资源
//        resources = new Resources(assetManager,r.getDisplayMetrics(),r.getConfiguration());

    }

替换宿主的classLoader,需要自定义一份寻找插件类的classLoader

java 复制代码
public static boolean hookPackageClassLoader(Context context, ClassLoader appClassLoaderNew) {
    try {
        Field packageInfoField = Class.forName("android.app.ContextImpl").getDeclaredField("mPackageInfo");
        packageInfoField.setAccessible(true);
        Object loadedApkObject = packageInfoField.get(context);
        Class LoadedApkClass = Class.forName("android.app.LoadedApk");
        Method getClassLoaderMethod = LoadedApkClass.getDeclaredMethod("getClassLoader");
        ClassLoader appClassLoaderOld = (ClassLoader) getClassLoaderMethod.invoke(loadedApkObject);
        Field appClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
        appClassLoaderField.setAccessible(true);
        appClassLoaderField.set(loadedApkObject, appClassLoaderNew);
        return true;
    } catch (Throwable ignored) {
        Log.d("Cath", "hookPackageClassLoader " + ignored.getMessage());
    }
    return false;
}
相关推荐
selt7915 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Yao_YongChao5 小时前
Android MVI处理副作用(Side Effect)
android·mvi·mvi副作用
非凡ghost6 小时前
JRiver Media Center(媒体管理软件)
android·学习·智能手机·媒体·软件需求
席卷全城6 小时前
Android 推箱子实现(引流文章)
android
齊家治國平天下7 小时前
Android 14 系统中 Tombstone 深度分析与解决指南
android·crash·系统服务·tombstone·android 14
maycho1239 小时前
MATLAB环境下基于双向长短时记忆网络的时间序列预测探索
android
思成不止于此9 小时前
【MySQL 零基础入门】MySQL 函数精讲(二):日期函数与流程控制函数篇
android·数据库·笔记·sql·学习·mysql
brave_zhao9 小时前
达梦数据库(DM8)支持全文索引功能,但并不直接兼容 MySQL 的 FULLTEXT 索引语法
android·adb
sheji34169 小时前
【开题答辩全过程】以 基于Android的网上订餐系统为例,包含答辩的问题和答案
android
fakerth10 小时前
【OpenHarmony】设计模式模块详解
c++·单例模式·设计模式·openharmony