现在的应用功能越来越多,代码膨胀,应用体积太大,为解耦模块方便开发,插件化有其必要性
插件化
背景
应用功能越来越多,代码膨胀,触及65535方法数的上限,应用体积太大
实时更新独立模块的需求越来越强
功能模块的解耦,方便维护
H5和Hybrid可以解决一些问题,但是体验比不上Native代码,因此插件化有其必要性
代理
代理可以用来进行方法增强或拦截
静态代理
1 | interface IAct { |
动态代理
传统的静态代理模式需要为每一个需要代理的类写一个代理类,比较麻烦,为了更优雅地实现代理模式,JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类
1 | IAct impl = new Actor(); |
动态代理主要处理InvocationHandler
和Proxy
类
1 | class ActHandler implements InvocationHandler { |
Hook
通过创建代理对象替代原始对象,进行功能增强,称为Hook,以startActivity()
为例
找到被Hook的对象,称为Hook点,静态变量和单例比较适合,因为相对不容易变化
Context.startActivity()
实际调用了ActivityThread
类的mInstrumentation
的execStartActivity()
方法,ActivityThread
是主线程,一个程序只有一个,因此是个良好的Hook点
把mInstrumentation
替换成代理对象,首先需要获得主线程对象,可以通过ActivityThread.currentActivityThread()
获得,ActivityThread
是一个隐藏类,需要反射获取
HookInstrumentation.java
1 | public class HookInstrumentation extends Instrumentation { |
我们加了一句打印,并把目标换成了HookActivity
,反射获取Instrumentation
类的execStartActivity()
方法并调用
1 | try { |
获得ActivityThread
类,找到currentActivityThread()
方法,调用方法获得ActivityThread
对象,然后找到mInstrumentation
字段,获取对象,用代理对象替代并设置进去,就完成了Hook
Binder Hook
Hook系统服务的机制称之为Binder Hook,因为本质上这些服务提供者都是存在于系统各个进程的Binder对象
1 | // 获取原始的IBinder对象 |
1 | public static android.content.IClipboard asInterface(android.os.IBinder obj) { |
可以通过修改queryLocalInterface()
来替换
1 | public static IBinder getService(String name) { |
1 | public class BinderHookHandler implements InvocationHandler { |
剪贴板接口IClipboard
的动态代理,替换了getPrimaryClip()
和hasPrimaryClip()
两个方法,构造方法中调用Clipboard$Stub
的asInterface()
方法,把传入的IBinder
转为IClipboard
1 | public class BinderProxyHookHandler implements InvocationHandler { |
代理替换IBinder
,修改其queryLocalInterface()
方法,使其返回Hook后的剪贴板管理器
1 | // 获得ServiceManager类 从中获得getService方法 |
AMS
ActivityManagerNative
实际上是ActivityManagerService
这个远程对象的Binder代理对象,因为比较常用,所以有一个gDefault
的全局变量保存
1 | // 获得ActivityManagerNative 获得其gDefault字段 |
PMS
1 | public PackageManager getPackageManager() { |
可见由Activity.getPackageManager()
获得,并经由ApplicationPackagemanager
包装
1 | public static IPackageManager getPackageManager() { |
可见可以Hook掉这个静态对象sPackageManager
1 | Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); |
使用同样的方法
1 | public class PackageManagerHandler implements InvocationHandler { |
Hook掉了getPackageInfo()
方法,修改了包名,Hook掉getInstalledApplications()
方法,在每个包名后面加了.hook
Activity生命周期管理
Activity必须在清单中显示声明,而清单不可能预先声明插件中的Activity
解决:可以预先注册一个中间Activity欺骗系统,然后替换成真正的Activity
首先处理ActivityManagerNative
1 | public class ActivityManagerHandler implements InvocationHandler { |
这样AMS侧就会认为启动了一个清单中注册的Activity,接下来在AMS对应用的回调里替换实际的Activity,所有的操作会在ActivityThread.H
里分发处理,因为Handler
的处理顺序是先Message.handleMessage()
,再使用全局mCallback
的handleMessage()
,最后调用Handler.handleMessage()
,所以可以Hook这个全局mCallback
1 | public class HookHandlerCallback implements Handler.Callback { |
因为AMS和应用之间并不传递具体的Activity信息,而是通过一个token关联,所以AMS侧仍然操作着伪造的Activity,应用侧则操作着真实的Activity,并且拥有正常的生命周期
ClassLoader
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制,是运行时动态加载的
Android通过DexClassLoader直接加载dex或apk
多ClassLoader方案
Activity的实例是在ActivityThread.performLaunchActivity()
中创建的
1 | private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { |
可见需要拿到一个ClassLoader和类名
这里的ClassLoader
是由r.packageInfo
的getClassLoader()
获得
r.packageInfo
是一个LoadedApk
对象,代表一个当前加载了的apk的本地状态,是apk在内存中的表示
在ActivityThread.H.handleMessage()
里,可以看到r.pakcageInfo
的来源
1 | case LAUNCH_ACTIVITY: { |
最后调用了getPackageInfo()
1 | private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { |
这里的mPackages
就是一个Hook点,可以构造自己的LoadedApk
添加进去
LoadedApk
又可以通过getPackageInfoNoCheck()
获得
1 | public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) { |
需要构造ApplicationInfo
和CompatibilityInfo
两个参数
前者描述了由清单的application
标签描述的信息,可以使用PackageParser
的generateApplication()
方法获得,然而这个类的兼容性非常差
1 | public static ApplicationInfo generateApplicationInfo(Package p, int flags, PackageUserState state) { |
参数Package
需要分析apk文件,可使用PackageParser.parsePackage()
获得
1 | public Package parsePackage(File packageFile, int flags) throws PackageParserException { |
需要传入apk文件和标志位
后者代表不同用户中包的信息,这里用默认的即可
1 | // 获得当前的ActivityThread对象 |
这样调用
1 | Intent intent = new Intent(); |
按照教程这么做,后来一直找不到对应的类,堆栈中显示ClassLoader并不是我们提供的自定义ClassLoader,而mPackages
中确实加入了插件信息
所以只可能是传入的键有问题,实际使用的是ActivityInfo.applicationInfo.packageName
,通过拦截AMS回调发现这个值仍然是宿主包名,在Hook后的ActivityThread.H
中修改之
1 | // 拿到真实包名 |
前面也要传一下真实包名
1 | Intent intent = new Intent(); |
再次启动会遇到无法实例化Application,,追踪发现系统会向PMS查询包信息,因为没有而抛出异常
1 | public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { |
需要Hook一下PMS做处理
1 | if (method.getName().equals("getPackageInfo")) { |
拦截getPackageInfo()
命令,改为在本地查找,找到则返回,否则交给系统做,这样就骗过去了
最后碰到一个问题,堆栈涉及AppCompatActivity
,把插件Activity的基类改为Activity
就好了,尚不清楚为什么
单ClassLoader方案
对宿主的ClassLoader做修改,使其可以找到我们插件中的类
在Application类中有一个成员变量mLoadedApk
,表示宿主的LoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中getClassLoader()
获取到的就是宿主程序的ClassLoader
LoadedApk.java
1 | public ClassLoader getClassLoader() { |
ApplicationLoaders.java
1 | private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, String librarySearchPath, String libraryPermittedPath, ClassLoader parent, String cacheKey) { |
PathClassLoader.java
1 | public class PathClassLoader extends BaseDexClassLoader { |
BaseDexClassLoader.java
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
可以看到它在一个pathList
里面查找类,是一个DexPathList
对象
DexPathList.java
1 | public Class findClass(String name, List<Throwable> suppressed) { |
会遍历它的dexElements
数组来查找类,可以在这里做修改
1 | // 获得DexClassLoader的pathList字段 |
多ClassLoader构架,每一个插件都有一个自己的ClassLoader,因此类的隔离性非常好——如果不同的插件使用了同一个库的不同版本,它们相安无事
单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,虽然代码简单,但是鲁棒性很差;无法避免插件之间甚至插件与宿主之间使用的类库有冲突
多ClassLoader还有一个优点可以真正完成代码的热加载,如果插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可(Java中,不同ClassLoader加载的同一个类被认为是不同的类)
多ClassLoader架构在大多数情况下是明智之举
插件BroadcastReceiver
静态注册的接收器在清单文件里,解析后保存在PMS中,以供AMS使用
1 | List receivers = null; |
receivers
里就是从PMS获得的静态注册的接收器,registeredReceivers
是动态注册的接收器
动态注册接收器
只需要反射即可使用
静态注册接收器
分析插件清单文件中的receiver
然后集中动态注册
缺点是无法在应用未运行时接收到广播
1 | private void registerPluginReceiver(File apkFile) throws IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException { |
在插件的ClassLoader信息添加之后执行上面的方法,动态注册所有静态的广播接收器
插件Service
Service和Activity一样,都需要通过AMS管理,但Service不能简单的套用Activity的插件化方案,因为Activity有栈管理,有限的StubActivity可以满足需求,而Service的数量可能无法被有限的StubService满足
可以使用手动管理Service:拦截到startService()
或bindService()
,没有创建Service则创建并调用onCreate
和onStartCommand()
,如果创建了则调用onStartCommand()
,拦截到stopService()
则调用onDestroy()
为了仍能使用Service的独立进程特性,可以在清单中声明一些不同进程中的Service,启动代理Service,在其onStartCommand()
中分发执行插件Service中的onStartCommand()
方法
启动Service
1 | <service android:name=".ProxyService" /> |
清单中定义代理Service
1 | public class ProxyService extends Service { |
代理Service的onStartCommand()
里分发到真实Service里
1 | public void onStartCommand(Intent intent, int flags, int startId) { |
分发onStartCommand()
给实际Service
1 | if (method.getName().equals("startService")) { |
Hook掉AMS,拦截startService()
,修改Intent的目标Service为代理Service,并将真实Intent保存起来
1 | ArrayList services = (ArrayList) ReflectUtil.getField("android.content.pm.PackageParser$Package", "services", pack); |
解析插件apk,把清单中的Service的信息保存起来
1 | intent.setComponent(new ComponentName("com.example.liuhan.subproject", "com.example.liuhan.subproject.PluginService")); |
填入包名和类名,启动Service
绑定Service
1 | ServiceConnection mConnection = new ServiceConnection() { |
这里绑定了指定包名和类名的服务,在绑定成功后通过传递包名、方法名和参数调用其方法
1 | public class ProxyService extends Service { |
在代理服务里添加相关代码
1 | public Object invoke(String className, String methodName, Object... args) { |
实际服务只是一个普通的对象,反射调用其方法即可,至于跨进程的Binder机制其实由代理服务完成了,这里还是只做了一个分发而已
插件ContentProvider
ContentProvider的初始化在Application启动时,比onCreate()
还要早,所以可以在attachBaseContext()
中进行插件ContentProvider的安装
1 | private void installContentProvider(File apkFile) { |
仍然可以使用分发代理的方法
1 | public class ProxyContentProvider extends ContentProvider { |
在清单中注册代理ContentProvider
1 | <provider |
插件资源
不做处理无法加载插件资源
1 | private void initResource() { |
在插件的Activity内,处理资源
1 |
|
插件Activity如果继承自AppCompatActivity,主题还是有问题……