修改可发Android插件化原理分析(基于Neptune框架)

前言

Android插件化不算是一门新技术,发展了有一些年头了。不同公司的插件化方案大体原理上很相似。本文通过阅读爱奇艺的Neptune框架来介绍插件化的整体思路和流程。

插件化基础知识点

插件应用安装

所谓的插件其实本质上也是一个apk。在原生的Android应用中,apk在运行时会被映射成一个LoadedApk对象。插件在安装之后也会被映射成类似的PluginLoadedApk对象,统一管理插件的相关信息。

public class PluginLoadedApk {
    public static final ConcurrentMap<String, Vector<Method>> sMethods = new ConcurrentHashMap<String, Vector<Method>>(1);
    private static final String TAG = "PluginLoadedApk";
    /* 保存注入到宿主ClassLoader的插件 */
    private static Set<String> sInjectedPlugins = Collections.synchronizedSet(new HashSet<String>());
    /* 保存所有的插件ClassLoader */
    private static Map<String, DexClassLoader> sAllPluginClassLoader = new ConcurrentHashMap<>();

    /* 宿主的Context */
    private final Context mHostContext;
    /* 宿主的ClassLoader */
    private final ClassLoader mHostClassLoader;
    /* 宿主的Resource对象 */
    private final Resources mHostResource;
    /* 宿主的包名 */
    private final String mHostPackageName;
    /* 插件的路径 */
    private final String mPluginPath;
    /* 插件运行的进程名 */
    private final String mProcessName;
    /* 插件ClassLoader的parent */
    private ClassLoader mParent;
    /* 插件的类加载器 */
    private DexClassLoader mPluginClassLoader;
    /* 插件的Resource对象 */
    private Resources mPluginResource;
    /* 插件的AssetManager对象 */
    private AssetManager mPluginAssetManager;
    /* 插件的全局默认主题 */
    private Resources.Theme mPluginTheme;
    /* 插件的详细信息,主要通过解析AndroidManifest.xml获得 */
    private PluginPackageInfo mPluginPackageInfo;
    /* 插件工程的包名 */
    private String mPluginPackageName;
    /* 插件的Application */
    private Application mPluginApplication;
    /* 自定义插件Context,主要用来改写其中的一些方法从而改变插件行为 */
    private PluginContextWrapper mPluginAppContext;
    /* 自定义Instrumentation,对Activity跳转进行拦截 */
    private PluginInstrument mPluginInstrument;
    ...
}    

插件的安装分为内置插件(asset目录,sdcard)和线上插件两部分。

  1. 内置插件:

    • 约定存放在assets/pluginapp/<plugin_pkg_name>.apk形式,安装时解压到/data/data/<host_pkg_name>/app_pluginapp目录
    • sdcard插件,允许调试模式下安装,以<plugin_pkg_name>.apk命名
  1. 线上插件:直接将插件下载到sdcard目录上,然后拷贝到/data/data/<host_pkg_name>/app_pluginapp目录下;为了减少拷贝操作,可以直接下载到/data/data/<hots_pkg_name>/app_pluginapp目录;

插件的安装通过运行在独立进程的Service完成,主要防止部分机型dexopt hang住主进程。

dexopt

Android根据系统版本不同会采用两种虚拟机。Dalvik虚拟机是JIT方式解释执行dex字节码;ART虚拟机是AOT方式将dex字节码转化为oat机器码。

  • Dalvik是运行时解释dex文件,安装比较快,开启应用比较慢,应用占用空间小
  • ART是安装的时候字节码预编译成机器码存储在本地,执行的时候直接就可以运行的,安装慢,开启应用快,占用空间大;

