携程DynamicAPK插件化框架源码分析

携程DynamicAPK插件化框架源码分析

Author:莫川

插件核心思想

1.aapt的改造

分别对不同的插件项目分配不同的packageId,然后对各个插件的资源进行编译,生成R文件,然后与宿主项目的R文件进行id的合并。

要求:由于最终会将所有的资源文件id进行合并,因此,所有的资源名称均不能相同。

2.运行ClassLoader加载各Bundle

和MultiDex的思路是一样的,所有的插件都被加载到同一个ClassLoader当中,因此,不同插件中的Class必须保持包名和类名的唯一。否则,加载过的类不会再次被加载。

优缺点:各个Bundle之间完全可以相互调用,但是这也造成了各个Bundle之间ClassLoader的非隔离性。并且随着数组的加长,每次findClass的时间会变长,对性能照成一定长度的影响。

让我们在熟悉一下这张图:

在DynamicAPK框架中,每个Bundle被加载到ClassLoader的调用栈如下:

Bundle的Application:BundleBaseApplication

->BundleBaseApplication(onCreate)

->BundleCore(run)

->BundleImpl(optDexFile)

->BundleArchiveRevision(optDexFile)

->BundlePathLoader(installBundleDexs)

->…

如下图所示:

3.热修复

由于所有的插件都被加载到同一个ClassLoader当中,因为,热修复的方案都是从dexElements数组的顺序入手,修改expandFieldArray方法的实现,将修复的类放到dexElements的前方。核心代码如下(详见BundlePathLoader):


 private static void expandFieldArray(Object instance, String fieldName,
                                         Object[] extraElements,boolean isHotFix) throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        synchronized (BundlePathLoader.class) {
            Field jlrField = findField(instance, fieldName);
            Object[] original = (Object[]) jlrField.get(instance);
            Object[] combined = (Object[]) Array.newInstance(
                    original.getClass().getComponentType(), original.length + extraElements.length);
            if(isHotFix) {
                System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
                System.arraycopy(original, 0, combined, extraElements.length, original.length);
            }else {
                System.arraycopy(original, 0, combined, 0, original.length);
                System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
            }
            jlrField.set(instance, combined);
        }
    }

调用的关键代码如下(HotPatchItem.class):

   public void optDexFile() throws Exception{
        List<File> files = new ArrayList<File>();
        files.add(this.hotFixFile);
        BundlePathLoader.installBundleDexs(RuntimeArgs.androidApplication.getClassLoader(), storageDir, files, false);
    }

    public void optHotFixDexFile() throws Exception{
        List<File> files = new ArrayList<File>();
        files.add(this.hotFixFile);
        BundlePathLoader.installBundleDexs(RuntimeArgs.androidApplication.getClassLoader(), storageDir, files, true);
    }

4.运行时资源的加载

所有插件的资源都加载到DelegateResources中,关键代码如下:

DelegateResources.class

...
public static void newDelegateResources(Application application, Resources resources) throws Exception {
        List<Bundle> bundles = Framework.getBundles();
        if (bundles != null && !bundles.isEmpty()) {
            Resources delegateResources;
            List<String> arrayList = new ArrayList();
            arrayList.add(application.getApplicationInfo().sourceDir);
            for (Bundle bundle : bundles) {
                arrayList.add(((BundleImpl) bundle).getArchive().getArchiveFile().getAbsolutePath());
            }
            AssetManager assetManager = AssetManager.class.newInstance();
            for (String str : arrayList) {
                SysHacks.AssetManager_addAssetPath.invoke(assetManager, str);
            }
            //处理小米UI资源
            if (resources == null || !resources.getClass().getName().equals("android.content.res.MiuiResources")) {
                delegateResources = new DelegateResources(assetManager, resources);
            } else {
                Constructor declaredConstructor = Class.forName("android.content.res.MiuiResources").getDeclaredConstructor(new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class});
                declaredConstructor.setAccessible(true);
                delegateResources = (Resources) declaredConstructor.newInstance(new Object[]{assetManager, resources.getDisplayMetrics(), resources.getConfiguration()});
            }
            RuntimeArgs.delegateResources = delegateResources;
            AndroidHack.injectResources(application, delegateResources);
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append("newDelegateResources [");
            for (int i = 0; i < arrayList.size(); i++) {
                if (i > 0) {
                    stringBuffer.append(",");
                }
                stringBuffer.append(arrayList.get(i));
            }
            stringBuffer.append("]");
            log.log(stringBuffer.toString(), Logger.LogLevel.DBUG);
        }
    }

...

