专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
stormzhang  ·  来自李子柒的压迫感 ·  昨天  
鸿洋  ·  理解Android ... ·  2 天前  
鸿洋  ·  Android H5页面性能分析策略 ·  4 天前  
51好读  ›  专栏  ›  郭霖

Android插件化原理讲解与实战

郭霖  · 公众号  · android  · 2017-03-30 08:00

正文

今日科技快讯

近日,滴滴方面表示:按照北京市网约车细则的规定,将于4月1日前停止对全北京地区(包括六环外)外地牌照网约车进行派单。同时因近期运力减少,在部分地区、部分时段上,可能会在一定程度上对用户打车成功率、等待时长等方面造成影响,对此表示歉意。

作者简介

本篇来自 刘镓旗 的投稿,详细地分析了插件化的原理并给出了实现过程。本文着重于思路分析以及实践,文中涉及的某些知识点(比如 Binder机制类的加载)作者的其它博客均有介绍(文中出现斜体加粗的文章标题),想了解的朋友可自行访问下面作者博客地址。

刘镓旗 的博客地址:

http://blog.csdn.net/yulong0809

正文

今天我们做一个真正的可运行的启动插件demo,要知道一个插件可能是随时从网上下载下来的,那么也就是说其实这个apk不会被安装,那么如果不被安装,怎么能被加载呢,又如何管理插件中四大组件的生命周期呢,没有生命周期的四大组件是没有意义的。而且 Activity 是必须要在 AndroidManifest 中注册的,不注册就会抛出异常,那么怎么能绕过这个限制呢,还有,一个apk中肯定会用过各种资源,那么又该如何动态的加载资源呢。下面我们就带着这些问问一一的来解决,实现插件化,或者或是模块化。

先来看一下最终的运行结果:

思路分析,代码的动态加载:

apk被安装之后,apk的文件代码以及资源会被系统存放在固定的目录比如 /data/app/package_name/base-1.apk 中,系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类

但是要知道插件apk是不会被安装的,那么系统也就不会将我们的代理及资源存在在这个目录下,换句话说系统并不知道我们插件apk中的任何信息,也就根本不可能加载我们插件中的类。我们之前分析过应用的启动过程,其实就是启动了我们的 主Activity,然后在ActivityThread 的 performLaunchActivity方法 中创建的 Activity对象 并回调了 attch 和 onCreate 方法,我们再来看一下创建 Activity 对象时的代码:

系统通过待启动的 Activity 的类名 className,然后使用 ClassLoader对象cl 把这个类加载,最后使用反射创建了这个 Activity类 的实例对象。

我们看一下这个 cl对象 是通过 r.packageInfo.getClassLoader() 被赋值的,这个 r.packageInfo 是一个 LoadedApk 类型的对象,我们看看这个 LoadedApk 是什么东西。

注释的大概意思是 LoadedApk对象 是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的 Activity,Service 等四大组件的信息我们都可以通过此对象获取。我们知道了 r.packageInfo 是一个 LoadedApk 对象了,我们再看他是在哪被赋值的,我们顺着代码往前倒,performLaunchActivity,handleLaunchActivity,到了H类里,这一个Handler:

在这里通过 getPackageInfoNoCheck方法 被赋值,进去看看:

又调用了 getPackageInfo,再进去:

从一个叫 mPackages 的map集合中试图获得缓存,如果缓存不存在直接 new 一个,然后存入map集合。

好了到这里,其实我们就有了大概的思路,而且有两种实现方法。

