概述
插件化是一个非常大的话题,他包含很多的知识点,我们今天简单的学习一下他的原理,并且从零开始实现插件化,这里主要用到了Hook技术
关联文章
插件化需要解决的问题和技术
- Hook技术
- 插件的类加载
- 插件的资源加载
- 启动插件Activity
Hook技术
如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象(劫持原始对象),那么就可以在这个代理对象为所欲为了,修改参数,替换返回值,我们称之为 Hook。
我们可用用Hook技术来劫持原始对象,被劫持的对象叫做Hook点,什么样的对象比较容易Hook呢?当然是单例和静态对象,在一个进程内单例和静态对象不容易发生改变,用代理对象来替代Hook点,这样我们就可以在代理对象中实现自己想做的事情,我们这里Hook常用的startActivity方法来举例
对于 startActivity过程有两种方式:Context.startActivity 和 Activity.startActivity。这里暂不分析其中的区别,以 Activity.startActivity 为例说明整个过程的调用栈。
Activity 中的 startActivity 最终都是由 startActivityForResult 来实现的。
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
startActivityForResult(intent, requestCode, null);
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
...
//注释1
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
...
}
我们看注释1处,调用了mInstrumentation.execStartActivity,来启动Activity,这个mInstrumentation是Activity成员变量,我们选择mInstrumentation作为Hook点
首先先写出代理Instrumentation类
public class ProxyInstrumentation extends Instrumentation {
private final Instrumentation instrumentation;
public ProxyInstrumentation(Instrumentation instrumentation){
this.instrumentation=instrumentation;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("mmm", "Hook成功,执行了startActivity"+who);
Intent replaceIntent = new Intent(target, TextActivity.class);
replaceIntent.putExtra(TextActivity.TARGET_COMPONENT, intent);
intent = replaceIntent;
try {
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;
}
}
ProxyInstrumentation类继承Instrumentation,并包含原始Instrumentation的引用,实现了execStartActivity方法,其内部会打印log并且反射调用原始Instrumentation对象的execStartActivity方法
接下来我们用ProxyInstrumentation类替换原始的Instrumentation,代码如下:
public static void doInstrumentationHook(Activity activity){
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = null;
try {
mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
// 创建代理对象
Instrumentation originalInstrumentation = (Instrumentation) mInstrumentationField.get(activity);
mInstrumentationField.set(activity, new ProxyInstrumentation(originalInstrumentation));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
然后再MainActivity中调用这个方法
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ProxyUtils.doInstrumentationHook(this);
}
然后启动一个Activity
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,TextActivity.class);
startActivity(intent);
}
})
看日志
12-19 10:25:29.911 8957-8957/com.renxh.hook D/mmm: Hook成功,执行了startActivitycom.renxh.hook.MainActivity@71f98a6
这样我们就成功Hook住了Instrumentation
插件的类加载
这块需要了解ClassLoader,以及如何合并不同ClassLoader中的类,这个我之前分析过,不太了解的请看
public class PluginHelper {
private static final String TAG = "mmm";
public static void loadPluginClass(Context context, ClassLoader hostClassLoader) {
// 获取到插件apk,通常都是从网络上下载,这里为了演示,直接将插件apk push到手机
String pluginPath = copyFile("/sdcard/plugin1.apk", context);
String dexopt = context.getDir("dexopt", 0).getAbsolutePath();
DexClassLoader pluginClassLoader = new DexClassLoader(pluginPath, dexopt, null, hostClassLoader);
// 通过反射获取到pluginClassLoader中的pathList字段
Field baseDexpathList = null;
try {
//获取插件中的类
baseDexpathList = BaseDexClassLoader.class.getDeclaredField("pathList");
baseDexpathList.setAccessible(true);
Object pathlist = baseDexpathList.get(pluginClassLoader);
Field dexElementsFiled = pathlist.getClass().getDeclaredField("dexElements");
dexElementsFiled.setAccessible(true);
Object[] dexElements = (Object[]) dexElementsFiled.get(pathlist);
//获取应用内的类
Field baseDexpathList1 = BaseDexClassLoader.class.getDeclaredField("pathList");
baseDexpathList1.setAccessible(true);
Object pathlist1 = baseDexpathList1.get(hostClassLoader);
Field dexElementsFiled1 = pathlist1.getClass().getDeclaredField("dexElements");
dexElementsFiled1.setAccessible(true);
Object[] dexElements1 = (Object[]) dexElementsFiled1.get(pathlist1);
//创建一个数组
Object[] finalArray = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
dexElements.length + dexElements1.length);
//合并插件和应用内的类
System.arraycopy(dexElements, 0, finalArray, 0, dexElements.length);
System.arraycopy(dexElements1, 0, finalArray, dexElements.length, dexElements1.length);
//把新数组替换掉原先的数组
dexElementsFiled1.set(pathlist1, finalArray);
Log.d("mmm","插件加载完成");
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
介绍下上方的代码,首先利用DexClassLoader加载未安装的插件apk,然后取出插件apk的dexElements数组,我们知道dexElements封装了Element,而Element内部封装了DexFile,DexFile用于加载dex文件,也就是说dexElements数组储存着插件所有的类,然后再拿到应用中的dexElements数组,他存储着应用中所有的类,最后把这俩个dexElements合并,然后把合并后的数组赋值给应用的dexElements变量,这时应用中就有了插件中所有类
启动插件Activity
我们知道没有在AndroidManifest中注册的Activity是不能启动的,但是我们插件中的Activity本来就没有在AndroidManifest中注册,无法启动,那么我们改咋么办呢?
使用占坑的Activity通能过AMS的验证
先在 Manifest 中预埋 TextActivity,启动时 hook时,将 Intent 替换成 TextActivity。
我们在上面的ProxyInstrumentation的execStartActivity方法加入点逻辑
public class ProxyInstrumentation extends Instrumentation {
private final Instrumentation instrumentation;
public ProxyInstrumentation(Instrumentation instrumentation){
this.instrumentation=instrumentation;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("mmm", "Hook成功,执行了startActivity"+who);
Intent replaceIntent = new Intent(target, TextActivity.class);
replaceIntent.putExtra(TextActivity.TARGET_COMPONENT, intent);
intent = replaceIntent;
try {
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;
}
}
我们把原来的intent保存起来,然后创建一个TextActivity的intent,达到通过AMS验证的目的,这里利用占坑的TextActivity通过验证,不过我还最终还是要打开插件的PluginActivity的,所以需要找个合适的时候,再把intent还原回来
那么什么时候还原回来呢?
我们知道在Activity启动时ActivityThread会收到Handler的消息,然后再打开Activity
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
最终会到handleMessage方法中,然后对LAUNCH_ACTIVITY消息进行处理,最终会调用Activity的onCreate方法
那么我们改在哪里还原intent呢?
我们看一下Handler的源码
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
- 当
msg中有callback时,则调用message.callback.run();方法,其中的callback指的Runnable - 如果
callback为空,那么则看一下成员变量的mCallback是否为空,这个是Handler的构造方法传入的 - 如果
mCallback也为空,则调用handleMessage方法,这个一般在Handler的子类中重写
我们看到只要mCallback不为null,就执行callback的handleMessage方法,我们可以以mCallback为Hook点,自定义mCallback,来替换当前·handler·对象的Callback
public class ProxyHandlerCallback implements Handler.Callback {
private Handler mBaseHandler;
public ProxyHandlerCallback(Handler mBaseHandler) {
this.mBaseHandler = mBaseHandler;
}
@Override
public boolean handleMessage(Message msg) {
Log.d("mmm", "接受到消息了msg:" + msg);
if (msg.what == LAUNCH_ACTIVITY) {
try {
Object obj = msg.obj;
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent intent = (Intent) intentField.get(obj);
Intent targetIntent = intent.getParcelableExtra(TextActivity.TARGET_COMPONENT);
intent.setComponent(targetIntent.getComponent());
Log.e("mmmintentField", targetIntent.toString());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
mBaseHandler.handleMessage(msg);
return true;
}
}
这个类继承了了Handler.Callback,重写了handleMessage方法,收到消息的类型为LAUNCH_ACTIVITY,在这个方法内还原intent
然后我们定义一个Hook这个Handler的方法
public static void doHandlerHook() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object activityThread = currentActivityThread.invoke(null);
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, new ProxyHandlerCallback(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
现在插件类加载到了应用中,插件Activity也已经还原了,还差一步,就是插件资源的加载
插件资源的加载
首先我们把插件apk的资源加载出来
public class ResourceHelper {
public static Resources sPluginResources;
public static AssetManager sNewAssetManager ;
public static void addResource(Context context, String path) {
//利用反射创建一个新的AssetManager
try {
sNewAssetManager = AssetManager.class.getConstructor().newInstance();
//利用反射获取addAssetPath方法
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
//利用反射调用addAssetPath方法加载外部的资源(SD卡)
if (((Integer) mAddAssetPath.invoke(sNewAssetManager, path)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
sPluginResources = new Resources(sNewAssetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
Log.d("mmm","资源加载完毕");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
首先创建一个AssetsManager,然后加载外部资源,然后构建一个新的Resouce
然后再应用的Aplication重写getResources和getAssets返回我们插件的资源
@Override
public Resources getResources() {
return ResourceHelper.sPluginResources == null ? super.getResources() : ResourceHelper.sPluginResources;
}
@Override
public AssetManager getAssets() {
return ResourceHelper.sNewAssetManager == null ? super.getAssets() : ResourceHelper.sNewAssetManager;
}
因为插件的四大组件都是在宿主中创建的,所以拿到的Application其实也是宿主的,所以插件Activity只需要getApplication().getResources()就可以方便的使用插件中的资源
最后在插件Activity重写getResources和getAssets方法
public class PluginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin);
}
@Override
public Resources getResources() {
return getApplication() != null && getApplication().getResources() != null ? getApplication().getResources() : super.getResources();
}
@Override
public AssetManager getAssets() {
return getApplication() != null && getApplication().getAssets() != null ? getApplication().getAssets() : super.getAssets();
}
}
现在资源也已经加载完了
注意事项
插件Activity的style要切换成NoActionBar,不然会出bug,这个bug我目前也不知道为什么
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Demo
demo使用很简单,把assets文件夹下的plugin1.apk推入sdcard就可以使用

参考
《Android开发进阶解密》
https://segmentfault.com/a/1190000015688023#item-4
https://www.jianshu.com/p/d3231a15afee
https://www.jianshu.com/p/ba00ac520aad
本文深入探讨了Android插件化技术,包括Hook技术的应用、插件类与资源加载机制、启动插件Activity的方法,以及实现过程中的关键步骤和注意事项。
3593

被折叠的 条评论
为什么被折叠?