如果当前运行在Dalvik虚拟机下,Dalvik会对classes.dex进行一次“翻译”,“翻译”的过程也就是守护进程installd的函数dexopt来对dex字节码进行优化,实际上也就是由dex文件生成odex文件,最终odex文件被保存在手机的VM缓存目录data/dalvik-cache下(注意!这里所生成的odex文件依旧是以dex为后缀名,格式如:system@priv-app@Settings@Settings.apk@classes.dex)。如果当前运行于ART模式下, ART同样会在首次进入系统的时候调用/system/bin/dexopt(此处应该是dex2oat工具吧)工具来将dex字节码翻译成本地机器码,保存在data/dalvik-cache下。 那么这里需要注意的是,无论是对dex字节码进行优化,还是将dex字节码翻译成本地机器码,最终得到的结果都是保存在相同名称的一个odex文件里面的,但是前者对应的是一个.dex文件(表示这是一个优化过的dex),后者对应的是一个.oat文件。通过这种方式,原来任何通过绝对路径引用了该odex文件的代码就都不需要修改了。 由于在系统首次启动时会对应用进行安装,那么在预置APK比较多的情况下,将会大大增加系统首次启动的时间。
对于插件安装来说,插件的安装通过运行在独立进程的Service完成,主要防止部分机型dexopt hang住主进程。

插件安装过程主要执行以下几步:

  1. 拷贝apk到内置存储区,重命名为<plugin_pkg_name>.apk
  2. 解压apk中的so库到app_pluginapp/<plugin_pkg_name>/lib目录
  3. dexopt优化插件dex,Android 7.0以上第一次会使用解释模式执行dex,优化加载速度

类加载

image.png

Java中的类都是通过ClassLoader加载的,而Android中类的加载也离不开ClassLoadder。在Android系统中,主要的ClassLoader有三个:

  • BootClassLoader:Android系统启动时用来预加载常用的类
  • PathClassLoader:用来加载系统和应用程序中的类,如果是非系统应用程序类,则会加载/data/app目录下的dex、apk或jar文件
  • DexClassLoader:可以加载指定路径的dex、apk或jar文件,支持从SD卡进行加载,是插件化的技术基础

类加载的双亲委派机制

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

关于插件中类的加载机制有两种处理方式,一种是单类加载机制,另一种是多类加载机制;单类加载器机制,即所有插件APP的类都通过宿主的ClassLoader(即PathClassLoader)进行加载,与MultiDex、Qzone热修复技术类似,通过Dex前插后者后插的方式实现。采用单类加载器模型,随着业务团队和插件的增加,很容易出现类重复问题,无法保证所有类都是独一无二的。多类加载器机制是指每个插件都由一个新的类加载器实例来加载,组件间的类是完全隔离,不能直接互相访问。

利用ClassLoader的双亲委派机制,多类加载有两种思路:

  • 自定义代理的ClassLoader设置为PathClassLoader的父类加载器,那么自定义的类加载器就能代理所有的类加载行为;在代理ClassLoader内部做类加载的逻辑分发,先尝试从宿主的ClassLoader加载,再尝试插件的ClassLoader加载。(好处:只需要在启动时hook ClassLoader,添加DelegateClassLoader,后续的类加载由DelegateClassLoader分发;对于未加载的插件,可以通过包名匹配,先触发插件加载,再加载类)
  • 每个PluginLoadedApk维护一个PluginClassLoader实例,其父ClassLoader是PathClassLoader;在类加载时,先尝试从宿主的ClassLoader加载,再尝试本插件的ClassLoader加载。(好处:每个插件维护自己的PluginLoadedApk,不存在分发,类隔离做的更好)

资源加载

Android APP运行除了类还有资源,运行时需要加载资源;对于Android来说,资源是通过AssetManager和Resources这两个类管理。App在运行时查找资源是通过当前Context的Resource实例中查找,在Resource内部是通过AssetManager管理当前的资源,AssetManager维护了资源包路径的数组。插件化的原理,就是将插件的资源路径添加到AssetManager的资源路径数组中,通过反射AssetManager的隐藏方法addAssetPath实现插件资源的加载。

