概述
这篇文章主要讲述的有以下几点:
- 如何hook系统中的一些类,以达到我们想要实现的功能;
- 如何去加载插件中的class文件;
- 如何去加载插件中的资源;
要了解插件化,对这几个点是必须要知道的,插件化也是在这几个点的基础上逐渐进行完善的。
hook系统中的哪些类
1、Instrumentation;
2、IActivityManager;
3、Handler;
Instrumentation
这里以启动activity为例来进行说明,一般启动activity都是调用startActivity(),这里可以从这个方法进行入手,最终都会执行到Instrumentation中来,你可以理解为这个类就是系统类和程序应用类交互的一个监视类(四大组件中的activity和系统交互都是通过这个类来转发的),这个类实例化是在ActivityThread中,并且在四大组件之前,也就是说,我们通过hook到这个类就可以做一些我们想做的事了,比如替换掉启动的activity,Instrumentation中的方法这里就不多说了,感兴趣的可以自己去看看,这里主要是如何去hook到这个类:
//先拿到ActivityThread的实例对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method sCurrentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
sCurrentActivityThread.setAccessible(true);
Object activityThread= sCurrentActivityThread.invoke(null);
//通过反射拿到Instrumentation对象,在自己创建一个实现了Instrumentation的类,可以把反射拿到的对象传进去,相当于我们实现的instrumentation起到的是一个装饰的作用
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field instrumentation = activityThreadClass.getDeclaredField("mInstrumentation");
instrumentation.setAccessible(true);
Instrumentation invoke1 = (Instrumentation) instrumentation.get(invoke);
IActivityManager
在Instrumentation里可以做一些处理,但也有一些处理不了的,比如四大组件的其他三个,这时候可以怎么处理呢?这时候IActivityManager就可以出马了(在获取IActivityManager时,在版本26以后是有变化的,所以实际使用时需要做兼容处理的,这里就以26之前的版本来做说明),在Instrumentation处理完后,最终调用的还是IActivityManager的方法,IActivityManager是一个接口,这里hook到IActivityManager后就可以使用动态代理进行一些操作了,其实也就是对intent中的信息偷梁换柱了,这里来看下如何去hook到IActivityManager:
public void hookIActivityManager(){
try {
Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field singletonFeild = activityManagerNativeClass.getDeclaredField("gDefault");
singletonFeild.setAccessible(true);
Object singleton = singletonFeild.get(null);
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstance = singletonClass.getDeclaredField("mInstance");
mInstance.setAccessible(true);
Object iActivityManager = mInstance.get(singleton);
Object o = Proxy.newProxyInstance(activityManagerNativeClass.getClassLoader(), new Class[]{Class.forName("android.app.IActivityManager")}, new AmsInvocationHandler(iActivityManager));
mInstance.set(singleton,o);
} catch (Exception e) {
e.printStackTrace();
}
}
public class AmsInvocationHandler implements InvocationHandler {
private Object iActivityManagerImpl;
public AmsInvocationHandler(Object iActivityManagerImpl){
this.iActivityManagerImpl = iActivityManagerImpl;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG, "在这里找到对应的方法名,如startActivity,然后取出intent,替换掉里面的参数," +
"以达到偷梁换柱的目的 = "+method.getName());
return method.invoke(iActivityManagerImpl,args);
}
}
Handler
对于Handler这个类,只要你是学android的,就肯定使用过,这个类对于android系统来说也是一个很重要的类,所以对这个类还不知道它内部是如何实现的话可以好好的再去学习一下,对于android 9.0之前,activity生命周期的执行都会通过Handler往下进行分发,具体可以去看下ActivityThread这个类的mH对象,他是一个实现了Handler的类的对象,这里先看下Handler对消息的处理:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
上面的mH对象只实现了handleMessage()方法,从这里可以看到,如果我们想多做一些处理,那我们可以给他设置一个mCallback,那它就可以先拿到消息进行处理,有了思路后,现在就来hook这个handler并给他设置一个mCallback:
public void hookHandler(){
try{
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method sCurrentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
sCurrentActivityThread.setAccessible(true);
Object activityThread = sCurrentActivityThread.invoke(null);
Method mH = activityThreadClass.getDeclaredMethod("getHandler");
mH.setAccessible(true);
Handler handler = (Handler) mH.invoke(activityThread);
Field mCallback = Handler.class.getDeclaredField("mCallback");
mCallback.setAccessible(true);
mCallback.set(handler, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
try {
Object obj = msg.obj;
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent intent = (Intent) intentField.get(obj);
Log.e(TAG, "handleMessage: intent中存有的信息这个时候就可以替换回来 what = "+msg.what+" "+msg);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
handler执行完成后,又会执行到Instrumentation中去,所以在Instrumentation中还可以对信息进行处理,到这里,hook系统的几个点都已经说到了。
加载插件中的class文件
对于插件中的class文件,android默认的类加载器(ClassLoad)是找不到类文件的,android默认的类加载器只会到它指定的目录下去查找class文件,也就是apk安装的目录下,既然问题已经摆出来了,那要怎么去解决呢?方案有两种:
1、将插件的apk路劲添加到android默认的类加载器中去,这种方法的优点是不需要我们另外指定类加载器去加载插件中的class文件,缺点是你要对android的加载器很了解,通过反射将路劲添加进去,过程比较繁琐;
2、自己创建一个类加载器,专门去加载插件中的class文件,这样做的缺点是我们需要去维护一个类加载器,但是实现起来很简单;
第一种方式加载class:
通过这种方式去加载类,需要对android的类加载器比较了解,对于类加载去,主要还是需要提供一个类的路径给类加载器,这样类加载器才能找到这个类,类加载器用到的是DexClassLoader,这个类里面又有一个DexPathList对象,DexPathList里面又有一个类型是Element数组的dexElements对象,这个数组提供的就是类加载的路径,所以我们需要做的就是hook这个Element数组对象,并将插件的路劲添加进去就可以了,下面就一起来看下代码中是如何实现的:
private void combineDex(){
String path = Environment.getExternalStorageDirectory()+ File.separator+"apkRes"+File.separator+"face.apk";
ClassLoader myClassLoader = new DexClassLoader(path,getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(),
getDir("validlib",Context.MODE_PRIVATE).getAbsolutePath(),getClassLoader());
Object newDexElements = getDexElements(getPathList(myClassLoader));
Log.d(TAG, "combineDex: getClassLoader = "+getBaseContext().getClassLoader().getClass().getName());
Object pathList = getPathList(getClassLoader());
Object baseDexElements = getDexElements(pathList);
Object allDexElements = combineArray(newDexElements, baseDexElements);
try{
Field dexElements = pathList.getClass().getDeclaredField("dexElements");
dexElements.setAccessible(true);
dexElements.set(pathList,allDexElements);
}catch (Exception e) {
e.printStackTrace();
}
}
private Object getPathList(Object object){
try{
Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(object);
Log.d(TAG, "getPathList: pathList = "+pathList);
return pathList;
}catch (Exception e) {
e.printStackTrace();
}
return null;
}
private Object getDexElements(Object object){
try{
Field dexElementsField = object.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElements = dexElementsField.get(object);
Log.d(TAG, "getDexElements: dexElements = "+dexElements);
return dexElements;
}catch (Exception e) {
e.printStackTrace();
}
return null;
}
private Object combineArray(Object firstArray,Object secondArray){
Class<?> componentType = firstArray.getClass().getComponentType();
int firstLength = Array.getLength(firstArray);
int secondLength = Array.getLength(secondArray);
Object dexElements = Array.newInstance(componentType, firstLength+secondLength);
System.arraycopy(firstArray,0,dexElements,0,firstLength);
System.arraycopy(secondArray,0,dexElements,firstLength,secondLength);
return dexElements;
}
这样处理了ClassLoader后,在应用中加载类文件就不需要再去指定类加载器了。
第二种方式加载class:
自己创建一个类加载器专门去加载插件中的class文件,这里只需要去创建一个DexClassLoader对象,当需要加载插件中的class文件时就使用这个类加载器进行加载就可以了,如下:
DexClassLoader classLoader;
private void createClassLoader(){
String path = Environment.getExternalStorageDirectory()+ File.separator+"apkRes"+File.separator+"face.apk";
classLoader = new DexClassLoader(path,getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(),
getDir("validlib",Context.MODE_PRIVATE).getAbsolutePath(),getClassLoader());
try{
Class<?> aClass = Class.forName("com.tangedegushi.utils.Constant", true, classLoader);
Object instance = aClass.newInstance();
Field[] declaredFields = aClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
Log.d(TAG, "createClassLoader: field = "+declaredField.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
加载插件中的资源文件
对于一个应用主要部分就是类文件和资源,关于类文件的上面已经全部说完了,接下来就来看看插件中的资源是如何加载的,对于应用中资源的加载,使用的是Resources,这里可以借鉴android是如何加载资源的,如果对Resources还不是比较了解的,建议先去看看这个类,Resources加载资源,你可以理解他就是一个代理,正真去加载资源的是他内部的一个对象AssertManger,一起来看看加载插件资源代码的实现:
public String getPluginString(String name){
Resources pluginResources = getPluginResources();
int identifier = pluginResources.getIdentifier(name, "string", "com.ubtechinc.cruzr.launcher");
String string = pluginResources.getString(identifier);
Log.d(TAG, "onCreate: this is other package string source = "+string+" version = "+ Build.VERSION.SDK_INT);
return string;
}
private Resources getPluginResources() {
try {
String path = Environment.getExternalStorageDirectory() + File.separator + "apkRes" + File.separator + "face.apk";
AssetManager assetManager = AssetManager.class.newInstance();
Log.d(TAG, "onCreate: path = " + path + " exits = " + (new File(path)).exists());
Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, path);
Resources resources = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());
return resources;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
还是很简单的,其实就是去创建了一个Resources对象,然后通过这个对象去获取资源就可以了。
该说的差不多都说到了,根据上面的所说的,自己撸个简单的插件框架是没什么问题了。上面的代码并不一定很完善,具体细节可能还需要处理。