1. 首先如果我们想要加载我们的插件apk我们需要一个 Classloader,那么我们知道系统的 Classloader 是通过 LoadedApk对象 获得的,而如果我们想要加载我们自己的插件apk,就需要我们自己构建一个 LoadedApk对象,然后修改其中的 Classloader对象,因为系统的并不知道我们的插件apk的信息,所有我们就要创建自己的 ClassLoader对象,然后全盘接管加载的过程,然后通过 hook 的思想将我们构建的这个 LoadedApk对象 存入那个叫 mPackages 的 map 中,这样的话每次在获取 LoadedApk对象 时就可以在map中得到了。
然后在到创建 Activity 的时候得到的 Classloader对象 就是我们自己改造过的cl了,这样就可以加载我们的外部插件了。这种方案需要我们 hook 掉系统系统的n多个类或者方法,因为创建 LoadedApk 对象时还需要一个 ApplicationInfo 的对象,这个对象就是解析 AndroidManifest 清单得来的,所以还需要我们自己手动解析插件中的 AndroidManifest清单,这个过程及其复杂,不过 360的DroidPlugin 就使用了这种方法。
2. 既然我们知道如果想启动插件 apk 就需一个 Classloader,那么我们换一种想法,能不能我们将我们的插件apk的信息告诉系统的这个 Classloader,然后让系统的 Classloader 来帮我们加载及创建呢?答案是肯定,之前我们说过讲过 android 中的 Classloader 主要分析 PathClassLoader 和 DexClassLoader,系统通过 PathClassLoader 来加载系统类和主dex中的类。而 DexClassLoader 则用于加载其他 dex文件中的类。他们都是继承自 BaseDexClassLoader。(如果没有看过的建议先看看 插件化知识详细分解及原理之ClassLoader及dex加载过程

我们再简单的回顾一下:

类在被加载的时候是通过 BaseDexClassLoader 的 findClass 的方法,其实最终调用了 DexPathList类 的 findClass,DexPathList类 中维护着 dexElements 的数组,这个数组就是存放我们dex文件的数组,我们只要想办法将我们插件apk的dex文件插入到这个 dexElements 中系统就可以知道我们的插件apk信息了,也自然就可以帮我们加载并创建对应的类。但是到这里还有一个问题,那就是 Activity 必须要在 AndroidManifest 注册才行,这个检查过程是在系统底层的,我们无法干涉,可是我们的插件apk是动态灵活的,宿主中并不固定的写死注册哪几个Activity,如果写死也就失去了插件的动态灵活性。

但是我们可以换一种方式,我们使用 hook 思想代理 startActivity 这个方法,使用占坑的方式,也就是说我们可以提前在 AndroidManifest 中固定写死一个 Activity,这个 Activity 只不过是一个傀儡,我们在启动我们插件apk的时候使用它去系统层校检合法性,然后等真正创建 Activity 的时候再通过 hook 思想拦截 Activity 的创建方法,提前将信息更换回来创建真正的插件apk。

总结一下分析结果:

1. startActivity 的时候最终会走到 AMS 的 startActivity 方法

2. 系统会检查一堆的信息验证这个 Activity 是否合法。

3. 然后会回调 ActivityThread 的 Handler 里的 handleLaunchActivity

4. 在这里走到了 performLaunchActivity 方法去创建 Activity 并回调一系列生命周期的方法

5. 创建 Activity 的时候会创建一个 LoaderApk对象,然后使用这个对象的 getClassLoader 来创建 Activity

6. 我们查看 getClassLoader() 方法发现返回的是 PathClassLoader,然后他继承自 BaseDexClassLoader

7. 然后我们查看 BaseDexClassLoader 发现他创建时创建了一个 DexPathList 类型的 pathList对象,然后在 findClass 时调用了 pathList.findClass 的方法

8. 然后我们查看 DexPathList类 中的 findClass 发现他内部维护了一个 Element[] dexElements的dex 数组,findClass 时是从数组中遍历查找的

根据我们的分析结果我们整理一下实现步骤:

1. 首先我们通过 DexClassloader 创建一个我们自己的 DexClassloader对象 去加载我们的插件apk,因为之前分析过,只有 DexClassloader 才能加载其他的dex文件,至于参数的意思之前的篇幅都讲过,不在啰嗦

2. 拿到宿主apk里 ClassLoader 中的 pathList对象 和我们Classloader 的 pathList,因为最终加载时是执行了 pathList.findClass方法

3. 然后我们拿到宿主 pathList对象 中的 Element[] 和我们创建的 Classloader 中的 Element[]

4. 因为我们要加入一个dex文件,那么原数组的长度要增加,所有我们要新建一个新的Element类型的数组,长度是新旧的和

5. 将我们的插件dex文件和宿主原来的dex文件都放入我们新建的数组中合并

6. 将我们的新数组设值给 pathList 对象

setField(suZhuPathList, suZhuPathList.getClass(), "dexElements", result);
7. 代理系统启动Activity的方法,然后将要启动的 Activity 替换成我们占坑的 Activity 已达到欺骗系统去检查的目的.

这里我们又要在继续分析了,我们要拦截 startActivity,之前我们分析启动过程时知道,最终会调用 ActivityManagerNative.getDefault().startActivity,其实也就是 ActivityManagerService 中的 startActivity。

我们再看看 ActivityManagerNative.getDefault() 的方法:

返回了一个 gDefault.get(),继续看

看到这个代码是不是很熟悉,因为我们第一篇分析 Binder 机制的时候就知道了,这其实就是aidl的方式远程通信方式,没看过的可以去看一下 插件化知识详细分解及原理之Binder机制

Singleton是系统提供的单例辅助类,这个类在4.x加入的,如果需要适配其他版本,请自行查阅源码

由于AMS需要频繁的和我们的应用通信,所有系统使用了一个单例把这个AMS的代理对象保存了起来;这也是AMS这个系统服务与其他普通服务的不同之处,这样我们就不需要通过 ServiceManager 去 Hook,我们只需要简单地Hook掉这个单例即可。 插件化知识详细分解及原理 之代理,hook,反射

那么也就是说我们要代理 ActivityManagerNative.getDefault() 的这个返回值就好了,也就是AMS的代理对象,有的朋友可能会问为什么不直接代理AMS,因为AMS是系统的,不在我们的进程中,我们能操作的只有这个AMS的代理类

现在我们已经拿到了这个ams的代理对象,现在我们需要创建一个我们自己的代理对象去拦截原ams中的方法:

然后我们使用动态代理去代理上面获取的ams:

8. 等系统检查完了之后,再次代理拦截系统创建 Activity 的方法将原来我们替换的 Activity 再次替换回来,已到达启动不在 AndroidManifest 注册的目的

这里我们继续分析怎么将我们前面存入要打开的 Activity 再换回来,我们之前分析应用的启动过程时知道,系统检查完了 Activity 的合法性后,会回调 ActivityThread 里的 scheduleLaunchActivity 方法,然后这个方法发送了一个消息到 ActivityThread 的内部类H里,这是一个Handler,看一下代码

那么我们如果想替换回我们的信息就要从这里入手了,至于 Handler 的消息机制这里不会深入,大概我们看一下 Handler 怎么处理消息的:

从上面我们看到,如果这个传递的机制:

  • 如果传递的 Message本身就有 callback,那么直接使用 Message 对象的 callback 方法;

  • 如果 Handler类 的成员变量 mCallback 不为空,那么首先执行这个 mCallback 回调;

  • 如果 mCallback 的回调返回 true,那么表示消息已经成功处理;直接结束。

  • 如果 mCallback 的回调返回 false,那么表示消息没有处理完毕,会继续使用 Handler类 的 handleMessage方法 处理消息。

通过上面给出的H的部分代码我们看到他只重写了 Handler的handleMessage 方法,并没有设置 Callback,那么我们就可以利用这一点,给这个 H 设置一个 Callback 让他在走 handleMessage 之前先走我们的方法,然后我们替换回之前的信息,再让他走 H 的 handleMessage

我们的 CallBack 的部分代码,具体替换信息代码请下载demo查看:

9. 最后调用我们之前写的这些代码,越早越好,在 Application 里调用也行,在 Activity 的 attachBaseContext 方法中也行

开始已经给出运行图,不在贴了,如果下载demo,ChaJianHuaTest 是宿主应用,请将ChaJianDemo 的 apk 文件放入sd卡根目录,因为demo中直接写死了路径

到这里我们已经把插件apk中的一个 activity 加载到了宿主中,有的人会问,生命周期没说呢,其实现在我们的这个插件Activity已经有了生命周期,因为我们使用了一个占坑的 Activity 去欺骗系统检查,后来我们又替换了我们自己真正要启动的 Activity,这个时候系统并不知道,所以系统还在傻傻的替我们管理者占坑的 Activity 的生命周期。有的朋友会问为什么系统可以将占坑的生命周期给了我们真正的 Activity 呢?

AMS 与 ActivityThread 之间对于 Activity 的生命周期的交互,并没有直接使用 Activity 对象进行交互,而是使用一个 token 来标识,这个 token 是 binder 对象,因此可以方便地跨进程传递。Activity 里面有一个成员变量 mToken 代表的就是它,token 可以唯一地标识一个 Activity 对象,这里我们只不过替换了要启动 Activity 的信息,并没有替换这个token,所以系统并不知道运行的这个 Activity 并不是原来的那个

这里我们已经完美的启动了一个插件apk中的Activity,但是还是有缺点,那就是我们插件的Activity中不能使用资源,只能使用代码布局,因为我们的插件apk现在属于宿主,而宿主根本就不知道插件apk中的资源存在,而且每一个apk都有自己的资源对象存在。

给出的demo中已经解决可以加载资源的问题,但是由于篇幅的问题,会在下一篇中再详细原理,下一篇完了后我们的插件化也就彻底说完了,觉得不错希望支持一下,谢谢。敬请期待下一篇,插件化资源的动态加载及使用。

完整demo地址:

https://github.com/ljqloveyou123/LiujiaqiAndroid

更多

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: