序:
1.本文是安卓插件化课程的第三篇,完整课程链接参见下面链接:
2.主要内容
本篇主要讲的是如何启动一个插件中的Activity。这里仍人是有前提的,前提条件变成插件activity中不能引用插件包中的资源,但是不需要提前在宿主mainfest中注册插件activity了。
一:原理简述
1.activity启动流程简述
启动流程不是本篇介绍的主要核心,所以这里简略的说一下启动流程(这里和上一章相同):
1.Activity.startActivity -> Instrumetation.execStartActivity -> 通过binder机制通知AMS。
2.AMS收到通知后,进行一系列的检查,最后也通过binder通知APP去执行创建等一系列的操作。APP端的binder实体类就是ActivityThread中的ApplicationThread。
这一系列的检查中,就包含mainfest的声明检查,本篇中我们先不hook,下一篇再讲。
3.ApplicationThread收到通知后,调用ActivityThread中的方法去完成Activity的创建。
2.系统是如何生成activity的
1.而处理创建activity的方法是handleLaunchActivity,它交给performLaunchActivity来完成最终的创建。
2.我们看一下最终创建的代码,传入classLoader,交给Instrumetation完成Activity的创建。
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
3.Instrumetation中newActivity方法
这里又通过Factory工厂类去完成创建
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
}
3.如何绕过manifest检查
第一步我们了解到,检查是系统层去做的,确实很难绕过。但是加载activity是app层去做的,这里我们是可以做一些操作的,目前比较流程的方式就是埋桩。即告诉系统启动的是activityA,但是真正生成的时候,启动的是activityB(插件中的)。
我们可以提前在宿主中声明一个activityA,启动插件APP中的activity时,改为启动A。这样的话,系统检查肯定是可以过的,因为本身activityA就是有注册。但是真正的生成时,我们在第1.2.3步,重写newActivity方法,返回一个插件中的activityB就好了。
4.如何Hook newActivity方法
hook newActivity方法的核心就是替换掉Instrumentation,用我们自定义的Instrumentation去替代原生的。这样我们就可以重写newActivity这个方法了。
我们看一下ActivityThread的源码,可以知道Instrumentation其实也是ActivityThread的一个成员变量,变量名为:mInstrumentation。所以我们只要通过反射,拿到ActivityThread对象,然后替换掉这个对象中的mInstrumentation变量就可以了。
二:代码编写
1.插件项目中创建Plugin2Activity
因为没有实现资源的加载,所以这里的activity没有引用任何资源。
package com.xt.appplugin;
import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.TextView;
public class Plugin2Activity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
LinearLayout linearLayout = new LinearLayout(getApplicationContext());
TextView textView = new TextView(getApplicationContext());
textView.setText("这是插件类Plugin2Activity");
linearLayout.addView(textView);
setContentView(linearLayout);
}
}
2.编译项目,拷贝apk到app的assets目录
3.在宿主中埋桩
这里注册的activity,以后让系统进行检查的也是这个,但是最终生成时我们替换掉。
<activity android:name="com.xt.client.HostActivity" />
4.自定义Instrumentation
自定义的Instrumentation中,获取即将要被启动的activity,如果是我们埋桩,则获取真正要启动的类名,然后通过插件的classLoader进行加载,
public class MyInstrumentation extends Instrumentation {
public static final String ClassLoader = "classLoader";
public static final String ClassName = "pluginClassName";
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//拦截指定的className
if (!className.equals("com.xt.client.HostActivity")) {
return super.newActivity(cl, className, intent);
}
String pluginClassName = intent.getStringExtra(ClassName);
//插件的classLoader
Object classLoader = ObjectCache.getInstance().getParams(ClassLoader);
if (classLoader instanceof ClassLoader) {
Class<?> aClass = ((ClassLoader) classLoader).loadClass(pluginClassName);
Object o = aClass.newInstance();
return (Activity) o;
}
return super.newActivity(cl, className, intent);
}
}
5.替换Instrumentation
if (position == 3) {
/**
* 启动acitivyt,通过插桩的方式进行
* hook Instrumentation
*/
val myInstrumentation =
MyInstrumentation()
//替换Acitivty中的mInstrumentation
val classLoader = javaClass.classLoader
val loadedApkClass = classLoader.loadClass("android.app.LoadedApk")
val activityThreadClass = classLoader.loadClass("android.app.ActivityThread")
val activityThreadField =
activityThreadClass.getDeclaredField("sCurrentActivityThread")
activityThreadField.isAccessible = true
val activityThreadGet = activityThreadField.get(null)
val instrumentationField = activityThreadClass.getDeclaredField("mInstrumentation")
instrumentationField.isAccessible = true
instrumentationField.set(activityThreadGet, myInstrumentation)
//hook Instrumentation之后,就可以启动activity
val intent = Intent(context, HostActivity::class.java)
intent.putExtra(MyInstrumentation.ClassName, "com.xt.appplugin.Plugin2Activity")
startActivity(intent)
return
}
6.测试验证
1.我们启动的插件activity的方式改了下,改成如下的方式。
val intent = Intent(context, HostActivity::class.java)
intent.putExtra(MyInstrumentation.ClassName, "com.xt.appplugin.Plugin2Activity")
startActivity(intent)
2.先点击加载插件,这时候会加载插件中的apk。并且生成对应的classLoader并且加入到缓冲中。
val apkPath = context.filesDir.absolutePath + File.separator + APK_NAME
val odexPath = context.filesDir.absolutePath + File.separator + APK_ODEX
selfClassLoader = DexClassLoader(apkPath, odexPath, null, javaClass.classLoader)
ObjectCache.getInstance().setParams(MyInstrumentation.ClassLoader, selfClassLoader)
3.点击启动apk插件中activity(无需manifest注册)
这时候,我们发现插件成功启动了。
三:要点总结
1.与第二章不同的是,本章我们hook的对象是Instrumentation,而不是classLoader。所以其实实现插件化有很多方式很多点可以hook。
2.由于是提前埋桩,所以在manifest中声明的一些属性会丢失,所以我们要把这部分的属性挪到activity中去实现。这样对插件化的便捷使用还是有一些影响的。
3.如果是加载多个插件的情况,那对应的就会有多个classLoader,我们可以根据包名来判断到底使用哪个classLoader来进行加载。
四。代码地址:
项目地址:
https://github.com/aa5279aa/android_all_demo
插件项目位置:https://github.com/aa5279aa/android_all_demo/tree/master/DemoClient/appplugin