参考1:https://www.jianshu.com/p/f2e5b7b7f72b
参考2(Android动态加载Activity原理):https://blog.csdn.net/cauchyweierstrass/article/details/51087198
参考3:(Android类加载之PathClassLoader和DexClassLoader)https://www.jianshu.com/p/4b4f1fa6633c
1. 原理:
坑位的概念是指在AndroidManifest中注册,但并没有真正的实现类,只作为其他Activity启动的坑位。
Hook点为ClassLoader,Android中的ClassLoader有两个,分别为DexClassLoader和PathClassLoader,用于加载APK的是PathClassLoader,他们的区别是:
DexClassLoader:能够加载自定义的jar/apk/dex
PathClassLoader:只能加载系统中已经安装过的apk
所以Android系统默认的类加载器为PathClassLoader,这个也就是需要Hook的地方,而DexClassLoader可以像JVM的ClassLoader一样提供动态加载。
2. 预热知识
这里需要有关于ClassLoader和Activity启动的知识:
在启动一个新的Activity的时候,AMS会对其进行很多检测,例如是否在AndroidManifest中注册,是否有权限启动等等。如果这些都通过,那么需要判断当前的进程是否存在,不存在需要先调用ActivityThread.main()方法,开启线程循环以及启动Application。最终会通过ActivityThread的Handler发送一条为“BIND_APPLICATION”的消息,通过这个消息,Handler来处理这次Application的创建过程。这里会创建Application、LoadedApk等。
- LoadedApk对象是APK文件在内存中的表示,APl文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。注意:这里会创建一个ClassLoader作为类加载器,也就是我们需要Hook的。
LoadedApk.java
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
- Activity的创建就是通过反射创建的,使用的就是上面提到的ClassLoader,所以我们只需要Hook住这个ClassLoader,通过类的双亲委派机制来实现我们自己的逻辑即可。
Activity启动过程源码分析如下:(ActivityThread发送一条“LAUNCH_ACTIVITY”的消息给对应的Handler,在处理LAUNCH_ACTIVITY的消息类型处执行handleLaunchAvtivity方法,在handlerLaunchActivity中又执行了PerformLaunchActivity()来完成Activity对象的创建和启动过程。)
PerformLaunchActivity这个方法主要完成了五件事
-
- 从ActivityClientRecord中获取待启动的Activity的组件信息
-
- 通过Instrumentation的newActivity方法使用类加载器创建Activity对象
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
// 1.创建ActivityClientRecord对象时没有对他的packageInfo赋值,所以它是null
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE);
}
// ...
Activity activity = null;
try {
// 2.非常重要!!这个ClassLoader保存于LoadedApk对象中,它是用来加载我们写的activity的加载器
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
// 3.用加载器来加载activity类,这个会根据不同的intent加载匹配的activity
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
// 4.这里的异常也是非常非常重要的!!!后面就根据这个提示找到突破口。。。
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
// 从这里就会执行到我们通常看到的activity的生命周期的onCreate里面
mInstrumentation.callActivityOnCreate(activity, r.state);
// 省略的是根据不同的状态执行生命周期
}
r.paused = true;
mActivities.put(r.token, r);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
// ...
}
return activity;
}
newActivity的实现也比较简单,就是通过类加载器来创建Activity对象
public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
这里留意这个ClassLoader的loadClass方法,在后面hook填坑的时候起到关键作用
-
- 通过LoadedApk的makeApplication方法来尝试创建Application,这里不贴源码了,如果Application已经被创建过了,就不会重复创建,这就意味着一个应用只有一个Application对象,Application对象的创建也是通过Instrumentation来完成的,这个过程和Activity的创建一样,都是通过类加载器来实现的,Application创建完毕后,系统会通过Instrumentation的callApplicationOnCreate来调用Application的onCreate方法。
-
- 创建ContextImpl对象通过Activity的attach方法来完成一些重要数据的初始化。
-
- 调用Activity的onCreate方法。
上面五个步骤没贴源码的步骤不是hook中的关键所以没贴源码,详情请拜读《Android开发艺术与探索》的p332
- 调用Activity的onCreate方法。
3. Hook代码实现
- 创建HookUtils
public class HookUtils {
public static final String TAG="HookUtils";
public static void hookClassLoader(Application context) {
try {
// 获取Application类的mLoadedApk属性值
Object mLoadedApk = getFieldValue(context.getClass().getSuperclass(), context, "mLoadedApk");
if (mLoadedApk != null) {
// 获取其mClassLoader属性值以及属性字段
final ClassLoader mClassLoader = (ClassLoader) getFieldValue(mLoadedApk.getClass(), mLoadedApk, "mClassLoader");
if (mClassLoader != null) {
Field mClassLoaderField = getField(mLoadedApk.getClass(), "mClassLoader");
// 替换成自己的ClassLoader
mClassLoaderField.set(mLoadedApk, new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
// 替换Activity
if (name.endsWith("MainActivity2")) {
Log.d(TAG, "loadClass: name = " + name);
name = name.replace("MainActivity2", "MainActivity3");
Log.d(TAG, "loadClass: 替换后name = " + name);
}
return mClassLoader.loadClass(name);
}
});
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 反射获取属性值
*
* @param c class
* @param o 对象
* @param fieldName 属性名称
* @return 值
* @throws NoSuchFieldException e
* @throws IllegalAccessException e
*/
public static Object getFieldValue(Class c, Object o, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = getField(c, fieldName);
if (field != null) {
return field.get(o);//返回指定对象上此 Field 表示的字段的值。
} else {
return null;
}
}
/**
* 反射获取对象属性
*
* @param aClass c
* @param fieldName 属性名称
* @return 属性
* @throws NoSuchFieldException e
*/
private static Field getField(Class> aClass, String fieldName) throws NoSuchFieldException {
Field field = aClass.getDeclaredField(fieldName);
if (field != null) {
field.setAccessible(true);
}
return field;
}
}
这里主要是从Application中拿到mLoadedApk属性的值,然后再通过反射获取其mClassLoader属性值,然后将mLoadedApk中的ClassLoader替换自定义的ClassLoader。因为Activity在启动的时候要走下面这个方法:
public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
所以我们使用下面自己定义的ClassLoader可以拦截要启动的Activity替换成其他我们想要启动的Activity,这就是填坑。一般这里对应的MainActivity2是个空白的Activity(只在清单文件里面注册了,并没有真正的实现类)。
mClassLoaderField.set(mLoadedApk, new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
// 替换Activity
if (name.endsWith("MainActivity2")) {
Log.d(TAG, "loadClass: name = " + name);
name = name.replace("MainActivity2", "MainActivity3");
Log.d(TAG, "loadClass: 替换后name = " + name);
}
return mClassLoader.loadClass(name);
}
});
4. 测试
- 在Application中进行初始化
public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
HookUtils.hookClassLoader(this);
}
}
- 设置坑位,在AndroidManifest注册一个不存在的Activity
- 启动Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void start(View view) {
Intent intent=new Intent();
ComponentName name=new ComponentName("com.example.houyl.hookdemo","com.example.houyl.hookdemo.MainActivity2");
intent.setComponent(name);
startActivity(intent);
// startActivity(new Intent(this,MainActivity2.class));
}
}
因为我们在前面已经将启动Activity过程中的ClassLoader替换成了自定义的ClassLoader,启动一个Activity的时候会走我们自定义的ClassLoader。
- 创建MainActivity3
运行结果:
可以看到,通过这种方式实现了不在AndroidManifest中注册,但是可以启动Activity的效果。这里可以应用到插件化中,如Replugin,编译时自动注入坑位,运行时进行确定坑位。