前言
手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候.
这个系列的文章:
1、用通俗易懂的讲解方式,讲解一门技术的实用价值
2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程
3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,抛砖引玉,请酌情cv
4、集合整理原理探索过程中的一些坑,或者demo的运行过程中的注意事项
5、用gif图,最直观地展示demo运行效果如果觉得细节太细,直接跳过看结论即可。
本人能力有限,如若发现描述不当之处,欢迎留言批评指正。
学到老活到老,路漫漫其修远兮。与众君共勉 !
引子
前面3篇文章,由易到难(入门,深入,高级),用通俗易懂的语言讲述了Hook的用法,最后一篇,实现了 启动没有在
menifest
中注册的Activity
的效果, 然而,这样做到底在生产开发中有什么样的应用呢?
答案:插件化.
插件化是一个宽泛的概念,只要是实现了 宿主app上插件功能的灵活拔插,实现了宿主app业务和插件功能的完全解耦,就可以称之为插件化.之前写过一篇 插件化的文章:手把手讲解 Android插件化启动Activity , 那时候用的插件化,
原理是用 宿主中真实Activity
作为代理
,来启动插件中的Activity
,管理插件中Activity
的生命周期
,并且处理好插件源代码
和资源文件
。现在,插件化有另一种方式,就是利用
无清单启动Activity的原理
,实现插件apk中Activity的启动.
Demo地址:https://github.com/18598925736/HookPluginDevDemo
鸣谢
感谢群里大佬 夜雨提供的demo
感谢享学课堂 VIP群里Alvin老师的提点
正文大纲
1.整体思路
2.实际效果展示
3.Demo源码讲解
4.坑坑更健康
正文
1.整体思路
下方有两张图:表示了插件化架构中,插件单独运行,和 插件作为宿主的一部分随宿主启动的技术关键点。
hook插件化.png
hook插件化2.png
如上图,如果跟随宿主一起启动,插件
apk
的资源文件要能够被宿主读到,插件的apk
的class
文件也必须能够被宿主读取,实现的方式就是,让在宿主的代码中进行hook
编程,生成一个能够读取宿主以及所有插件内class
的ClassLoader
,以及 一个能够读取 宿主以及插件内所有资源的Resource
.而,实现的具体过程,就是一个融合
过程.
2.实际效果展示
mumu模拟器上的效果
plugin.gif
宿主manifest文件
image.png
3.Demo源码讲解
宿主
插件
image.png
如果您down了我的Demo,那么观察一下,就会发现,无论是宿主的代码, 还是插件的代码,都非常简单,唯一阅读价值的,就是 宿主的Hook核心代码
。
在讲解Hook核心代码
之前,先回顾一下我的上篇文章所实现的效果:
能够绕过系统的manifest检测机制,让没有在manifest中注册的Activity也能够正常启动
一定有读者在看完上篇文章之后,会想,能够不去注册就可以启动Activity,是很神奇,但是又有什么利用价值呢?仅仅是为了不去注册就去干涉系统逻辑,太华而不实了.
这个问题的答案:
用 hook
实现插件化启动 Activity
,插件中的 manifest
并不会和宿主的 manifest
发生融合,也就是说,即使我们完成了 对 ClassLoader
和 Resource
的融合,实现了宿主对插件 class
和资源的访问,如果不能绕过系统的 manifest
检测,依然不能启动插件的 Activity
.
所以,用hook技术实现插件化启动Activity,完整思路是:
hook插件化完整思路.png
以下是关键代码 :
宿主的 MyApplication.java
主要用于调用Hook核心代码 :
public class MyApplication extends Application {
private Resources newResource;
public static String pluginPath = null;
@Override
public void onCreate() {
super.onCreate();
pluginPath = AssetUtil.copyAssetToCache(this, Const.PLUGIN_FILE_NAME);
//Hook第一次,绕过manifest检测
GlobalActivityHookHelper.hook(this);
//Hook第二次把插件的源文件class导入到系统的ClassLoader中
HookInjectHelper.injectPluginClass(this);
//Hook第三次,加载插件资源包,让系统的Resources能够读取插件的资源
newResource = HookInjectHelper.injectPluginResources(this);
}
//重写资源管理器,资源管理器是每个Activity自带的,
// 而Application的getResources则是所有Activity共有的
//重写了它,就不必一个一个Activity去重写了
@Override
public Resources getResources() {
return newResource == null ? super.getResources() : newResource;
}
}
绕过manifest检测的hook核心代码 GlobalActivityHookHelper.java
public class GlobalActivityHookHelper {
public static void hook(Context context) {
hookAMS(context);//使用假的Activity,骗过AMS的检测
if (ifSdkOverIncluding28())
hookActivityThread_mH_AfterIncluding28();//将真实的Intent还原回去,让系统可以跳到原本该跳的地方.
else {
hookActivityThread_mH_before28(context);
}
hookPM(context);//由于AppCompatActivity存在PMS检测,如果这里不hook的话,就会包PackageNameNotFoundException
}
//设备系统版本是不是大于等于26
private static boolean ifSdkOverIncluding26() {
int SDK_INT = Build.VERSION.SDK_INT;
if (SDK_INT > 26 || SDK_INT == 26) {
return true;
} else {
return false;
}
}
//设备系统版本是不是大于等于26
private static boolean ifSdkOverIncluding28() {
int SDK_INT = Build.VERSION.SDK_INT;
if (SDK_INT > 28 || SDK_INT == 28) {
return true;
} else {
return false;
}
}
...太长了就不都贴出来了,可以到demo里面去看
}
将宿主和插件的ClassLoader/Resource融合的 HookInjectHelper.java
public class HookInjectHelper {
/**
*
* 此方法的作用是:插件内的class融合到宿主的classLoader中,让宿主可以直接读取插件内的class
*
* @param context
*/
public static void injectPluginClass(Context context) {
String cachePath = context.getCacheDir().getAbsolutePath();
String apkPath = MyApplication.pluginPath;
//还记不记得dexClassLoader?它是专门用于加载外部apk的classes.dex文件的
//(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
// 4个参数分别是,外部dex的path,优化之后的目录,lib库文件查找目录,我们这没有用到lib里面的so,所以可以设置为null,最后一个是父ClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, cachePath, null, context.getClassLoader());
//先构造一个能够读取外部apk的classLoader对象
// 第一步 找到 插件的Elements数组 dexPathlist ----?dexElement
try {
Class myDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListFiled = myDexClazzLoader.getDeclaredField("pathList");
myPathListFiled.setAccessible(true);
Object myPathListObject = myPathListFiled.get(dexClassLoader);
Class myPathClazz = myPathListObject.getClass();
Field myElementsField = myPathClazz.getDeclaredField("dexElements");
myElementsField.setAccessible(true);
// 自己插件的 dexElements[]
Object myElements = myElementsField.get(myPathListObject);
// 第二步 找到 系统的Elements数组 dexElements
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Class baseDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListFiled = baseDexClazzLoader.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object pathListObject = pathListFiled.get(pathClassLoader);
Class systemPathClazz = pathListObject.getClass();
Field systemElementsField = systemPathClazz.getDeclaredField("dexElements");
systemElementsField.setAccessible(true);
//系统的 dexElements[]
Object systemElements = systemElementsField.get(pathListObject);
// 第三步 上面的dexElements 数组 合并成新的 dexElements 然后通过反射重新注入系统的Field (dexElements )变量中
// 新的 Element[] 对象
// dalvik.system.Element
int systemLength = Array.getLength(systemElements);
int myLength = Array.getLength(myElements);
// 找到 Element 的Class类型 数组 每一个成员的类型
Class<?> sigleElementClazz = systemElements.getClass().getComponentType();
int newSysteLength = myLength + systemLength;
Object newElementsArray = Array.newInstance(sigleElementClazz, newSysteLength);
//融合
for (int i = 0; i < newSysteLength; i++) {
// 先融合 插件的Elements
if (i < myLength) {
Array.set(newElementsArray, i, Array.get(myElements, i));
} else {
Array.set(newElementsArray, i, Array.get(systemElements, i - myLength));
}
}
Field elementsField = pathListObject.getClass().getDeclaredField("dexElements");
;
elementsField.setAccessible(true);
// 将新生成的EleMents数组对象重新放到系统中去
elementsField.set(pathListObject, newElementsArray);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Resources injectPluginResources(Context context) {
AssetManager assetManager;
Resources newResource = null;
String apkPath = MyApplication.pluginPath;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, apkPath);
Resources supResource = context.getResources();
newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return newResource;
}
}
关于Resource
的融合,我的文章:手把手讲解 Android hook技术实现一键换肤 里面有提及。
绕过manifest检测,在另一篇文章 手把手讲解 Android Hook-实现无清单启动Activity有详解,我就不再赘述了。
详细讲讲 ClassLoader
如何融合.
推荐一下 安卓源码的查看网址:https://www.androidos.net.cn/sourcecode,可以很方便帮助我们阅读系统源码,而不必去花大时间去下载整个安卓源码。
老规矩,先上图,下图是
ClassLoader融合.png相关类
的关系图:
我们用context.getClassLoader
拿到的是PathClassLoader
,而我们构建能够访问插件中class
的classLoader
是DexClassLoader
,他们有共同的父类BaseDexClassLoader
,而且,这个BaseDexClassLoader
类的本身就拥有能够装载多个dex
路径的能力。
插件DexClassLoader
读取的是插件apk
中的classes.dex
,宿主PathClassLoader
读取的是data/app/包名/base.apk
的classes.dex
. 他们分别将读取到的路径,存到了上图中的Element[] dexElements
数组中.
那么如果我们可以将插件DexClassLoader
中的dexElements
融合到 宿主PathClassLoader
的dexElements
中去,就可以实现宿主读取插件apk
的class.dex
.
demo代码中 HookInjectHelper类中的 injectPluginClass 方法,就是以上面的思路为依据进行的hook。
具体步骤为:
1.构建插件DexClassLoader
对象
2.获得系统的PathClassLoader
对象
3.分别获得插件DexClassLoader
和系统PathClassLoader
的DexPathList
中的dexElements
数组
4.将上述两个dexElements
数组进行融合
5.将融合之后的的dexElements
设置到系统PathClassLoader
中
至此,系统也能够访问插件apk
中的class
了.
就讲到这里,具体可以看源码。
那么接下来,如何启动插件中的Activity呢?
image.png
我的Demo中,由于我们在写宿主代码的时候,并不能直接引用插件的类,所以我们只能通过如下方式:
那么又如何启动宿主自身的Activity其他呢?可以按照上面的方式。
image.png
或者也可以用普通的方式:
而宿主的
image.pngmanifest
里,依然只有一个Activity
,其他的都可以不经注册直接启动,剩下的这一个是为了作为launch Activity
:
OK,全部讲完。
4.坑坑更健康
前方高能,惊天巨坑
细心的读者一定发现了,我在宿主里面用的是android.app.Activity
,而不是 AppCompatActivity
。
包括宿主内的第二个Main2Activity
,依然是android.app.Activity
。
因为我发现,如果换成AppCompatActivity
,我启动宿主的时候,就会报莫名其妙的异常。
03-09 18:39:19.069 16437-16437/study.hank.com.myhookplugindevdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: study.hank.com.myhookplugindevdemo, PID: 16437
java.lang.RuntimeException: Unable to start activity ComponentInfo{study.hank.com.myhookplugindevdemo/study.hank.com.myhookplugindevdemo.ui.MainActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2443)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5529)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
at android.support.v7.app.AppCompatDelegateImplV9.createSubDecor(AppCompatDelegateImplV9.java:410)
at android.support.v7.app.AppCompatDelegateImplV9.ensureSubDecor(AppCompatDelegateImplV9.java:323)
at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:284)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:139)
at study.hank.com.myhookplugindevdemo.ui.MainActivity.onCreate(MainActivity.java:22)
at android.app.Activity.performCreate(Activity.java:6278)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2396)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5529)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)
咨询了度娘,一无所获,然后请教了大佬,得到了靠谱答案,
AppCompatActivity
在启动的时候会进行上下文检查,于是报出了上面的问题。使用Activity
就好了, 不用使用AppCompatActivity
.
实际上后续我也查了两者的区别,AppCompatActivity
是为了兼容低版本设备而设计的,他和Activity
的区别是,AppCompatActivity
拥有默认的ActionBar
,也拥有自己的Theme
类。而Activity
默认不带ActionBar
,Theme
的使用也和前者不同.
所以我到目前为止也很疑惑,不过倒并不影响我们插件化开发,用android.app.Activity
和AppCompatActivity
开发的Activity
也并没有出现什么兼容问题.
其实在 我的 手把手讲解 Android插件化启动Activity 中,也出现过一次类似的问题,使用
android.app.Activity
没问题,但是换成AppCompatActivity
,则会报上面一样的错误,相当诡异,但是也同样不影响开发.
有知道原因的兄弟们记得留言啊,一起讨论一下.
结语
插件化开发这个话题,看起来高深莫测,实际上玩起来也并不简单。实现的方式也不止一种。
目前就我了解,看来有两种解决方案,用宿主的真实Activity
去代理插件Activity
,另一种就是用hook
去绕过manifest
检查. 两种方案各有优劣,hook
可能会失效,因为谷歌最近发布了 禁用反射的API名单,而且android Studio
也在使用反射的时候提示,反射可能失效。但是,还是那句话,天塌下来砸不到我们的头上,自然有大佬顶着,到时候,如果谷歌真的禁用反射,国内的巨佬们自然有新的解决办法,到时候跟随大流就好了。 而代理Activity
的方式,则多了一个PluginLib
层,需要维护,好处就是,不用看谷歌脸色。
hook插件化四部曲:
手把手讲解 Android Hook入门Demo
手把手讲解 Android Hook-Activity的启动流程
手把手讲解 Android Hook-实现无清单启动Activity
手把手讲解 Android Hook无清单启动Activity的应用
欢迎大家留言指点.
网友评论