首选,当我们启动一个activity,在没有在配置文件注册的前提下,会抛出以下异常:
以下分析基于9.0源码,兼容代码可自行下载完整代码。
一、异常分析
那么首先我们需要分析这个异常是在哪里出现的?源码跟踪如下:
在checkStartActivityResult()方法中就能看到上面的异常信息。那么如何才能不让系统报这个异常呢,那就需要在这个方法执行之前做些手脚了。查看系统源码,在执行checkStartActivityResult()方法之前,会执行AMS检查的方法,并把检查结果传给了这个方法,如下:
二、hook系统AMS检查
有了上面的异常分析,我们得到这样的结论,只要AMS检查通过,就不会有下面的异常,怎么样才能让它检查通过呢?继续分析源码
1 、ActivityManager.getService()是什么
对于不同该系统版本,被定义的文件类型不一样,在26版本之前,它是一个java文件。26及之后版本它是一个aidl文件,无论如何它都是一个接口,里面都会有startActivity()方法。
2、startActivity()参数分析
最重要的一个参数就是第3个参数,携带我们的跳转信息。那么我们就可以在执行检查的时候拦截原来没有注册的activity替换成已经注册的代理activity,重新封装成intent,作为参数。这样就会检查通过。那么只有动态代理才能在系统执行之前,进行方法拦截。
三、hook系统EXECUTE_TRANSACTION消息处理
当完成了ams的检查,下一步就是在系统启动activity之前,我们要把intent信息替换成我们目标intent。这样才能跳转到我们想要的界面。继续分析源码
1 、拦截什么?
ActivityThread源码1806行就是我们activity即将要启动的时刻,我们不能在方法中拦截,而是在方法之前拦截,这是一个handler消息回调方法。所以可以在消息分发的地方进行拦截我们需要的EXECUTE_TRANSACTION消息。
2 、handler消息分发
进入Hander源码97行开始,最终发现是通过mCallback进行消息分发的,也就是说只要我们将mCallback替换成自己的Callback,就可以拦截系统的消息,拿到系统消息后进一步做处理。
四、hook实现
1 、hook系统的AMS检查
private void hookAMS_up_26() throws Exception {
//1-------------拿到IActivityManager类 需要hook的类
Class<?> IActivityManagerClass = Class.forName("android.app.IActivityManager");
//2-------------拿到ActivityManager
Class<?> activityManagerClass = Class.forName("android.app.ActivityManager");
//以下为了拿到IActivityManager对象 ,因为后续仍然需要它进行invoke public static IActivityManager getService()
Method getServiceMethod = activityManagerClass.getMethod("getService");
getServiceMethod.setAccessible(true);
final Object activityManagerObj = getServiceMethod.invoke(null);
//本质就是IActivityManager
Object activityManagerProxy = Proxy.newProxyInstance(this.getClassLoader(),
new Class[]{IActivityManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
//跳转到代理activity
Intent intent = new Intent();
intent.setClass(BaseApp.this, ProxyActivity.class);
//并且将TestActivity信息携带过去
Intent preIntent = (Intent) args[2];
intent.putExtra("preIntent", preIntent);
//第三个参数就是intent
args[2] = intent;
}
//继续执行系统的方法
return method.invoke(activityManagerObj, args);
}
});
//拿到ActivityManager类
Class<?> activityManagerClass2 = Class.forName("android.app.ActivityManager");
//3-------------拿到类中的IActivityManagerSingleton属性
Field iActivityManagerSingleton = activityManagerClass2.getDeclaredField(
"IActivityManagerSingleton");
iActivityManagerSingleton.setAccessible(true);
//拿到属性所属的类对象---》Singleton
Object singletonObj = iActivityManagerSingleton.get(null);
//4------------拿到Singleton类
Class<?> singletonClass = Class.forName("android.util.Singleton");
//拿到类中的mInstance属性,mInstance就是系统的IActivityManager对象
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
//授权
mInstanceField.setAccessible(true);
//5-------------最终是将IActivityManagerClass类对象替换成我们的代理对象
//第一个参数,要操作的类对象 第二个参数替换的类对象
mInstanceField.set(singletonObj, activityManagerProxy);
}
2、hook系统消息回调
/**
* 8.0及以上
*/
private class Callback_Up_26 implements Handler.Callback {
private Handler mH;
public Callback_Up_26(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case EXECUTE_TRANSACTION:
try {
/*
1、mIntent所属对象在ClientTransaction
源码: private List<ClientTransactionItem> mActivityCallbacks;
*/
Class<?> clientTransactionClass = Class.forName("android.app" +
".servertransaction" +
".ClientTransaction");
Field mActivityCallbacksField = clientTransactionClass.getDeclaredField(
"mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
/*
2、拿到mActivityCallbacks所属对象,就是msg.obj;
源码:final ClientTransaction transaction = (ClientTransaction) msg.obj;
*/
Object transactionObj = msg.obj;
//数组中的第1个就是LaunchActivityItem对象
List mActivityCallbacksObj =
(List) mActivityCallbacksField.get(transactionObj);
if (mActivityCallbacksObj == null || mActivityCallbacksObj.size() == 0) {
return false;
}
Class<?> launchActivityItemClass = Class.forName("android.app" +
".servertransaction" +
".LaunchActivityItem");
Field mIntentField = launchActivityItemClass.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
//判断是不是launchActivityItem类型
if (!launchActivityItemClass.isInstance(mActivityCallbacksObj.get(0))) {
return false;
}
//3、要替换的intent对象
Intent intentNow = (Intent) mIntentField.get(mActivityCallbacksObj.get(0));
//我们的目标intent
Intent preIntent = intentNow.getParcelableExtra("preIntent");
if (preIntent != null) {
mIntentField.set(mActivityCallbacksObj.get(0), preIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
}
最后 return false 的意义是继续交给系统进行消息分发,不能返回true。完成以上两步骤,运行发现,跳转到本类没有在配置文件中注册的activity是没有任何问题的。当我们尝试跳转到插件的activity,发现抛出如下异常:
五、dex合并
1、异常产生原因
进入ActivityThread源码2829行开始,伪代码如下:
这里还有两个操作,首先拿到类加载器,然后通调用newActivity方法,通过类加载器创建activity对象。
(1)类加载器
跟踪getClassLoader()方法,最终可在源码ClassLoader,224行返回PathClassLoader()对象。
(2)newActivity()
跟踪该方法:
最终进入父类的findClass方法,代码如下:
没错,这里就发现了我们之前的异常。当pathList中没有找到这个类,就会抛出异常,也就是DexPathList才是问题的关键。
2、DexPathList是什么?
它里面有一个dexElements数组,类里面会调用findClass()
遍历dexElements,没有找到就返回null。记住这是宿主的dexElements。每一个apk都会有dexElements数组,这是因为java虚拟机会将java文件编译成class文件,并且打包成dex文件。那么当我们需要加载类文件的时候就需要去执行dex文件,所以dexElements就是dex文件在内存中表现形式,真正可执行的文件是Element元素中的DexFile文件。既然知道了报错的原因是dexElements没有插件的dexElements。那么我们可不可以将插件的也添加进来,和宿主的组合成新的dexElements。
3、代码实现
/**
* 合并宿主和插件的dex文件
* 源码分心dexElements中包含插件和宿主的dex即可
*/
private void combinedDex() throws Exception {
//1、拿到宿主的dexElements []
//最终调用ContextImpl的getClassLoader,返回PathClassLoader
PathClassLoader classLoader = (PathClassLoader) getClassLoader();
//拿到 private final DexPathList pathList;
Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListClass = mBaseDexClassLoaderClass.getDeclaredField("pathList");
pathListClass.setAccessible(true);
Object pathListObj = pathListClass.get(classLoader);
//拿到DexPathList中的Element[]数组 private Element[] dexElements;
Field appElementsField = pathListObj.getClass().getDeclaredField("dexElements");
appElementsField.setAccessible(true);
Object appElementsObj = appElementsField.get(pathListObj);
//2、拿到插件的dexElements []
/*
通过DexClassLoader加载插件apk的dex文件
参数1:插件包路径
参数2:使用DexClassLoader记载需要一个缓存路径 /data/data/插件包名/dexDir
参数3:需要的支持库
参数4:加载器
*/
String apkPath =
Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator +
"q1.apk";
File file = this.getDir("dexDir", Context.MODE_PRIVATE);
DexClassLoader pluginClassLoader = new DexClassLoader(apkPath, file.getAbsolutePath(), null,
getClassLoader());
//拿到 private final DexPathList pathList;
Class mPluginBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field dexPathListClass = mPluginBaseDexClassLoaderClass.getDeclaredField("pathList");
dexPathListClass.setAccessible(true);
Object dexPathListObj = dexPathListClass.get(pluginClassLoader);
//拿到DexPathList中的Element[]数组 private Element[] dexElements;
Field dexElementsField = dexPathListObj.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsObj = dexElementsField.get(dexPathListObj);
//3、合并成新的Elements[]
int appElementLength = Array.getLength(appElementsObj);
int pluginElementLength = Array.getLength(dexElementsObj);
int totalLength = appElementLength + pluginElementLength;
Object newElement = Array.newInstance(appElementsObj.getClass().getComponentType(),
totalLength);
for (int i = 0; i < totalLength; i++) {
if (i < appElementLength) {
//填充宿主的element
Array.set(newElement, i, Array.get(appElementsObj, i));
} else {
//填充插件的element
Array.set(newElement, i, Array.get(dexElementsObj, i - appElementLength));
}
}
//4、将新的数组设置给宿主的Elements[]
dexElementsField.set(pathListObj, newElement);
//5、将插件资源添加到AssetManager中
loadAssetAndResource(apkPath);
}
4、加载插件资源
上面已经完成了插件class文件的加载,但是还需要加载插件的资源文件。才能在资源管理中找到插件的布局文件
对外提供getResource()方法:
并在插件module的基类BaseActivity中重写getResources()方法
这样就完成了插件的加载。
测试的话需要将插件apk放在sdCard目录下,并且不要忘记在宿主配置文件中添加访问权限。
完整代码:
代码做了5.0~9.0的兼容
hook式插件兼容代码传送门