上述代码就是将所有Bundle中的资源,通过调用AssetManager的addAssetPath方法,加载到assetManager对象中,然后再用assetManager对象,创建delegateResources对象,并保存在RuntimeArgs.delegateResources当中,然后调用AndroidHack.injectResources方法,对Application和LoadedApk中的mResources成员变量进行注入,代码如下:

 public static void injectResources(Application application, Resources resources) throws Exception {
        Object activityThread = getActivityThread();
        if (activityThread == null) {
            throw new Exception("Failed to get ActivityThread.sCurrentActivityThread");
        }
        Object loadedApk = getLoadedApk(activityThread, application.getPackageName());
        if (loadedApk == null) {
            throw new Exception("Failed to get ActivityThread.mLoadedApk");
        }
        SysHacks.LoadedApk_mResources.set(loadedApk, resources);
        SysHacks.ContextImpl_mResources.set(application.getBaseContext(), resources);
        SysHacks.ContextImpl_mTheme.set(application.getBaseContext(), null);
    }

其中,上述获取LoadedApk的代码,也是通过反射,获取运行时ActivityThread类的LoadedApk对象.

5.运行时动态替换Resource对象

ContextImplHook,动态替换getResources

为了控制startActivity的时候,能够及时替换Activity的Resource和AssetsManager对象,使用ContextImplHook类对Comtext进行替换,然后动态的返回上一步加载的RuntimeArgs.delegateResources委托资源对象。ContextImplHook的核心代码如下:


    @Override
    public Resources getResources() {
        log.log("getResources is invoke", Logger.LogLevel.INFO);
        return RuntimeArgs.delegateResources;
    }

    @Override
    public AssetManager getAssets() {
        log.log("getAssets is invoke", Logger.LogLevel.INFO);
        return RuntimeArgs.delegateResources.getAssets();
    }

如何在Activity跳转过程中,动态的替换呢

通过反射替换ActivityThread的mInstrumentation对象,替换成InstrumentationHook.class,然后就可以在执行startActivity时,拦截其newActivity和callActivityOnCreate方法,在newActivity方法中,动态的替换newActivity的mResources对象。在callActivityOnCreate方法中将ContextImplHook注入到新创建的Activity中。核心代码如下:


    @Override
    public Activity newActivity(Class<?> cls, Context context, IBinder iBinder, Application application, Intent intent, ActivityInfo activityInfo, CharSequence charSequence, Activity activity, String str, Object obj) throws InstantiationException, IllegalAccessException {
        Activity newActivity = this.mBase.newActivity(cls, context, iBinder, application, intent, activityInfo, charSequence, activity, str, obj);
        if (RuntimeArgs.androidApplication.getPackageName().equals(activityInfo.packageName) && SysHacks.ContextThemeWrapper_mResources != null) {
            SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources);
        }
        return newActivity;
    }

    @Override
    public Activity newActivity(ClassLoader classLoader, String str, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Activity newActivity;
        try {
            newActivity = this.mBase.newActivity(classLoader, str, intent);
            if (SysHacks.ContextThemeWrapper_mResources != null) {
                SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources);
            }
        } catch (ClassNotFoundException e) {
            String property = Framework.getProperty("ctrip.android.bundle.welcome", "ctrip.android.view.home.CtripSplashActivity");
            if (StringUtil.isEmpty(property)) {
                throw e;
            } else {
                List runningTasks = ((ActivityManager) this.context.getSystemService(Context.ACTIVITY_SERVICE)).getRunningTasks(1);
                if (runningTasks != null && runningTasks.size() > 0 && ((ActivityManager.RunningTaskInfo) runningTasks.get(0)).numActivities > 1) {
                    if (intent.getComponent() == null) {
                        intent.setClassName(this.context, str);
                    }
                }
                log.log("Could not find activity class: " + str, Logger.LogLevel.WARN);
                log.log("Redirect to welcome activity: " + property, Logger.LogLevel.WARN);
                newActivity = this.mBase.newActivity(classLoader, property, intent);
            }
        }
        return newActivity;
    }

    @Override
    public void callActivityOnCreate(Activity activity, Bundle bundle) {
        if (RuntimeArgs.androidApplication.getPackageName().equals(activity.getPackageName())) {
            ContextImplHook contextImplHook = new ContextImplHook(activity.getBaseContext());
            if (!(SysHacks.ContextThemeWrapper_mBase == null || SysHacks.ContextThemeWrapper_mBase.getField() == null)) {
                SysHacks.ContextThemeWrapper_mBase.set(activity, contextImplHook);
            }
            SysHacks.ContextWrapper_mBase.set(activity, contextImplHook);
        }
        this.mBase.callActivityOnCreate(activity, bundle);
    }

总结如下图,Resource的加载和动态替换:

6.插件Activity在宿主AndroidManifest中的预注册

每个插件的Activity,必须在宿主的AndroidManifest.xml中进行注册。

DynamicAPK源码导读:

