【插件&热修系列】Shadow源码解析之sample-manager(二)

引言

上一节,我们学习了Shadow源码解析之sample-manager(一),主要讲解了宿主的插件脚本/sample-manager.apk插件设计理念/宿主到sample-manager.apk插件入口的流程等

接下来,我们将学习宿主到sample-manager.apk插件入口后的系列流程,帮助我们了解插件sample-manager的动态管理其他业务插件的功能(如:加载其他插件/更新插件逻辑等)

代码分析

1.上节回顾

11.png

根据宿主的代码可以看出,mPluginManager.enter进入到了插件里面

2.插件入口代码位置

22.png

进入到插件里面后,enter的实现如上图,其中可以看到核心的实现是:onStartActivity

3.onStartActivity实现

 private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
        //0)参数准备
        final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
        final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
        final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
        final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
        Log.i(TAG, "SamplePluginManager, onStartActivity,pluginZipPath = " + pluginZipPath);
        Log.i(TAG, "SamplePluginManager, onStartActivity,partKey = " + partKey);
        Log.i(TAG, "SamplePluginManager, onStartActivity,className = " + className);

        executorService.execute(() -> {
            try {
                //1)插件的优化等,然后返回插件列表的第一个(默认)
                InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);

                //2)intent的包装
                Intent pluginIntent = new Intent();
                pluginIntent.setClassName(context.getPackageName(), className);
                if (extras != null) {
                    pluginIntent.replaceExtras(extras);
                }

                //3)加载框架插件(如:loader/runtime)和业务插件,同时启动插件activity
                startPluginActivity(installedPlugin, partKey, pluginIntent);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "SamplePluginManager, 插件启动,这个环节先不展开,下个阶段展开");
            }
        });

 }
复制代码

根据代码可以看出,主要做了如下几件事:

1)参数解析

2)异步处理一些事物

2.1)插件的优化等,然后返回插件列表的第一个(默认)

2.2)intent的包装

2.3)加载框架插件(如:loader/runtime)和业务插件,同时启动插件activity
复制代码

3.参数解析

这里解析了几个参数:

1)pluginZipPath:插件包的路径,具体包里面的内容戳这里>>>

2)partKey:要启动插件的名字

3)className:要启动插件的activity名字

4.异步处理一些事物之插件的优化等

 InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
复制代码

简单的1句代码调用,完成了插件的优化等系列工作

可以看出输入参数是 pluginZipPath,输出为 InstalledPlugin 对象

InstalledPlugin 为 pluginZipPath 内存抽象,具体戳这里>>>

下面,我们看下具体的实现代码:

public InstalledPlugin installPlugin(String zip, String hash, boolean odex)
            throws IOException, JSONException, InterruptedException, ExecutionException {

        //1)zip 转换为 PluginConfig 配置
        final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);

        final String uuid = pluginConfig.UUID;
        List<Future> futures = new LinkedList<>();

        //2)插件runTime/pluginLoader 的 odex 优化
        if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
            //runTime
            Future odexRuntime = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME, pluginConfig.runTime.file);
                return null;
            });
            futures.add(odexRuntime);

            //pluginLoader
            Future odexLoader = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER, pluginConfig.pluginLoader.file);
                return null;
            });
            futures.add(odexLoader);
        }

        //3)业务插件的so解压/odex优化等
        for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
            final String partKey = plugin.getKey();
            final File apkFile = plugin.getValue().file;

            //业务插件,插件apk的so解压
            Future extractSo = mFixedPool.submit((Callable) () -> {
                //插件apk的so解压
                extractSo(uuid, partKey, apkFile);
                return null;
            });
            futures.add(extractSo);

            //业务插件,odex优化
            if (odex) {
                Future odexPlugin = mFixedPool.submit((Callable) () -> {
                    oDexPlugin(uuid, partKey, apkFile);
                    return null;
                });
                futures.add(odexPlugin);
            }
        }

        //4)任务执行
        for (Future future : futures) {
            /**
             * get()方法可以:
             * 1)当任务结束后返回一个结果值,
             * 2)如果工作没有结束,则会阻塞当前线程,直到任务执行完毕
             * */
            future.get();
        }

        //5)执行完毕,将插件信息持久化到数据库(如:soDir/oDexDir等)
        onInstallCompleted(pluginConfig);

        //6)获取已安装的插件,最后安装的排在返回List的最前面
        return getInstalledPlugins(1).get(0);
    }
