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;
}
相关推荐
Estar.Lee31 分钟前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh1 小时前
uiautomator案例
android
工业甲酰苯胺2 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee4 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯4 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey5 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
小白不太白9507 小时前
设计模式之 模板方法模式
java·设计模式·模板方法模式
大白要努力!7 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
色空大师7 小时前
23种设计模式
java·开发语言·设计模式