try{
    AssetManager am = AssetManager.class.newInstance();
    Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
    addAssetPath.setAccessible(true);
    addAssetPath.invoke(am, pluginApkPath);
    Resources pluginResources = new Resources(am, hostResource.getDisplayMetrics(), hostResources.getConfiguration());
} catch (Exception e) {
    e.printStackTrace();
}

各种插件化方案的资源加载原理都是一样,区别主要在于不同插件的资源管理,是公用一套资源还是插件独立资源,插件和宿主的资源访问ID冲突问题。

  • 公用一套资源需要采用固定资源id及ID分段机制避免冲突
  • 独立资源方案,不同插件管理自己的资源

插件化中资源使用限制

限制:插件不能使用自己的转场动画,只能使用宿主、系统定义的转场动画。

转场动画最终会调用到IActivityManager,发起IPC请求,与AMS交互

    public void overridePendingTransition(IBinder token, String packageName,
            int enterAnim, int exitAnim) throws RemoteException;

Apk打包流程

先附上两张Android原生打包流程图

image.png

image.png

在插件编译打包时,需要完成以下几件事:

  • 插件的资源和宿主的资源通过不同的资源分段区分
  • 在插件化中,如果插件需要引用宿主的资源,则需要将宿主的资源id进行固定
  • 处理插件aapt的编译产物,不将宿主的资源打入apk中
  • 处理Manifest文件,将占坑的四大组件写入Manifest文件中
  • 在字节码层面对代码做修改

Hook点

  • Hook MergeResources Task,将public.xml文件拷贝至资源merge完成的目录
  • Hook ProcessAndroidResources Task,修改生成的arsc文件。
  • Hook ManifestProcessorTask, 在Manifest中插入特定信息。
  • Hook dexTask/Transform,最源代码的修改

四大组件的插件化

Activity的插件化

Activity启动可以分为两个阶段:往AMS发起启动Activity的请求、AMS校验后执行Activity启动。

往AMS发起请求

image.png

在Android 8.0(api 26)以下,应用往AMS发起启动Activity请求的流程如上。在Android 8.0及以上版本,AMN、AMP已经被弃用,而是使用ActivityManager类;

Hook点:

  • Hook Instrumentation类,代理execStartActivity方法
  • Hook AMN(<26)/ActivityManager(>=26),动态代理IActivityManager接口的实例对象

AMS校验后启动Activity

image.png

Android P(api 28)对Activity的启动过程做了修改;在Android P之前,是在H类的handleMessage方法的switch分支语句中,有专门处理启动Activity的逻辑

 public void handleMessage(Message msg) {
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                } break;
            
            //以下省略很多代码
            }
        }

在Android P中,启动Activity的这部分逻辑,被转移到了LaunchActivityItem类的execute方法中

public class LaunchActivityItem extends ClientTransactionItem {

    @Override
    public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
                mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
                mPendingResults, mPendingNewIntents, mIsForward,
                mProfilerInfo, client);
        client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
    }
}

Android P把H类中的100-109这10个消息都删除了,取而代之的是159这个消息,名为EXECUTE_TRANSACTION。收敛了Activity相关的message分发。

Hook点:

  • Hook H类,将占坑Activity替换成真实的Activity(需要做Android P的适配)
  • Hook Instrumentation类,替换成自定义的Instrument,重写newActivity、callActivityOnCreate等方法

Service的插件化

Service启动可以分为两个阶段:往AMS发起启动Service的请求、AMS校验后执行Service启动。

往AMS发起启动Service的请求

image.png

在Android 8.0(api 26)以下,应用往AMS发起启动Service请求的流程如上。在Android 8.0及以上版本,AMN、AMP已经被弃用,而是使用ActivityManager类。

Hook点

  • Hook ContextWrapper;替换成自定义的ContextWrapper,将Service替换成占坑的Service
  • Hook AMN(<26)/ActivityManager(>=26),动态代理IActivityManager接口的实例对象

创作不易喜欢的话记得点击+关注哦

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容