复制代码

代码较长,主要做了如下几件事:

1)zip 转换为 PluginConfig 配置

2)框架插件runTime/pluginLoader 的 odex 优化

3)业务插件的so解压/odex优化等

4)插件信息持久化到数据库(如:soDir/oDexDir等)

5)返回业务插件中的第一个

那么每一件事怎么实现的?然后涉及的安卓知识有哪些?下面进行一一讲解

4.1 zip 转换为 PluginConfig 配置

//1)zip 转换为 PluginConfig 配置
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
复制代码

可以看出输入参数是 pluginZipPath,输出为 PluginConfig 对象

这里把zip内存化,具体戳这里>>>

4.2 框架插件runTime/pluginLoader 的 odex 优化

//2)框架插件runTime/pluginLoader 的 odex 优化
if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {

            //runTime
            Future odexRuntime = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME, pluginConfig.runTime.file);
                return null;
            });
            futures.add(odexRuntime);

            //pluginLoader
            Future odexLoader = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER, pluginConfig.pluginLoader.file);
                return null;
            });
            
            futures.add(odexLoader);
}
复制代码

这里主要是对插件框架 runTime 和 pluginLoader的优化,下面我们看进一步的实现

public final void oDexPluginLoaderOrRunTime(String uuid, int type, File apkFile) throws InstallPluginException {
        try {
            File root = mUnpackManager.getAppDir();
            File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
            String key = type == InstalledType.TYPE_PLUGIN_LOADER ? "loader" : "runtime";
            //核心实现
            ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, key));
        } catch (InstallPluginException e) {
            throw e;
        }
}
复制代码

上面创建一些目录,给到后面odex优化做准备

下面看下具体优化实现

public static void oDexPlugin(File apkFile, File oDexDir, File copiedTagFile) throws InstallPluginException {
        String key = apkFile.getAbsolutePath();
        //key:sample-loader-debug.apk / sample-runtime-debug.apk 等
        //value:Object
        Object lock = sLocks.get(key);
        if (lock == null) {
            lock = new Object();
            sLocks.put(key, lock);
        }

        synchronized (lock) {
            if (copiedTagFile.exists()) {
                return;
            }

            //如果odex目录存在但是个文件,不是目录,那超出预料了。删除了也不一定能工作正常。
            if (oDexDir.exists() && oDexDir.isFile()) {
                throw new InstallPluginException("oDexDir=" + oDexDir.getAbsolutePath() + "已存在,但它是个文件,不敢贸然删除");
            }

            //创建oDex目录
            oDexDir.mkdirs();

            new DexClassLoader(
                    apkFile.getAbsolutePath(),//dexPath
                    oDexDir.getAbsolutePath(),//optimizedDirectory
                    null,//librarySearchPath
                    ODexBloc.class.getClassLoader());//ClassLoader parent

            //执行成功,就创建tag标志文件
            try {
                copiedTagFile.createNewFile();
            } catch (IOException e) {
                throw new InstallPluginException("oDexPlugin完毕 创建tag文件失败:" + copiedTagFile.getAbsolutePath(), e);
            }
        }
}
复制代码

这里最核心的操作就是实例化了 DexClassLoader 来达到优化dex的操作

怎么实例化就可以了?

这里涉及到了一个安卓dex加载的知识点:

应用程序在第一次启动app的时候,会在/dalvik/dalvik-cache目录下生成odex文件结构

而我们的插件apk是没有经过安装的,启动也不是系统启动,所以插件的odex优化需要自己做,也就是直接new DexClassLoader 对象即可;

具体为什么系统需要做odex这件事,又涉及到了系统方面的知识,这里截取网上的一段话:

213.png

然后为什么直接new DexClassLoader 就可以实现dex优化为odex?具体细节流程解析可以网上自行查找知识点或者戳这里>>>

4.3 业务插件的so解压/odex优化等

for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
            final String partKey = plugin.getKey();
            final File apkFile = plugin.getValue().file;

            //业务插件,插件apk的so解压
            Future extractSo = mFixedPool.submit((Callable) () -> {
                //插件apk的so解压
                extractSo(uuid, partKey, apkFile);
                return null;
            });
            futures.add(extractSo);

            //业务插件,odex优化
            if (odex) {
                Future odexPlugin = mFixedPool.submit((Callable) () -> {
                    oDexPlugin(uuid, partKey, apkFile);
                    return null;
                });
                futures.add(odexPlugin);
            }
}
复制代码

