概述
插件化是一个非常大的话题,他包含很多的知识点,我们今天简单的学习一下他的原理,并且从零开始实现插件化,这里主要用到了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