写在前面:关于插件化涉及到的实在是太多了,这里强烈推荐大家看一看包建强的《Android插件化开发指南》。另外关于本文内作为示例的VirtualAPK,最后一次更新是2018年,兼容到Android9.0。大家也可以看看腾讯在2019年开源的 Shadow。
Hook技术是一种在程序运行时对程序进行调试的技术。举个例子,使用Hook前如下图:
使用Hook,变成了下面这样:
Hook挂在了对象A和对象B之间,这样就可以对对象A的参数做修改,以及修改对象B的返回值,也就是一种劫持——对象B收到的不是对象A发出的,对象A收到的也不是对象B返回的。
我们知道应用程序进程与系统进程之间、以及应用程序进程之间都是彼此独立的,但是使用了Hook技术,就可以在进程之间进行行为更改了。比如想要使用应用程序进程更改系统进程的某些行为,就可以使用Hook融入到系统进程中,然后再来通过Hook改变系统进程中对象的行为,如下图所示:
需要注意的是,这个被劫持的对象B(Hook点),为了保证Hook的稳定性,一般会选择容易找到且不易变化的对象,比如静态变量或者是单例。
了解了什么是Hook技术,接下来我们就说说在Android开发中会涉及到的Hook技术。
由于我们讲Hook技术是为了讲插件化原理做准备,因此在这里主要讲讲Hook Java。
首先我们来复习一下代理模式,之所以要讲代理模式,这是因为用代理对象来替代Hook点,我们就可以在代理上实现自己想做的操作了。
真实主题类实现抽象主题接口方法,实现真实方法意图;代理类实现抽象主题接口方法,在这其中调用所持有的被代理者所实现的接口方法。最后在客户端类创建真实主题类,之后创建 “抽象主题接口 代理= new 代理类(真实主题类)”,并调用“代理.实现的接口方法”。
以上就是我们经常使用的静态代理。之所叫静态代理,是因为代码运行前就已经存在了代理类的class编译文件。相对的,动态代理就是在代码运行时通过反射来动态地生成代理对象,并确定到底来代理谁。
Java提供了动态的代理接口InvocationHandler,实现该接口需要重写invoke方法。首先创建动态代理类,如下所示:
public class 动态代理类 implements InvocationHandler{
private Object obj;//指向被代理类
public 动态代理类(Object obj){
this.obj=obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result=method.invoke(obj, args);
return result;
}
}
之后修改客户端类的调用方式,如下:
抽象主题接口 真实主题=new真实主题类();
//创建动态代理
动态代理类 动态代理=new 动态代理类(真实主题);
//创建真实主题类的ClassLoader
ClassLoader loader=真实主题.getClass().getClassLoader();
//动态创建代理类
抽象主题接口 代理= (抽象主题接口) Proxy.newProxyInstance(loader,new Class[]{IShop.class},动态代理);
代理.实现的接口方法();
上面调用了Proxy.newProxyInstance()方法来生成动态代理类,最后调用代理.实现的接口方法会调用动态代理类的invoke方法。
了解了这些必备知识点后,我们就以Hook常用的startActivity为例进行讲解。
我们知道Activity是通过startActivity方法启动的,不了解这部分知识点可以查看《Android四大组件工作过程》一文。在startActivity方法中,实际上最终调用的是startActivityForResult方法,如下所示:
注意查看用红框圈起来的部分,这里就是我们要进行Hook的地方。可以看到,使用了mInstrumentation的execStartActivity方法来启动Activity,而这个mInstrumentation正是Activity的成员变量,因此我们可以选择Instrumentation为Hook点,用代理的Instrumentation来替代原始的Instrumentation,从而完成Hook。
首先写一个代理Instrumentation类,如下:
public class InstrumentationProxy extends Instrumentation {
private static final String TAG = "InstrumentationProxy";
Instrumentation instrumentation;
public InstrumentationProxy(Instrumentation instrumentation){
this.instrumentation = instrumentation;
}
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token,
Activity target, Intent intent, int requestCode,
Bundle options){
Log.d(TAG,"进到这里表示Hook成功:"+who);
try {
//通过反射找到Instrumentation的execStartActivity方法
Method execStartActivity = Instrumentation.class.
getDeclaredMethod("execStartActivity",Context.class,IBinder.class,
IBinder.class,Activity.class,Intent.class,int.class,Bundle.class);
return (ActivityResult)execStartActivity.invoke(instrumentation,who,contextThread,token,
target,intent,requestCode,options);
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
可以看到,InstrumentationProxy 继承了Instrumentation ,且包含了Instrumentation 的引用,并且实现了execStartActivity方法。execStartActivity方法内部通过反射找到并调用Instrumentation 的execStartActivity方法,这就用到了我们刚才复习过的动态代理模式。
写完了代理类,我们再来用InstrumentationProxy 来替换掉Instrumentation ,如下所示:
//使用InstrumentationProxy替换Instrumentation
public void replaceActivityInstrumentation(Activity activity){
try {
//得到Activity的mInstrumentation字段
Field field = Activity.class.getDeclaredField("mInstrumentation");
//取消Java的权限控制检查
field.setAccessible(true);
//得到Instrumentation对象
Instrumentation instrumentation = (Instrumentation)field.get(activity);
//创建InstrumentationProxy并注入Instrumentation对象
Instrumentation instrumentationProxy = new InstrumentationProxy(instrumentation);
//使用InstrumentationProxy替换Instrumentation
field.set(activity,instrumentationProxy);
}catch (Exception e){
e.printStackTrace();
}
}
每一步的含义都写在注释里,这里就不再赘述了。
最后在我们应用的Activity的onCreate方法中使用这个replaceActivityInstrumentation方法来进行替换。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
replaceActivityInstrumentation(this);
//打个电话试一下有没有成功
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:10086"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
运行一下,看一下Log,显示了我们在代理类中的log,成功。
我们知道Context的实现类是ContextImpl,因此最后是通过ContextImpl的startActivity方法启动Activity的,不了解这部分知识点可以查看《Context、Activity任务栈模型、WindowManager、Window以及WindowManagerService》一文。startActivity方法代码如下所示:
frameworks/base/core/java/android/app/ContextImpl.java
还是注意查看用红框圈起来的部分,这里就是我们要进行Hook的地方。可以看到,这里首先通过 ActivityThread类型的mMainThread.getInstrumentation()获得Instrumentation,然后再调用execStartActivity方法来启动Activity。因为一个进程中只有一个ActivityThread,因此我们可以选择Instrumentation为Hook点,用代理的Instrumentation来替代原始的Instrumentation,从而完成Hook。
还是用之前写好的代理类,我们直接看替换部分,如下所示:
//使用InstrumentationProxy替换Instrumentation(Hook Context)
public void replaceContextInstrumentation(){
try {
//获取ActivityThread类
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
//获取ActivityThread中定义的sCurrentActivityThread字段
Field activityThreadfield = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
//取消Java的权限控制检查
activityThreadfield.setAccessible(true);
//获得sCurrentActivityThread对象
Object currentActivityThread = activityThreadfield.get(null);
//获得ActivityThread中的mInstrumentation字段
Field mInstrumentationField = activityThreadClazz.getDeclaredField("mInstrumentation");
//取消Java的权限控制检查
mInstrumentationField.setAccessible(true);
//得到Instrumentation对象
Instrumentation mInstrumentation = (Instrumentation)mInstrumentationField.get(currentActivityThread);
//创建InstrumentationProxy并注入Instrumentation对象
Instrumentation mInstrumentationProxy = new InstrumentationProxy(mInstrumentation);
//使用InstrumentationProxy替换Instrumentation
mInstrumentationField.set(currentActivityThread,mInstrumentationProxy);
}catch (Exception e){
e.printStackTrace();
}
}
每一步的含义都写在注释里,这里依旧不再赘述了。
最后在我们应用的Activity的onCreate方法中使用这个replaceContextInstrumentation方法来进行替换。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
replaceContextInstrumentation();
//打个电话试一下有没有成功
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:10010"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(intent);
}
运行一下,看一下Log,显示了我们在代理类中的log,成功。
关于替换的时间点,也可以尝试在Activity的attachBaseContext方法中进行替换,这是因为attachBaseContext方法要优先于onCreate方法被调用。
插件化技术和热修复技术都属于动态加载技术,其中本文所讲的插件化技术被广泛地应用在各大型应用中,热修复技术可以查看前文。
相对于热修复技术主要用来修复Bug,插件化技术则更多地用于解决应用越来越庞大问题以及使功能模块解耦。需要注意的是,动态加载技术本身并没有被官方认可,属于非常规技术,随着Android版本的更新,很多方案也都会在新版本中被限制使用(例如Android 9.0开始限制非公开SDK接口访问)。
插件化的客户端由宿主和插件两个部分组成,宿主就是我们平时安装的APK,而插件一般指经过处理的APK、so和dex等文件。插件可以被宿主进行加载,甚至于有的插件也可以作为APK独立运行。这样第一次加载到内存的就只有宿主,当使用到其他插件时才会加载相应的插件到内存,从而减少内存的占用。
注:阅读以下部分需要有四大组件工作过程相关基础,不了解这部分知识点可以查看《Android四大组件工作过程》一文。
注:本节内容只针对Android 9.0前的实现方式,因为Android 9.0开始限制非公开SDK接口(经过@hide修饰的方法)访问。
核心方案思想如下:
首先在AndroidManifest.xml中注册一个占坑Activity,用来通过AMS的校验,在这之后再适时用我们的实际插件Activity来替换掉那个占坑Activity。
说完了方案思想,接下来就来讲讲具体的实际操作。
我们来看看Activity的启动,以Android8.0为例,Activity启动会调用ActivityManager.的getService方法,如下所示:
frameworks/base/core/java/android/app/ActivityManager.java
Singleton类如下:
frameworks/base/core/java/android/util/Singleton.java
通过红框部分结合Singleton类源码可以看到,getService最终返回的是一个IActivityManager单例,因此,可以使用这个IActivityManager作为Hook点。
由于Android8.0前的启动方式和之后的不太一样,但是现在很多应用还兼容到8.0以前,我们也来看看8.0之前的启动方式。
以Android7.0为例,Activity启动会调用ActivityManagerNative.的getDefault方法,如下所示:
frameworks/base/core/java/android/app/ActivityManagerNative.java
最后返回的也是单例的IActivityManager,因此,同样可以使用这个IActivityManager作为Hook点。
从前面的Hook基础我们了解到,Hook需要多次对字段进行反射操作,这里我们首先将字段反射方法进行封装,写一个字段工具类。
/**
* 字段工具类,用于Hook反射操作
*/
public class FieldUtil {
public static Object getField(Class clazz,Object target,String name) throws Exception{
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
public static Field getField(Class clazz,String name) throws Exception{
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
}
public static void setField(Class clazz,Object target,String name,Object value) throws Exception{
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(target,value);
}
}
之后我们分两步走,分别是使用占坑Activity通过AMS校验,以及还原我们的插件Activity。
新建一个占坑的Activity,并在AndroidManifest.xml中注册,我们这里就新建一个TemporarilyActivity作为占坑Activity。
定义一个替换IActivityManager的代理类,其中主要步骤都在代码注释中指出,不再赘述。
/**
* 代理类,用来替换掉IActivityManager
*/
public class IActivityManagerProxy implements InvocationHandler {
private Object activityManager;
private static final String TAG = "IActivityManagerProxy";
public IActivityManagerProxy(Object activityManager){
this.activityManager = activityManager;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG,method.getName());
//拦截startActivity方法,并获取参数args中第一个Intent对象,这个对象是原本要启动插件Activity的Intent
if ("startActivity".equals(method.getName())){
Log.d(TAG,"startActivity");
Intent intent = null;
int index = 0;
for (int i = 0;i<args.length;i++){
if (args[i] instanceof Intent){
index = i;
break;
}
}
intent = (Intent) args[index];
//新建一个占坑Intent,并且启动占坑Activity
Intent temporarilyIntent = new Intent();
String packageName = "com.example.hookstartactivity";
temporarilyIntent.setClassName(packageName,packageName+".TemporarilyActivity");
//将插件Activity的Intent保存到占坑Intent中,以便之后还原插件Activity
temporarilyIntent.putExtra(HookHelper.PlugIn_INTENT,intent);
//用占坑Intent赋值给参数args,使启动目标变为占坑Activity,用来通过AMS校验
args[index] = temporarilyIntent;
}
return method.invoke(activityManager,args);
}
}
然后用代理类IActivityManagerProxy 替换IActivityManager。
public class HookHelper {
public static final String PlugIn_INTENT = "plugIn_intent";
/**
* 使用代理类IActivityManagerProxy替换掉IActivityManager
*/
public static void hookAMS() throws Exception{
Object defaultSingleton = null;
//区分系统版本(8.0为分界线,因为Activity启动方式不一样,8.0开始直接采用AIDL来进行进程间通信)
if (Build.VERSION.SDK_INT>=26){
Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
//获取ActivityManager中的IActivityManagerSingleton字段
defaultSingleton = FieldUtil.getField(activityManagerClazz,null,
"IActivityManagerSingleton");
}else {
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
//获取ActivityManagerNative中的gDefault字段
defaultSingleton = FieldUtil.getField(activityManagerNativeClazz,null,"gDefault");
}
Class<?> singletonClazz = Class.forName("android.util.Singleton");
//获取Singleton类中的mInstance字段,类型为T
Field mInstanceField = FieldUtil.getField(singletonClazz,"mInstance");
//获取T的类型——IActivityManager
Object iActivityManager = mInstanceField.get(defaultSingleton);
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
//使用动态创建的代理类IActivityManagerProxy替换掉IActivityManager
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[] {iActivityManagerClazz},new IActivityManagerProxy(iActivityManager));
mInstanceField.set(defaultSingleton,proxy);
}
}
在Application中调用hookAMS方法。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
HookHelper.hookAMS();
}catch (Exception e){
e.printStackTrace();
}
}
}
别忘了在AndroidManifest.xml中注册。
最后按照正常方式启动这个原本没有在AndroidManifest.xml中注册过的插件Activity。
Intent intent = new Intent(MainActivity.this,PlugInActivity.class);
startActivity(intent);
这个PlugInActivity就是我们的插件Activity。将模拟器调整为Android8.0系统,可以看到系统启动的是TemporarilyActivity(占坑Activity),而不是我们代码中写的PlugInActivity(插件Activity),说明使用占坑Activity通过了AMS校验。
使用占坑Activity成功通过AMS校验之后,就需要将我们真正需要启动的插件Activity还原了,也就是我们最开始画的那个图的第三步。
我们知道在得到应用程序进程的ApplicationThread后,ApplicationThread会将启动Activity的参数封装成ActivityClientRecord,并向H类发送类型为LAUNCH_ACTIVITY的消息,且在发消息时将ActivityClientRecord传递过去,那我们就可以从这里入手。这部分代码如下:
首先是最终发送系统消息的部分。
frameworks/base/core/java/android/os/Handler.java
再看看H类的发送LAUNCH_ACTIVITY消息部分:
frameworks/base/core/java/android/app/ActivityThread.java
看上面的dispatchMessage方法,如果Callback类型的mCallback != null,就会执行下面的mCallback.handleMessage(msg),那么我们就可以把这个mCallback 作为Hook,用自定义的Callback来替换掉这个mCallback 。
自定义的Callback如下所示,其中主要步骤都在代码注释中指出,不再赘述。
/**
* 用来替换掉dispatchMessage方法中的mCallback
*/
public class HCallback implements Handler.Callback {
public static final int LAUNCH_ACTIVITY = 100;//与H类中定义的保持一致
Handler handler;
public HCallback(Handler handler){
this.handler = handler;
}
@Override
public boolean handleMessage(@NonNull Message msg) {
if (msg.what == LAUNCH_ACTIVITY){
Object r = msg.obj;
try {
//得到占坑Activity的Intent
Intent intent = (Intent) FieldUtil.getField(r.getClass(),r,"intent");
//得到在IActivityManagerProxy中保存的启动插件Activity的Intent
Intent plugInIntent = intent.getParcelableExtra(HookHelper.PlugIn_INTENT);
//将启动占坑Activity的Intent替换为启动插件Activity的Intent
intent.setComponent(plugInIntent.getComponent());
}catch (Exception e){
e.printStackTrace();
}
}
handler.handleMessage(msg);
return true;
}
}
然后用代理类HCallback 替换mCallback,还是在HookHelper中定义。
public static void hookHandler() throws Exception{
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
//获取ActivityThread中定义的private static volatile ActivityThread sCurrentActivityThread
Object currentActivityThread = FieldUtil.getField(activityThreadClass,null,
"sCurrentActivityThread");//sCurrentActivityThread表示当前的ActivityThread对象
//获取ActivityThread中定义的final H mH
Field mHField = FieldUtil.getField(activityThreadClass,"mH");
//获取当前ActivityThread中的mH对象
Handler mH = (Handler) mHField.get(currentActivityThread);
//用HCallback替换mH中的mCallback
FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
}
最后依旧是在Application中调用hookHandler方法。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
HookHelper.hookAMS();
HookHelper.hookHandler();
}catch (Exception e){
e.printStackTrace();
}
}
}
将模拟器调整为Android8.0系统运行,可以看见这次启动的是我们的插件Activity。
由于AMS和ActivityThread之间的通信采用了token来对Activity进行标识,并且此后对Activity生命周期的处理也是根据token来对Activity进行标识,我们在通信之前就用插件Activity替换回了占坑Activity,因此我们的插件Activity也是具有生命周期的。
在Android 9.0中,hook mH已经失效,因此我们可以采用Hook Instrumentation方案(当然,这个方案在9.0以前也可以使用),也就是在Hook技术一节中“Activity的startActivity方法”部分。通过“Activity的startActivity方法”一节,我们知道在startActivityForResult方法中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期,那么又是在哪里创建Activity实例的呢,请看下面的代码:
frameworks/base/core/java/android/app/ActivityThread.java中private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent)方法
红框的部分调用了mInstrumentation的newActivity方法,它的内部会用类加载器来创建Activity实例。因此我们这次的方案可以是,在Instrumentation的execStartActivity方法中使用占坑Activity来通过AMS验证,然后在mInstrumentation的newActivity方法中来还原插件Activity。
我们还是来自定义一个Instrumentation,里面有execStartActivity和newActivity两个方。首先在execStartActivity方法中将启动的插件Activity替换为占坑Activity。
public class InstrumentationProxyPlugInUse extends Instrumentation {
private Instrumentation instrumentation;
private PackageManager packageManager;
public InstrumentationProxyPlugInUse(Instrumentation instrumentation,PackageManager packageManager){
this.instrumentation = instrumentation;
this.packageManager = packageManager;
}
/**
* 将启动的插件Activity替换为占坑Activity
*/
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token,
Activity target, Intent intent, int requestCode,
Bundle options){
//查找要启动的Activity是否已经在AndroidManifest.xml中注册
List<ResolveInfo> infos = packageManager.queryIntentActivities(intent,PackageManager.MATCH_ALL);
//没有注册
if (infos == null || infos.size()==0){
//将要启动的插件Activity的ClassName保存起来,用于后面还原插件Activity
intent.putExtra(HookHelper.PlugIn_INTENT_NAME, intent.getComponent().getClassName());
//替换要启动的Activity为占坑Activity
intent.setClassName(who,"com.example.hookstartactivity.TemporarilyActivity");
}try {
//通过反射调用execStartActivity方法,使用占坑Activity通过AMS验证
Method execStartActivity = Instrumentation.class.
getDeclaredMethod("execStartActivity",Context.class,IBinder.class,
IBinder.class,Activity.class,Intent.class,int.class,Bundle.class);
return (ActivityResult)execStartActivity.invoke(instrumentation,who,contextThread,token,
target,intent,requestCode,options);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
之后我们来重写newActivity方法。先来看一下在9.0以后的newActivity方法源码,如下:
可以看到,在newActivity方法中,检查Instrumentation的mThread变量,如果为空,就会抛出一个异常,因此需要重写这个方法,如下所示:
/**
* 还原插件Activity
*/
public Activity newActivity(ClassLoader classLoader,String className,Intent intent) throws
InstantiationException,IllegalAccessException,ClassNotFoundException{
String intentName = intent.getStringExtra(HookHelper.PlugIn_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)){
return instrumentation.newActivity(classLoader,intentName,intent);
}
return instrumentation.newActivity(classLoader,className,intent);
}
在newActivity方法中创建了之前保存的插件Activity,从而完成了还原插件Activity,并且因为我们return了instrumentation.newActivity,因此也不会抛出mThread为空的异常信息。
然后用代理类InstrumentationProxyPlugInUse 替换Instrumentation ,还是在HookHelper中定义。
/**
* 用InstrumentationProxyPlugInUse替换掉Instrumentation
*/
public static void hookInstrumentation(Context context) throws Exception{
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
//获取ContextImpl类的ActivityThread类型的mMainThread字段
Field mainThreadField = FieldUtil.getField(contextImplClass,"mMainThread");
//获取当前上下文环境的ActivityThread对象
Object activityThread = mainThreadField.get(context);
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
//获取ActivityThread类中的mInstrumentation字段
Field instrumentationField = FieldUtil.getField(activityThreadClass,"mInstrumentation");
//用InstrumentationProxyPlugInUse替换掉Instrumentation
FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",
new InstrumentationProxyPlugInUse((Instrumentation)instrumentationField.get(activityThread),
context.getPackageManager()));
}
最后依旧是在Application中调用hookInstrumentation方法,之后将模拟器调整为Android9.0以上系统运行,可以看见这次启动的是我们的插件Activity。
最后总结一下,无论系统版本是多少,核心思想都是用是一样的,即是首先在AndroidManifest.xml中注册一个占坑Activity,用来通过AMS的校验,在这之后再适时用我们的实际插件Activity来替换掉那个占坑Activity。
Service的特点在于它有startService和bindService两套机制,因此对于Service的插件化,需要保证这两点都能正常工作。我们以startService为例做具体讲解,bindService和startService的差别在于调用的方法名不同,以及被调用的位置不同而已,思路都是一样的。
我们来看看Activity和Service的异同点:
相同点:都是借助于Context来完成自身的启动,通知AMS,然后AMS通知App进程要启动哪个组件,最后通过ActivityThread和H类转发消息。
不同点:①用户和界面的交互会影响Activity的生命周期,插件的生命周期需要交给系统管理;而Service的生命周期不受用户影响,可以由开发者管理生命周期。
②在Standard模式(默认)下,即使是同一个Activity被启动多次,也会在栈顶放置这个Activity的多个实例;而多次启动同一个Service并不会启动多个Service实例,只有一个实例。
③ActivityThread最终是通过Instrumentation启动一个Activity的;而ActivityThread启动Service并不借助于Instrumentation。
由此可以看到,Service插件化无法使用Hook Instrumentation方案实现,而且为了保证它的优先级,需要用一个真正的Service来实现,而不仅仅是一个占坑用的。我们可以使用代理分发的方式来实现。所谓的代理分发可以理解为:当启动插件Service的时候,先启动代理Service,然后在代理Service运行起来后,在它的onStartCommand等方法里进行分发,执行插件Service的onCreate等方法。
我们还是分两步走。
首先我们就在AndroidManifest.xml中注册一个代理Service,名为ProxyService 。
<service android:name=".ProxyService"/>
使用我们之前实现Activity插件化时定义的替换IActivityManager的代理类,在invoke方法中添加下面的代码,其中主要步骤都在代码注释中指出,不再赘述。
//拦截startService方法,并获取参数args中第一个Intent对象,这个对象是原本要启动插件Service的Intent
if ("startService".equals(method.getName())){
Log.d(TAG,"startService");
Intent intent = null;
int index = 0;
for (int i = 0;i<args.length;i++){
if (args[i] instanceof Intent){
index = i;
break;
}
}
intent = (Intent) args[index];
//新建一个代理Intent,并且启动代理Service
Intent proxyIntent = new Intent();
String packageName = "com.example.hookstartactivity";
proxyIntent.setClassName(packageName,packageName+".ProxyService");
//将代理Service的Intent保存到代理Intent中,以便之后进行分发
proxyIntent.putExtra(ProxyService.PlugIn_SERVICE,intent.getComponent().getClassName());
//用代理Intent赋值给参数args,使启动目标变为代理Service,用来通过AMS校验
args[index] = proxyIntent;
Log.d(TAG,"Hook成功");
}
可以看到,无论是步骤还是原理,都和之前实现Activity插件化时类似,同时,替换系统IActivityManager这步不变,还是用之前那个,这里就不再写了。最后在Application中调用hookAMS方法,之后按照正常方式启动这个原本没有在AndroidManifest.xml中注册过的插件Service。
Intent intent = new Intent(MainActivity.this,PlugInService.class);
startService(intent);
这个PlugInService就是我们的插件Service。运行一下,可以看到系统启动的是ProxyService(代理Service),而不是我们代码中写的PlugInService(插件Service),说明使用代理Service通过了AMS校验。
我们说过,代理分发就是在代理Service运行起来后,在它的onStartCommand等方法里进行分发,执行插件Service的onCreate等方法,那么我们就来看看这部分的代码,如下:
public class ProxyService extends Service {
private static final String TAG = "ProxyService";
public static final String PlugIn_SERVICE = "plugIn_service";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG,"onCreate");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG,"onStartCommand");
//ProxyService需要长时间对Service进行分发处理
//在参数条件不满足时需要返回START_STICKY,这样ProxyService会重新被创建并执行onStartCommand方法
if (null == intent || !intent.hasExtra(PlugIn_SERVICE)){
return START_STICKY;
}
String serviceName = intent.getStringExtra(PlugIn_SERVICE);
if (null == serviceName){
return START_STICKY;
}
//创建PlugInService并反射调用PlugInService的attach方法
Service plugInService = null;
try {
Class activityThreadClazz = Class.forName("android.app.ActivityThread");
Method getActivityThreadMethod = activityThreadClazz.getDeclaredMethod("getApplicationThread");
getActivityThreadMethod.setAccessible(true);
//得到ActivityThread对象
Object activityThread = FieldUtil.getField(activityThreadClazz,null,"sCurrentActivityThread");
//得到applicationThread对象
Object applicationThread = getActivityThreadMethod.invoke(activityThread);
Class iInterfaceClazz = Class.forName("android.os.IInterface");
Method asBinderMethod = iInterfaceClazz.getDeclaredMethod("asBinder");
asBinderMethod.setAccessible(true);
//反射调用applicationThread的asBinder方法得到token对象(IBinder类型)
Object token = asBinderMethod.invoke(applicationThread);
Class serviceClazz = Class.forName("android.app.Service");
Method attachMethod = serviceClazz.getDeclaredMethod("attach", Context.class,
activityThreadClazz,String.class,IBinder.class, Application.class,Object.class);
attachMethod.setAccessible(true);
Object defaultSingleton = null;
//区分系统版本(8.0为分界线,8.0开始直接采用AIDL来进行进程间通信),目的是获取IActivityManager
if (Build.VERSION.SDK_INT>=26){
Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
//获取ActivityManager中的IActivityManagerSingleton字段
defaultSingleton = FieldUtil.getField(activityManagerClazz,null,
"IActivityManagerSingleton");
}else {
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
//获取ActivityManagerNative中的gDefault字段
defaultSingleton = FieldUtil.getField(activityManagerNativeClazz,null,"gDefault");
}
Class<?> singletonClazz = Class.forName("android.util.Singleton");
//获取Singleton类中的mInstance字段,类型为T
Field mInstanceField = FieldUtil.getField(singletonClazz,"mInstance");
//获取T的类型——IActivityManager
Object iActivityManager = mInstanceField.get(defaultSingleton);
//反射得到插件Service
plugInService = (Service)Class.forName(serviceName).newInstance();
//反射执行插件Service的attach方法,并传入此前得到的参数,最后执行插件Service的onCreate方法来完成代理分发
attachMethod.invoke(plugInService,this,activityThread,
intent.getComponent().getClassName(),token,getApplication(),iActivityManager);
plugInService.onCreate();
}catch (Exception e){
e.printStackTrace();
//在出现异常时需要返回START_STICKY,这样ProxyService会重新被创建并执行onStartCommand方法
return START_STICKY;
}
//在代码执行完毕时需要返回START_STICKY,这样ProxyService会重新被创建并执行onStartCommand方法
plugInService.onStartCommand(intent, flags, startId);
return START_STICKY;
}
}
可以看到主要就是做了三件事,①代理Service需要长时间对Service进行分发处理,所以在参数条件不满足、出现异常和代码执行完毕时需要返回START_STICKY,这样代理Service就会被重新创建并且执行onStartCommand方法;②创建插件Service,并反射调用插件Service的attach方法;③进行代理分发,执行插件Service的onCreate方法。
ContentProvider插件化的精髓在于分发,外界在使用App提供的ContentProviderA时,
只知道发送给一个在宿主AndroidManifest中声明过的ContentProviderA。而我们要做的是,在ContentProvider中接收到请求,再二次分发给插件中相应的ContentProvider。
我们看一下VirtualAPK框架官方描述的实现ContentProvider插件化原理:“动态代理IContentProvider,拦截provider相关的请求,将其中转给 Provider Runtime去处理,Provider Runtime会接管系统的所有操作。”,由于ContentProvider插件化并不十分常用,并且实现它的框架也不多,那么我们就以VirtualAPK对ContentProvider插件化实现为例进行说明(使用版本:0.9.8.6)。
地址:VirtualAPK
看一下官方对于VirtualAPK初始化相关的描述:
再看看这个PluginManager的描述:
我们就来看看这个PluginManager类里面的loadPlugin方法,如下所示:
CoreLibrary/src/main/java/com/didi/virtualapk/PluginManager.java
红框处调用createLoadedPlugin方法来创建LoadedPlugin对象,我们来看看LoadedPlugin的构造方法。
CoreLibrary/src/main/java/com/didi/virtualapk/internal/LoadedPlugin.java
可以看到,LoadedPlugin的构造方法主要用来创建一些类型的对象,比如PackageInfo、Resources、ClassLoader等,以及创建四大组件相关的数据结构,比如Map等。红框处会创建插件的上下文PluginContext。
讲完了初始化,我们对VirtualAPK有了一个初步的了解。那么接下来就来看看ContentProvider插件化部分。
还是来看代码,如下:
App/src/main/java/com/didi/virtualapk/MainActivity.java
在Activity启动前VirtualAPK就通过Hook Instrumentation的形式,在VAInstrumentation的callActivityOnCreate方法中(实际调用的是injectActivity方法)用PluginContext替换了ContextWrapper的成员变量mBase。在红框处的getContentResolver实际上调用的是PluginContext的getContentResolver方法,如下:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/PluginContext.java
可以看见,getContentResolver方法中创建了PluginContentResolver并传入了宿主的Context,因此当我们调用getContentResolver的query方法时,实际上是调用了PluginContentResolver的query方法。我们由Content Provider的启动过程可知,query方法会调用ContentResolver的acquireUnstableProvider,这个PluginContentResolver也复写了acquireUnstableProvider方法,如下:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/PluginContentResolver.java
可以看到,方法中会查找插件中是否有匹配的ContentProvider,如果没有就调用系统ContentResolver的acquireUnstableProvider方法,有的话会调用PluginManager的getIContentProvider方法。
我们来看看PluginManager的getIContentProvider方法,如下所示:
CoreLibrary/src/main/java/com/didi/virtualapk/PluginManager.java
看看这个hookIContentProviderAsNeeded方法写的是什么,如下:
稍微有一点长,我们用了三个彩色框画出了三个重点的地方。首先由这个方法的名字就可以看出来,它是用来Hook IContentProvider的。先来看蓝框的部分,它首先获得了代理ContentResolver的Uri,然后又调用了ContentResolver的call方法,其中mContext是宿主的Context,因此这里调用的就是宿主的ContentResolver的call方法,用于得到IContentProvider。然后看黄框的地方,在这里获取了ActivityThread的mProviderMap。之后就是遍历mProviderMap找到匹配的ContentProvider。最后红框部分,使用代理IContentProviderProxy替换掉了IContentProvider,从而完成Hook IContentProvider。
那么接下来就来看看这个IContentProviderProxy。在IContentProviderProxy的wrapperUri方法中实现了替换Uri的操作,如下:
CoreLibrary/src/main/java/com/didi/virtualapk/delegate/IContentProviderProxy.java
可以看见在第一个红框处得到了一个新的Uri,看看红框里的方法具体是什么:
原来是获取插件的Uri并封装成StringBuilder,然后对StringBuilder进行拼接得到的Uri。回到上面的方法,在第二个红框处替换Uri,这样我们启动一个插件ContentProvider时就会先启动代理ContentProvider了。
看一下AndroidManifest.xml中,代理ContentProvider已经注册了,如下:
CoreLibrary/src/main/AndroidManifest.xml
当我们调用ContentProvider的query方法时,实际上会调用RemoteContentProvider的query方法,那么我们就来看看RemoteContentProvider的query方法。
CoreLibrary/src/main/java/com/didi/virtualapk/delegate/RemoteContentProvider.java
红框处会通过RemoteContentProvider的getContentProvider方法得到一个ContentProvider,并且当ContentProvider不为空时,调用它的query方法。
RemoteContentProvider的getContentProvider方法如下所示:
方法还是稍微有些长,我们用红框画出了4个重点的地方。首先可以看到,传入的参数是插件uri,并且被解析为auth,这第一个红框就是会从缓存中读取是否有匹配auth的插件ContentProvider,如果没有的话就会在第二个红框的位置加载插件APK,并且会从已加载APK中得到匹配auth的ProviderInfo,也就是第三个红框部分。如果得到了这个匹配auth的ProviderInfo,那么就可以根据这个ProviderInfo在第四个红框的地方创建插件ContentProvider,之后再调用插件ContentProvider的attachInfo方法,attachInfo方法内部会为插件ContentProvider配置参数并且调用它的onCreate方法,这样就启动了插件ContentProvider。最后再将这个新创建的插件ContentProvider加入缓存,避免重复创建插件ContentProvider(参见“ClassLoader与热修复原理”一节)。
我们知道BroadcastReceiver分为静态广播和动态广播两种,首先来简单说一下它们的区别:①静态广播需要在AndroidManifest.xml中注册。 因为安装和Android系统重启时,PMS都会解析App中的AndroidManifest.xm文件,所以静态广播的注册信息存在于PMS中;②动态广播是通过写代码的方式进行注册,通过调用Context的registerReceiver方法,最终调用AMS的registerReceiver方法,所以动态广播的注册信息存在于AMS中。
静态广播和动态广播的区别仅在于上述注册方式的不同,之后就都一样了,包括发送广播和接收广播,如下:①发送广播,也就是Context的sendBroadcast方法,最终会调用AMS的broadcastlntent方法,把要发送的广播告诉AMS;②AMS在收到上述信息后,搜索AMS和PMS中保存的广播,看哪些广播符合条件,然后通知App进程启动这些广播,也就是调用这些广播的onReceive方法。
无论是发送广播,还是接收广播,都携带一个筛选条件:intent filter。在发送广播时,要设置intent-filter,这样AMS才知道要通知哪些广播符合intent-filter。
前面说过,静态广播会在AndroidManifest.xml中设置标签,BroadcastReceiver根据这个标签中的值来接收“感兴趣”的广播,这样的话,采取类似Activity插件化的Hook IActivityManager方案用一个占坑BroadcastReceiver来接收广播是不行的,因为我们无法预料插件中静态注册的广播的标签,这样占坑BroadcastReceiver是无法接收到“感兴趣”广播的。虽然静态注册广播的标签无法动态设置,但是动态注册广播是可以动态设置IntentFilter的,那么我们就可以将静态注册广播全部转换为动态注册来处理。思路如下:
①由于PMS只能读取宿主App的AndroidManifest.xml文件,读取其中的静态广播并注册,那么我们就可以通过反射,手动控制PMS读取插件的AndroidManifest.xml中声明的静态广播列表;
②遍历这个静态广播列表,使用插件的classLoad加载列表中的每个广播类,实例化成一个对象,然后作为动态广播注册到 AMS中。
来看看在VirtualAPK中是怎么实现将静态注册广播转为动态注册广播的,还是回到之前我们曾经看过的LoadedPlugin的构造方法,如下所示:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/LoadedPlugin.java
代码不长,我们来分析一下。receivers用于存储插件中静态注册的BroadcastReceiver信息。之所以receivers的泛型类型是ActivityInfo,这是因为PackageParser在解析时将标签当作标签处理,所以解析得到的BroadcastReceiver信息会存储在ActivityInfo中。for循环内第一行代码将获取到的BroadcastReceiver信息存储到receivers中。下一步是将一个对象转换为BroadcastReceiver类型,那么是将什么转换为BroadcastReceiver类型呢,看一下cast方法传入,原来是根据插件BroadcastReceiver的类名,用ClassLoader加载并创建类型为Object的对象。最后再调用宿主Context的registerReceiver方法,完成插件BroadcastReceiver的注册。
Android资源文件分为两类,第一类是res目录下存放的可编译的资源文件。编译时,系统会自动在R.java中生成资源文件的十六进制值,访问时使用Context的getResources方法,得到Resources对象,进而通过Resources的getXXX方法得到各种资源。第二类是assets 目录下存放的原始资源文件。因为apk在编译的时候不会编译assets下的资源文件,所以无法通过R.xx的方式访问它们,又因为apk下载后不会解压到本地,所以也无法直接读取到assets的绝对路径,这时候就只能借助AssetManager类的open方法来获取assets目录下的文件资源了。
由于AssetManager来源于Resources类的getAssets方法,那么我们就可以以Resources为中心构建资源的插件化方案。
在VirtualAPK中使用了两种方案来实现资源的插件化,一种是合并资源方案,即将插件的资源全部添加到宿主的Resources中,这种方案下插件可以访问宿主的资源;另一种是构建插件资源方案,即每个插件都构造出独立的Resources,这种方案下插件就不可以访问宿主资源了。
我们来看看在VirtualAPK中是怎么实现,依旧回到之前我们曾经看过的LoadedPlugin中,如下所示:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/LoadedPlugin.java
一共有两个分支,分别是合并资源方案和构建插件资源方案。我们先看看合并资源方案的实现,如下:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/ResourcesManager.java
由于android 7.0 新增私有目录访问权限,所以需要针对7.0前后做两套方案。
首先看7.0之前的实现方式,简单说来就是调用ResourcesManager的createResourcesSimple方法,其内部会先得到包含宿主资源的AssetManager,再通过反射调用AssetManager的assAssetPath来添加插件资源,返回新的Resource,之后再通过Hook的方式用新的Resource替换掉之前的Resource。
再来看看从7.0开始的实现方式,也就是createResourcesForN里。由于方法过长,我们来总结下方法里都做了什么:首先是修改ApplicationInfo.splitSourceDirs,将未添加的插件APK路径添加进去并且反射给PackageInfo,设置合并之后的splitSourceDirs,将插件资源路径添加进去,然后获取ResourceImpl及其配置的映射,最后获取添加插件资源路径后的Resource对象,并且更新每个插件apk对应的LoadedPlugin中的Resources。
看完了合并资源方案的实现,再来看看构建插件资源方案。可以看到先是调用了createAssetManager方法来创建AssetManager,之后再创建Resources,并且将AssetManager作为参数传了进去。
那么这个createAssetManager方法是怎么创建的AssetManager呢?现在就来看一下:
可以看到,首先动态创建了AssetManager,然后反射调用了AssetManager的addAssetPath方法来加载插件。因为AssetManager中只包含了插件的资源,所以新创建的Resources就是插件的资源。
之前在讲热修复时,说到加载 so 有两种方法,如下所示:
以上两种方法最终都会调用 Android 底层的dlopen来打开so文件。其中第1种方法是使用so的最常规的方式,而第2种方法,我们可以把so放到服务器上,当App下载so到手机后,再加载so。这其实就是 so 的动态加载技术。
在VirtualAPK中使用了将so插件插入到NativeLibraryElement数组中,并且将存储so插件的文件添加到NativeLibraryDirectories集合中的方式来实现so的插件化。
我们还是来看看在VirtualAPK中是怎么实现,依旧回到之前我们曾经看过的LoadedPlugin中,如下所示:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/LoadedPlugin.java
红框处创建了用于加载插件的DexClassLoader,并且在后面作为参数传进了DexUtil的insertDex方法里。
来看看DexUtil的insertDex方法。
CoreLibrary/src/main/java/com/didi/virtualapk/internal/utils/DexUtil.java
前三行代码将宿主和插件的DexElements合并得到allDexElements,之后通过反射用allDexElements替换了dexElements。之后就是核心代码insertNativeLibrary方法。
来看看insertNativeLibrary方法。
由于Android5.1之前实现较为简单,所以我们就以实现起来复杂的Android版本大于5.1的情况为例。
第一个红框作为一个flag,用于避免重复插入so,之后在第二个红框处获取宿主的PathList。在第三个红框的地方得到了宿主存储so文件的List集合nativeLibraryDirectories后,将插件存储so文件添加到nativeLibraryDirectories中。两个黄框分别获取了宿主的nativeLibraryPathElements和插件的nativeLibraryPathElements。在蓝框部分得到插件的nativeLibraryPathElements类型,并创建这个类型的数组allNativeLibraryPathElements,然后将宿主的nativeLibraryPathElements复制到allNativeLibraryPathElements中,之后遍历插件的nativeLibraryPathElements,将so也添加到这个allNativeLibraryPathElements中,allNativeLibraryPathElements就有了宿主和插件全部的nativeLibraryPathElements。最后通过反射,用allNativeLibraryPathElements替换掉宿主的nativeLibraryDirectories,这样就完成了so的插件化。
总结:AMS负责管理四大组件的生命周期以及各种调度工作,hook它可以实现对插件四大组件的管理及调度;PMS负责管理系统中安装的所有App,hook它是为了让插件以为自己已经被安装;Handler是系统向插件传递消息的一个桥梁,hook它是为了把系统发向宿主的消息转发给插件。
在实际项目中,相比于组件化,插件化的各个业务模块是一个个打包好的apk文件,它们被放在宿主App的assets目录下,这样,发版后如果有某个模块更新,那么只需要重新打包这个模块的代码,生成增量包放在服务器上供用户下载就可以了。