首先是so的解压

public final void extractSo(String uuid, String partKey, File apkFile) throws InstallPluginException {
        try {
            File root = mUnpackManager.getAppDir();
            String filter = "lib/" + getAbi() + "/";
            File soDir = AppCacheFolderManager.getLibDir(root, uuid);
            CopySoBloc.copySo(apkFile, soDir
                    , AppCacheFolderManager.getLibCopiedFile(soDir, partKey), filter);
        } catch (InstallPluginException e) {
            throw e;
        }
}
复制代码

代码比较简答,把so拷贝到指定的目录文件夹里面

然后是dex的优化

 public final void oDexPlugin(String uuid, String partKey, File apkFile) throws InstallPluginException {
        try {
            File root = mUnpackManager.getAppDir();
            File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
            ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, partKey));
        } catch (InstallPluginException e) {
            throw e;
        }
 }
复制代码

和上面奖到的优化原理一样,所以不再累赘

4.4 插件信息持久化到数据库(如:soDir/oDexDir等)

//5)执行完毕,将插件信息持久化到数据库(如:soDir/oDexDir等)
onInstallCompleted(pluginConfig);
复制代码
public final void onInstallCompleted(PluginConfig pluginConfig) {
        File root = mUnpackManager.getAppDir();
        String soDir = AppCacheFolderManager.getLibDir(root, pluginConfig.UUID).getAbsolutePath();
        String oDexDir = AppCacheFolderManager.getODexDir(root, pluginConfig.UUID).getAbsolutePath();

        mInstalledDao.insert(pluginConfig, soDir, oDexDir);
}
复制代码

这个只是简单的把插件包zip的一些信息数据库持久化,为后续业务使用做准备

4.5 返回业务插件中的第一个

//6)获取已安装的插件,最后安装的排在返回List的最前面
return getInstalledPlugins(1).get(0);
复制代码
 /**
     * 获取已安装的插件,最后安装的排在返回List的最前面
     *
     * @param limit 最多获取个数
     */
public final List<InstalledPlugin> getInstalledPlugins(int limit) {
    return mInstalledDao.getLastPlugins(limit);
}
复制代码

这个也是比较简单,在刚才持久化数据库里面去除第一个业务插件的信息返回加载

5.异步处理一些事物之intent的包装

 //2)intent的包装
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
if (extras != null) {
   pluginIntent.replaceExtras(extras);
}
复制代码

要启动的插件的activity类名字给到intent等

6.异步处理一些事物之加载/启动系列插件等

//3)加载框架插件(如:loader/runtime)和业务插件,同时启动插件activity
startPluginActivity(installedPlugin, partKey, pluginIntent);
复制代码

 public void startPluginActivity(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
        //1)intent 的包装
        Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        //2)启动
        mPluginLoader.startActivityInPluginProcess(intent);
 }
复制代码

这个环节出要做了2件事:

1)intent 2次的包装

2)启动插件的activity

6.1 intent 2次的包装

首先看下第一点,intent 2次的包装

 //1)intent 的包装
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
复制代码
 public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
        //1)加载框架插件(如:loader/runtime)和业务插件
        loadPlugin(installedPlugin.UUID, partKey);

        //2)包装插件的intent等
        return mPluginLoader.convertActivityIntent(pluginIntent);
 }
复制代码

根据代码可以看出,在对intent进行包装前进行了框架插件的加载,然后才是包装

因为intent目的是启动业务插件的业务activity,所以首先要先准备好框架插件和业务插件,然后在插件中提取信息,封装到intent,这样才能启动成功

下面我们来详细看看里面的实现

a)加载框架插件(如:loader/runtime)和业务插件

private void loadPlugin(String uuid, String partKey) {
        //1)加载框架插件: loader 和 runtime
        loadPluginLoaderAndRuntime(uuid, partKey);

        //2)加载业务插件:通过框架loader加载插件;
        //例子:partKey = sample-plugin-app
        mPluginLoader.loadPlugin(partKey);
}
复制代码

这里是先加载框架插件loader 和 runtime,然后利用框架loader来加载业务插件

下面我们来看下是如何加载框架插件的

private void loadPluginLoaderAndRuntime(String uuid, String partKey) {
        /***
         * sample-runtime-release.apk
         * */
        loadRunTime(uuid);
        /**
         *sample-loader-release.apk
         * */
        loadPluginLoader(uuid);
}
复制代码