源代码的目录结构图

  • framework

    管理各个Bundle以及各个Bundle的封装、版本控制等。

  • hack

    通过反射的形式,hack类,方法,成员变量等

  • hotpatch

    热修复相关的封装

  • loader

    对MultiDex的修改,各Bundle加载到ClassLoader,热修复。

  • log

    日志管理

  • runtime

    运行时,对Resources进行动态替换

  • util

    工具类

博客相关Demo的Github地址

参考

1.AssetManager源码

2.LoadedApk源码

3.ActivityThread源码

4.DynamicAPK源码

时间: 2024-10-04 10:13:21

携程DynamicAPK插件化框架源码分析的相关文章

Android Small插件化框架源码分析

Android Small插件化框架源码分析 目录 概述 Small如何使用 插件加载流程 待改进的地方 一.概述 Small是一个写得非常简洁的插件化框架,工程源码位置:https://github.com/wequick/Small 插件化的方案,说到底要解决的核心问题只有三个: 1.1 插件类的加载 这个问题的解决和其它插件化框架的解决方法差不多.Android的类是由DexClassLoader加载的,通过反射可以将插件包动态加载进去.Small的gradle插件生成的是.so包,在初始

.NET MVC 插件化框架源码

本来想把源码整理了放github上,但最近一直忙,就直接在这里放出来了,还写得不太完整,里面有几个例子,插件上传也没写,只写了插件zip包解压,如果大家在使用中有什么疑问,可以加QQ群:142939183 这里我写了两个插件,前面那个插件是网站,后面那个插件是缓存插件,另外随便写了个插件管理界面,因为忙没写全,如果要测试插件zip文件加载功能,需要把插件zip包手动拷贝到Plugins目录下的pluginzips目录下,然后在页面上指定路径加载 这个是Plugins下面的目录结构,如果自己开发完

android插件化-apkplugdemo源码阅读指南-10

阅读本节内容前可先了解 apkplug基础教程 本教程是基于apkplug V1.6.8 版本编写  最新开发方式以官网为准 可下载最新的apkplugdemo源码http://git.oschina.net/plug/apkplugDemos apkplugdemo演示图 一 apkplugdemo工程源码结构 src |-com.apkplugdemo.adapter             --插件列表Adapter |-com.apkplugdemo.adapter.base     

YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

                        YII 框架源码分析             百度联盟事业部--黄银锋   目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 2.2.YiiBase 静态类 5 2.3.组件 6 2.4.模块 9 2.5 .App 应用   10 2.6 .WebApp 应用   11 3.系统组件 13 3.1.日志路由组件  13 3.2.Url 管理组件  15 3.3.异常

android 网络框架 源码分析

android 网络框架 源码分析 导语: 最近想开发一个协议分析工具,来监控android app 所有的网络操作行为, 由于android 开发分为Java层,和Native层, 对于Native层我们只要对linux下所有网络I/O接口进行拦截即可,对于java 层,笔者对android 网络框架不是很了解,所以这个工具开发之前,笔者需要对android 的网络框架进行一个简单的分析. 分析结论: 1. android 的网络框架都是基于Socket类实现的 2. java 层Socket

Unity时钟定时器插件——Vision Timer源码分析之二

Unity时钟定时器插件--Vision Timer源码分析之二 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面的已经介绍了vp_Timer(点击前往查看),vp_TimeUtility相对简单很多,vp_TimeUtility定义了个表示时间的结构Units: C#代码   /// <summary> /// represents a time measured in standard units /// </summar

介绍开源的.net通信框架NetworkComms框架 源码分析

原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 售价249英镑 我曾经花了2千多购买过此通讯框架, 目前作者已经开源  许可是:Apache License v2 开源地址是:https://github.com/MarcFletcher/NetworkComms.Net 这个框架给我的感觉是,代码很优美,运行很稳定,我有一个项目使用此框架已经稳定运行1年多.这个框架能够

Unity时钟定时器插件——Vision Timer源码分析之一

Unity时钟定时器插件--Vision Timer源码分析之一 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 因为项目中,UI的所有模块都没有MonBehaviour类(纯粹的C#类),只有像NGUI的基本组件的类是继承MonoBehaviour.因为没有继承MonoBehaviour,这也不能使用Update,InVoke,StartCoroutine等方法,这样就会显得很蹩脚.后来一个同事添加vp_Timer和vp_TimeUti

CodeIgniter框架——源码分析之入口文件index.php

CodeIgniter框架的入口文件主要是配置开发环境,定义目录常量,加载CI的核心类core/CodeIgniter.php. 源码分析如下: <?php //这个文件是入口,后期所有的文件都要在这里执行. /*----------------------------------------------- * 系统环境配置常量 * 能够配置错误显示级别 * ----------------------------------------------- * 默认情况下: * developmen