Hook实现动态加载Activity
Activity的启动流程
Activity的启动是一个很复杂的过程,涉及的类也非常多,这是一张启动UML流程图:
简化一下大致就是:①startActivity > ②系统获取启动信息 > ③校验Manifast > ④创建Activity > ⑤调用生命周期。
如果我们想要动态加载的Activity位于一个独立的APK(或.jar、.aar文件)中,那么,在主工程的Manifast中提前注册好想要启动的Activity编译器都会给我们报错,显然不是正确的做法。
可选择的正确思路应该是:
- 在①startActivity时中传入包含启动目标
TargetActivity
的Intent
,无需在主工程的Manifast文件中申明。 - 在主工程中创建一个空的
ProxyActivity
并注册在Manifast中,在②系统获取启动信息(Intent)前,将Intent
中包含的TargetActivity
替换为ProxyActivity
,使得系统拿到的是在Manifast中注册过的ProxyActivity
,以通过第③步中的系统校验。 - 然后在系统④创建Activity之前将Intent中包含启动信息
ProxyActivity
还原回TargetActivity
。
寻找Hook锚点
我们要通过系统的校验,需要在系统获取Intent
信息之前将他替换掉。从 Activity的启动流程 中我们得知ActivityStarter
是用于确定intent
和 flags
应如何转换为 activity的类,所以替换锚点应从启动流程进入ActivityStarter
类之前的ActivityTaskManagerService
(AMS)类中找。
较为理想的Hook锚点是Instrumentation#execStartActivity
和ActivityTaskManager#startActivity
。
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
>>>
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
>>>
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
>>>
int result = ActivityTaskManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);//Activity是否在Manifast中注册过,就是在这个方法里校验的。
>>>
}
我们看一下execStartActivity
方法的实现,发现在不同的SDK版本中谷歌已经对他进行了三次改动:
- Android8.0之前的版本中通过
ActivityManagerNative.getDefault
获取ActivityManagerService
的实例来调用其startActivity
方法。 - Android10之前的版本是通过
ActivityManager.getService
获取ActivityManagerService
的实例来调用其startActivity
方法。 - Android10开始的版本是通过
ActivityTaskManager.getService
获取ActivityTaskManagerService
的实例来调用其startActivity
方法。
不同版本之间,获取AMS实例的类是不同的。从版本兼容性上来讲,把ActivityTaskManager
作为Hook目标并不是很理想。我们再把目光转向Instrumentation
。
Instrumentation
用于实现应用程序插装代码的基本类,主要负责Activity
和Application
的创建和生命周期调用。在运行时,这个类将在任何应用程序代码之前被实例化,从而允许我们监视系统与应用程序的所有交互。
从类的职能上来讲execStartActivity
是可以选做Hook目标的。而且,此类并没有因为版本的差异而改变其创建方式。所以,替换ProxyActivity
选择Hook Instrumentation#execStartActivity
的方式 要更合适一些。
替换的锚点找到了,那么还原的呢?我们的最终目的是要系统在创建启动Activity的时候使用我们指定的Intent,所以要从Activity创建之前去寻找。
还是从 Activity的启动流程 来看。ActivityThread
在收到Handler
发送的启动Activity的消息后会在ActivityThread#performLaunchActivity
中处理:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
>>>
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
>>>
}
>>>
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
>>>
}
Instrumentation#newActivity:
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);
}
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();
}
Activity
的实例是在类Instrumentation
中开始创建的。因为替换点我们选择了 Instrumentation
类的execStartActivity
方法。所以,Instrumentation
类的newActivity
可以作为还原点,这样我们只需要Hook Instrumentation
一个类,可以避免过多的Hook操作,后续SDK版本中谷歌如果对API做了改动,我们也只需要主要关注这一个类。
替换目标Activity
Hook操作会涉及到一系列反射操作,我们可以封装一个反射的工具类ReflectUtil:
public class ReflectUtil{
public static Object getField(Class clazz, Object target, String name) throws Exception {
Field field = getField(clazz, name);
return field.get(target);
}
public static Field getField(Class clazz, String name) throws Exception {
Field field = findField(clazz, name);
field.setAccessible(true);
return field;
}
public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
Field field = getField(clazz, name);
field.set(target, value);
}
private static Field findField(@NonNull Class clazz, @NonNull String name) throws NoSuchFieldException {
try {
return clazz.getField(name);
} catch (NoSuchFieldException e) {
for (Class<?> cls = clazz; cls != null; cls = cls.getSuperclass()) {
try {
return cls.getDeclaredField(name);
} catch (NoSuchFieldException ex) {
// Ignored
}
}
throw e;
}
}
}
创建一个继承自Instrumentation
的类,重写其execStartActivity
方法(该方法被标注为Hide,不对开发者开放,我们创建一个同名同参的方法)。InstrumentationProxy
作为代理类,当系统需要调用Instrumentation
的execStartActivity
方法时,会直接调用到我们的代理类中对应的方法。在完成了代理Activity的替换工作后,我们仍然需要系统按照正常流程走下去。所以,必须调用Instrumentation
的execStartActivity
方法。
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
public InstrumentationProxy(Instrumentation mInstrumentation) {
this.mInstrumentation = mInstrumentation;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infoList = who.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);
Intent mIntent;
// 如果目标intent中关联的Activity没有注册过,则为其注入代理类
if (infoList.size() == 0) {
mIntent = new Intent(who, ProxyActivity.class);
mIntent.putExtra(HookHelper.TARGET_INTENT, intent);
}else {
mIntent = intent;
}
try {
@SuppressLint("DiscouragedPrivateApi")
Method execMethod = mInstrumentation.getClass().getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
Object object = execMethod.invoke(mInstrumentation, who, contextThread, token,
target, mIntent, requestCode, options);
if (object != null) {
return (ActivityResult) object;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
然后通过反射把InstrumentationProxy
类实例注入到ActivityThread
中:
public static void hookInstrumentation(Context activity) throws Exception {
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Object mMainThread = ReflectUtil.getField(contextImplClass, activity, "mMainThread");
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Instrumentation mInstrumentation = (Instrumentation)ReflectUtil.getField(activityThreadClass, mMainThread, "mInstrumentation");
ReflectUtil.setField(activityThreadClass, mMainThread, "mInstrumentation", new InstrumentationProxy(mInstrumentation));
}
做完这一步,运行代码,当我们调用startActivity
时就会发现代码将会执行到我们的静态代理类中的execStartActivity
方法,并且成功完成了代理Activity的替换。
还原目标Activity
还原的hook点选定在Instrumentation#newActivity
方法:
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);
}
还是InstrumentationProxy
类,重写Instrumentation
的newActivity
方法:
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
public InstrumentationProxy(Instrumentation mInstrumentation) {
this.mInstrumentation = mInstrumentation;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (intent != null) {
Intent targetIntent = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
if (targetIntent != null && targetIntent.getComponent() != null) {
ComponentName componentName = targetIntent.getComponent();
return mInstrumentation.newActivity(cl, componentName.getClassName(), targetIntent);
}
}
return super.newActivity(cl, className, intent);
}
}
注意,第15行不能直接使用super调用newActivity,否则会抛出如下异常:Uninitialized ActivityThread
因为我们在注入代理实例时只是直接new了一个InstrumentationProxy
,在实际的系统流程中Instrumentation
还包含一些初始化工作(init
和basicInit
方法),我们的代理类只做Intent
中的activity
信息的替换,其他的任何操作仍然需要系统自己的Instrumentation
实例对象来完成。
接下来在Application
中注入代理:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
HookHelper.hookInstrumentation(base);
} catch (Exception e) {
e.printStackTrace();
}
}
加载插件APK
因为我们要加载的Activity是在一个独立的APK文件中,所以这里要用到动态加载。实现Activity的动态加载需要对 Java类的加载机制 有一定程度的了解。
Android为我们提供了两个用于动态加载的类:DexClassLoader和PathClassLoader。
- DexClassLoader是一个可以从包含classes.dex实体的.jar或.apk文件中加载classes的类加载器。这个类加载器必须要一个app的私有、可写目录来缓存经过优化的classes(odex文件)
- PathClassLoader是一个可以操作在本地文件系统的文件列表或目录中classes放入类加载器,但不可以从网络中加载classes。
动态加载APK需要使用DexClassLoader
。首先,把下载下来的插件APK从Download目录复制到APP自己的私有存储空间:
private File copyToCache(String name) {
String path = Environment.getExternalStorageDirectory().getPath() +
File.separator + "Download" + File.separator + name;
File source = new File(path);
if (!source.exists() || !source.isFile()) {
return null;
}
File dir = getCacheDir();
if (!dir.exists() && dir.mkdirs()) {
Log.i("MainActivity", "创建新文件夹");
}
File plugin = new File(dir.getPath() + File.separator + name);
FileChannel input = null;
FileChannel output = null;
try {
if (!plugin.exists() || plugin.createNewFile()) {
Log.i("MainActivity", "创建新文件");
}
if (plugin.length() != source.length()) {
input = new FileInputStream(source).getChannel();
output = new FileOutputStream(plugin).getChannel();
output.transferFrom(input, 0, input.size());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return plugin;
}
然后,使用DexClassLoader
加载:(注:下面代码我们创建的DexClassLoader
对象实例要保留起来以备后用)
private void loadFile(final File file) {
// 根据apk路径加载apk代码到DexClassLoader中
DexClassLoader classLoader = new DexClassLoader(file.getPath(), file.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
try {
Class clazz = classLoader.loadClass("usage.ywb.wrapper.audio.ui.activity.MainActivity");
startActivity(new Intent(MainActivity.this, clazz));
} catch (Exception e) {
e.printStackTrace();
}
}
这两个过程涉及到I/O操作,实际操作中应该需要在一个单独的工作线程中执行。
运行一遍,发现在newActivity方法中出现一个运行时异常ClassNotFoundException
:
通过断点调试发现这里系统使用的类加载器是PathClassLoader
,我们要加载的是一个APK文件,所以要把类加载器换成DexClassLoader
(即上文中loadFile
方法中使用的classLoader
实例)。
经过修改,最终的InstrumentationProxy
类(静态代理模式)如下:
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
public InstrumentationProxy(Instrumentation mInstrumentation) {
this.mInstrumentation = mInstrumentation;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infoList = who.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);
Intent mIntent;
if (infoList.size() == 0) {
mIntent = new Intent(who, ProxyActivity.class);
mIntent.putExtra(HookHelper.TARGET_INTENT, intent);
} else {
mIntent = intent;
}
try {
// 在完成了代理Activity的替换工作后,我们仍然需要系统按照正常流程走下去。
// 所以,我们需要手动调用Instrumentation的execStartActivity方法。
@SuppressLint("DiscouragedPrivateApi")
Method execMethod = mInstrumentation.getClass().getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
Object object = execMethod.invoke(mInstrumentation, who, contextThread, token,
target, mIntent, requestCode, options);
if (object != null) {
return (ActivityResult) object;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (intent != null) {
Intent targetIntent = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
if (targetIntent != null && targetIntent.getComponent() != null) {
ComponentName componentName = targetIntent.getComponent();
ClassLoader classLoader = PluginClassLoaderHelper.getHelper().getClassLoader("audio.apk");
return mInstrumentation.newActivity(classLoader, componentName.getClassName(), targetIntent);
}
}
return mInstrumentation.newActivity(cl, className, intent);
}
}
做完以上工作,已经能实现动态加载未安装APK中的Activity
了。因为我们只对Intent做了一个“偷梁换柱”,Activity的完整创建过程还是由系统完成的,所以是保有生命周期的。
加载资源文件
虽然我们的Activity
类已经替换成了插件中的类,但是系统在加载资源文件时使用的Resource
还是主工程的,所以无法找到来自插件APK中的资源。
解决这个问题有两种方式:
- 1,通过反射
AssetManager#addAssetPath
将插件中的资源合并到主工程,使得主工程能同时访问App和插件Apk的资源文件。 - 2,保持插件的独立性,创建插件自己的
AssetManager
,加载插件中的Resource
。
这里我们以第二种方式为例
private Resources loadResources(String path) {
//插件资源独立,该resource只能访问插件自己的资源
Resources hostResources = HookManager.getInstance().getBaseContext().getResources();
AssetManager assetManager = null;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, path);
} catch (Exception e) {
e.printStackTrace();
}
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
在InstrumentationProxy
类中重写Instrumentation#callActivityOnCreate
方法,在这里替换Resource
:
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
injectActivity(activity);
mInstrumentation.callActivityOnCreate(activity, icicle);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
injectActivity(activity);
mInstrumentation.callActivityOnCreate(activity, icicle, persistentState);
}
private void injectActivity(Activity activity) {
Context base = activity.getBaseContext();
try {
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
if (isIntentFromPlugin) {
ReflectUtil.setField(contextImplClass, base, "mResources", HookManager.getInstance().getResource());
Context context = createPluginContext(base);
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", context);
ReflectUtil.setField(Activity.class, activity, "mApplication", mInstrumentation.newApplication(HookManager.getInstance().getClassLoader(),
pluginApplicationClaaName, context));
Intent targetIntent = activity.getIntent().getParcelableExtra(HookManager.TARGET_INTENT);
ComponentName component = targetIntent.getComponent();
Intent wrapperIntent = new Intent(activity.getIntent());
wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
activity.setIntent(wrapperIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
}
[参考文档]