我们以加载框架插件runtime为例子展开,其实就是加载插件zip里面的《sample-runtime-release.apk》

public final void loadRunTime(String uuid) {
        InstalledApk installedApk;
        // 1)loader插件内存化
        installedApk = getRuntime(uuid);
        InstalledApk installedRuntimeApk = new InstalledApk(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath);
        //2)根据内存化对象,加载框架插件loader
        boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
        if (loaded) {
            DynamicRuntime.saveLastRuntimeInfo(mHostContext, installedRuntimeApk);
        }
}
复制代码

这个实现是阉割过的,因为原著那边是通过IPC BInder来远程调用,这里为了避免链路长所以实现把IPC那边调用省略了

这里主要是《loader插件内存化》 和 《根据内存化对象加载框架插件loader》

loader插件内存化,这一块没什么好讲的,所以省略

根据内存化对象加载框架插件loader,下面来重点看下这个

 public static boolean loadRuntime(InstalledApk installedRuntimeApk) {
        //宿主的 ClassLoader
        ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();

        //ClassLoader 的继承关系被改过,具体见: hackParentToRuntime(installedRuntimeApk, contextClassLoader);
        RuntimeClassLoader runtimeClassLoader = getRuntimeClassLoader();

        if (runtimeClassLoader != null) {
            String apkPath = runtimeClassLoader.apkPath;
            if (TextUtils.equals(apkPath, installedRuntimeApk.apkFilePath)) {
                //已经加载相同版本的runtime了,不需要加载
                Log.i(TAG, "DynamicRuntime, 已经加载相同apkPath的runtime了,不需要加载");
                return false;
            } else {
                //版本不一样,说明要更新runtime,先恢复正常的classLoader结构
                recoveryClassLoader();
            }
        }

        //将runtime 挂到 pathclassLoader 之上
        try {
            Log.i(TAG, "DynamicRuntime, 正常处理,将runtime 挂到 pathclassLoader 之上");
            hackParentToRuntime(installedRuntimeApk, contextClassLoader);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return true;
 }
复制代码
 /*
  *	contextClassLoader 为宿主的loader
  */
  private static void hackParentToRuntime(InstalledApk installedRuntimeApk,ClassLoader contextClassLoader) throws Exception {
        RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(
                installedRuntimeApk.apkFilePath,
                installedRuntimeApk.oDexPath,
                installedRuntimeApk.libraryPath,
                contextClassLoader.getParent());

        hackParentClassLoader(contextClassLoader, runtimeClassLoader);
   }
复制代码
static void hackParentClassLoader(ClassLoader classLoader, ClassLoader newParentClassLoader) throws Exception {
      
        Field field = getParentField();
        if (field == null) {
            throw new RuntimeException("在ClassLoader.class中没找到类型为ClassLoader的parent域");
        }
        field.setAccessible(true);
        field.set(classLoader, newParentClassLoader);
  }
复制代码

这里看起来代码比较多,但实际上原理很简单

就是构建Classloader来加载插件,用的方式是《替换 PathClassloader 的 parent》的方案,这个方案的介绍具体戳这里>>>

到这里加载框架插件runtime(sample-runtime-release.apk)就讲完了;

下面我们看下框架插件loader(sample-loader-release.apk)的加载

public final void loadPluginLoader(String uuid) {
        InstalledApk installedApk;
        installedApk = getPluginLoader(uuid);
        File file = new File(installedApk.apkFilePath);
        if (!file.exists()) {
            Log.e(TAG, file.getAbsolutePath() + ", 文件不存在");
        }
        LoaderImplLoader implLoader = new LoaderImplLoader();
        mPluginLoader = implLoader.load(installedApk, uuid, mHostContext);
 }
复制代码
 public PluginLoaderImpl load(InstalledApk installedApk, String uuid, Context appContext) {
        ApkClassLoader pluginLoaderClassLoader = new ApkClassLoader(
                installedApk,
                LoaderImplLoader.class.getClassLoader(),
                loadWhiteList(installedApk),
                1
        );

        //从apk中,读取接口的实现
        //plugin-debug.zip/sample-loader-debug.apk
        LoaderFactory loaderFactory = null;
        try {
            loaderFactory = pluginLoaderClassLoader.getInterface(
                    LoaderFactory.class,
                    sLoaderFactoryImplClassName
            );
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (loaderFactory == null) {
            return null;
        }

        //buildLoader
        return loaderFactory.buildLoader(uuid, appContext);
}
复制代码

根据代码可以看出loader的实现是在sample-loader-release.apk里面的

而加载插件框架用到的是加载器是

class ApkClassLoader extends DexClassLoader {

    static final String TAG = "daviAndroid";
    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    ApkClassLoader(InstalledApk installedApk,
                   ClassLoader parent,////parent  =  宿主ClassLoader
                   String[] mInterfacePackageNames,
                   int grandTimes) {
        super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);

        //默认代
        ClassLoader grand = parent;//parent  =  宿主ClassLoader

        //外面定第几代
        for (int i = 0; i < grandTimes; i++) {
            grand = grand.getParent();
        }
        mGrandParent = grand;

        this.mInterfacePackageNames = mInterfacePackageNames;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        String packageName;
        int dot = className.lastIndexOf('.');
        if (dot != -1) {
            packageName = className.substring(0, dot);
        } else {
            packageName = "";
        }
        boolean isInterface = false;
        for (String interfacePackageName : mInterfacePackageNames) {
            if (packageName.equals(interfacePackageName)) {
                isInterface = true;
                break;
            }
        }
        //apkFilePath = /data/user/0/com.tencent.shadow.sample.host/files/pluginmanager.apk
        //oDexPath = /data/user/0/com.tencent.shadow.sample.host/files/ManagerImplLoader/ksi9pl9k
        if (isInterface) {
            //情况1:插件可以加载宿主的类实现:
            return super.loadClass(className, resolve);
        } else {
            //情况2:插件不需要加载宿主的类实现
            Class<?> clazz = findLoadedClass(className);//1)系统里面找
            if (clazz == null) {
                ClassNotFoundException suppressed = null;
                try {
                    //否则先从自己的dexPath中查找
                    clazz = findClass(className);//2)自己的dexPath中查找
                } catch (ClassNotFoundException e) {
                    suppressed = e;
                }
                if (clazz == null) {
                    //如果找不到,则再从parent的parent ClassLoader中查找。
                    //BootClassLoader
                    try {
                        clazz = mGrandParent.loadClass(className);//父亲找
                    } catch (ClassNotFoundException e) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            e.addSuppressed(suppressed);
                        }
                        throw e;
                    }
                }
            }
            return clazz;
        }
    }

    <T> T getInterface(Class<T> clazz, String className) throws Exception {
        try {
            Class<?> interfaceImplementClass = loadClass(className);
            Object interfaceImplement = interfaceImplementClass.newInstance();
            return clazz.cast(interfaceImplement);
        } catch (ClassNotFoundException | InstantiationException
                | ClassCastException | IllegalAccessException e) {
            throw new Exception(e);
        }
    }

}
复制代码

