字节码替换
Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换
插件化两种方式:
ProxyActivity代理
代理方式的关键总结起来有下面两点:
1,ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。
生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。
2,PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。
该方式有几个明显缺点:
插件中的Activity必须继承PluginActivity,开发侵入性强。
如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。
插件中需要小心处理Context,容易出错。 如果想把之前的模块改造成插件需要很多额外的工作。
hook系统启动Activity的过程
基于Activity启动流程,使用hook思想代理startActivity这个方法,使用占坑的方式,在AndroidManifest中固定写死一个Activity,我们在启动我们插件apk的时候使用它去系统层校检合法性,然后等真正创建Activity的时候再通过hook思想拦截Activity的创建方法,提前将信息更换回来创建真正的插件apk。
一,Hook IActivityManager实现Activity插件化
2.1 AndroidManifest.xml中注册占坑Activity
2.2 使用占坑Activity通过AMS校验
仔细想一下,我们的目标其实很简单,就是在AMS执行startActivity()方法之前,将要启动的插件Activity替换成占坑Activity。而调用AMS的startActivity()方法是由AMS在本地的代理对象来完成的,所以我们就把目光聚焦到了这个AMS本地代理对象。
在Activity启动流程源码解析中我们提到,关于获取AMS代理对象的方式,Android 8.0和之前的版本是有一些差别的。Android 8.0采用的是AIDL的实现方式获取AMS的代理对象,而Android 8.0之前是通过ActivityManagerNative.getDefault()来获取AMS的代理对象的。不过这个对我们影响不是很大,做好兼容处理就行。
Android 8.0源码
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
Android 8.0之前源码
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
由上述源码可知,不管是Android 8.0,还是Android之前,最终返回的AMS本地代理对象都是IActivityManager类型的对象。因此,IActivityManager就是一个很好的Hook点,我们只需要去拦截它的startActivity()方法,并且将要启动的插件Activity替换成占坑Activity。为了简单起见,省略了加载插件Activity的过程,直接创建了一个PluginActivity来代表已经加载进来的插件Activity,并且没有在AndroidManifest.xml中进行注册。同时,由于IActivityManager又是一个接口,所以我们完全可以采用动态代理来拦截它的startActivity()方法,具体实现如下:
public class IActivityManagerProxy implements InvocationHandler {
private static final String TAG = "IActivityManagerProxy";
private Object mActivityManager;
public IActivityManagerProxy(Object activityManager) {
this.mActivityManager = activityManager;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
Log.e(TAG, "invoke startActivity");
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
// 获取启动PluginActivity的Intent
Intent pluginIntent = (Intent) args[index];
// 新建用来启动StubActivity的Intent
Intent stubIntent = new Intent();
stubIntent.setClassName("com.lxbnjupt.pluginactivitydemo",
"com.lxbnjupt.pluginactivitydemo.activity.StubActivity");
// 将启动PluginActivity的Intent保存在subIntent中,便于之后还原
stubIntent.putExtra(HookHelper.PLUGIN_INTENT, pluginIntent);
// 通过stubIntent赋值给args,从而将启动目标变为StubActivity,以此达到通过AMS校验的目的
args[index] = stubIntent;
}
return method.invoke(mActivityManager, args);
}
}
之后,创建代理类IActivityManagerProxy,并且使用IActivityManagerProxy去替换原来的IActivityManager即可:
/**
* Hook IActivityManager
*
* @throws Exception
*/
public static void hookAMS() throws Exception {
Log.e(TAG, "hookAMS");
Object singleton = null;
if (Build.VERSION.SDK_INT >= 26) {
Class<?> activityManageClazz = Class.forName("android.app.ActivityManager");
// 获取ActivityManager中的IActivityManagerSingleton字段
Field iActivityManagerSingletonField = ReflectUtils.getField(activityManageClazz, "IActivityManagerSingleton");
singleton = iActivityManagerSingletonField.get(activityManageClazz);
} else {
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
// 获取ActivityManagerNative中的gDefault字段
Field gDefaultField = ReflectUtils.getField(activityManagerNativeClazz, "gDefault");
singleton = gDefaultField.get(activityManagerNativeClazz);
}
Class<?> singletonClazz = Class.forName("android.util.Singleton");
// 获取Singleton中mInstance字段
Field mInstanceField = ReflectUtils.getField(singletonClazz, "mInstance");
// 获取IActivityManager
Object iActivityManager = mInstanceField.get(singleton);
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
// 获取IActivityManager代理对象
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));
// 将IActivityManager代理对象赋值给Singleton中mInstance字段
mInstanceField.set(singleton, proxy);
}
2.3 还原插件Activity
在AMS执行startActivity()方法之前,我们使用占坑Activity替换插件Activity,从而通过了AMS的校验。但是,我们真正要启动的是插件Activity,那么势必还是要替换回来的。那么,回想一下Activity的启动流程,我们的目标就换成了要在ActivityThread执行handleLaunchActivity()方法之前,将占坑Activity替换回插件Activity。ActivityThread会通过Handler内部类H将代码的逻辑切换到主线程中,H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,我们可以将H的mCallback作为Hook点。
public class HCallback implements Handler.Callback {
private static final int LAUNCH_ACTIVITY = 100;
Handler mHandler;
public HCallback(Handler handler) {
mHandler = handler;
}
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
Object obj = msg.obj;
try {
// 获取启动SubActivity的Intent
Intent stubIntent = (Intent) ReflectUtils.getField(obj.getClass(), "intent", obj);
// 获取启动PluginActivity的Intent(之前保存在启动SubActivity的Intent之中)
Intent pluginIntent = stubIntent.getParcelableExtra(HookHelper.PLUGIN_INTENT);
// 将启动SubActivity的Intent替换为启动PluginActivity的Intent
stubIntent.setComponent(pluginIntent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
mHandler.handleMessage(msg);
return true;
}
}
由上述代码可知,HCallback实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动占坑Activity的Intent替换为启动插件Activity的Intent。 之后,我们创建HCallback的实例,并且用它来替换H的mCallback:
/**
* Hook ActivityThread中Handler成员变量mH
*
* @throws Exception
*/
public static void hookHandler() throws Exception {
Log.e(TAG, "hookHandler");
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
// 获取ActivityThread中成员变量sCurrentActivityThread字段
Field sCurrentActivityThreadField = ReflectUtils.getField(activityThreadClazz, "sCurrentActivityThread");
// 获取ActivityThread主线程对象
Object currentActivityThread = sCurrentActivityThreadField.get(activityThreadClazz);
// 获取ActivityThread中成员变量mH字段
Field mHField = ReflectUtils.getField(activityThreadClazz, "mH");
// 获取ActivityThread主线程中Handler对象
Handler mH = (Handler) mHField.get(currentActivityThread);
// 将我们自己的HCallback对象赋值给mH的mCallback
ReflectUtils.setField(Handler.class, "mCallback", mH, new HCallback(mH));
}
二,Hook Instrumentation实现Activity插件化
3.1 替换和还原插件Activity
Hook Instrumentation实现Activity插件化的思想同样也是使用占坑Activity,与Hook IActivityManager不同的地方是替换和还原的地方不同而已,而且相对来说会稍微简洁一些。
由Activity启动流程可知,启动一个Activity的过程中会调用到Instrumentation的execStartActivity()方法,在此方法中我们可以用占坑Activity来替换插件Activity,以此来通过AMS的验证。然后,在回到ActivityThread主线程的performLaunchActivity方法中时,会调用Instrumentation的newActivity方法创建Activity,在此方法中我们可以用插件Activity来替换占坑Activity。
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;
public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
this.mInstrumentation = instrumentation;
this.mPackageManager = packageManager;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// 查找要启动的Activity是否已经在AndroidManifest.xml中注册
List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
if (infos == null || infos.size() == 0) {
// 要启动的Activity没有注册,则将启动它的Intent保存在Intent中,便于之后还原
intent.putExtra(HookHelper.PLUGIN_INTENT, intent.getComponent().getClassName());
// 替换要启动的Activity为StubActivity
intent.setClassName(who, "com.lxbnjupt.pluginactivitydemo.activity.StubActivity");
}
try {
Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
// 通过反射调用execStartActivity方法,将启动目标变为StubActivity,以此达到通过AMS校验的目的
return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.PLUGIN_INTENT);
if (!TextUtils.isEmpty(intentName)) {
// 还原启动目标Activity
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
接着,我们需要创建InstrumentationProxy对象,并且让其替换主线程中的Instrumentation对象即可:
/**
/**
* Hook Instrumentation
*
* @param context 上下文环境
* @throws Exception
*/
public static void hookInstrumentation(Context context) throws Exception {
Log.e(TAG, "hookInstrumentation");
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
// 获取ActivityThread中成员变量sCurrentActivityThread字段
Field sCurrentActivityThreadField = ReflectUtils.getField(activityThreadClazz, "sCurrentActivityThread");
// 获取ActivityThread中成员变量mInstrumentation字段
Field mInstrumentationField = ReflectUtils.getField(activityThreadClazz, "mInstrumentation");
// 获取ActivityThread主线程对象(应用程序启动后就会在attach方法中赋值)
Object currentActivityThread = sCurrentActivityThreadField.get(activityThreadClazz);
// 获取Instrumentation对象
Instrumentation instrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
PackageManager packageManager = context.getPackageManager();
// 创建Instrumentation代理对象
InstrumentationProxy instrumentationProxy = new InstrumentationProxy(instrumentation, packageManager);
// 用InstrumentationProxy代理对象替换原来的Instrumentation对象
ReflectUtils.setField(activityThreadClazz, "mInstrumentation", currentActivityThread, instrumentationProxy);
}