这个加载器的原理其实就是以前说过的,具体戳这里>>>

加载完框架插件runtime和loader之后,那么就会利用框架插件loader来加载业务插件

 //2)加载业务插件:通过框架loader加载插件;
 //例子:partKey = sample-plugin-app
 mPluginLoader.loadPlugin(partKey);
复制代码

有人可能会好奇,为什么业务插件的loader要做成框架插件方式来加载业务插件?

这个就是shadow的动态设计,所有的模块都是动态的(如:业务插件下载逻辑/业务插件加载等),具体前面有介绍戳这里>>>

因为目前阶段的讲解是在sample-manager模块,也就是框架插件中的sample-manager.apk的源码解析,具体框架loader是怎么加载业务插件的实现这里先不展开,后面模块解析到loader.apk的时候会讲,知道有这么回事即可

b)包装插件的intent等

 //2)包装插件的intent等
return mPluginLoader.convertActivityIntent(pluginIntent);
复制代码

这个实现也是在loader里面的,其实内容主要是把业务插件的一些信息包装到intent里面

具体实现暂时不展开,后续讲解loader.apk模块的时候讲解

6.2 启动插件的activity

//2)启动
mPluginLoader.startActivityInPluginProcess(intent);
复制代码

这个实现也是在loader里面的,具体实现暂时不展开,后续讲解loader.apk模块的时候讲解

结尾

哈哈,该篇就写到这里(一起体系化学习,一起成长)

Tips

更多精彩内容,请关注 ”Android热修技术“ 微信公众号

猜你喜欢

转载自juejin.im/post/7018820423353630751