插件化开发之坑位的理解(Hook)
参考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. 预热知识
image这里需要有关于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
<?xml version="1.0" encoding="utf-8"?>
- 启动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
运行结果:
image
可以看到,通过这种方式实现了不在AndroidManifest中注册,但是可以启动Activity的效果。这里可以应用到插件化中,如Replugin,编译时自动注入坑位,运行时进行确定坑位。
更多相关文章
- android使用web加载网页的js问题
- 【Android开发小记--6】动画--属性动画以及Fragment切换动画(3D)
- Android Binder进程间通信-ServiceManager代理对象的获取过程
- Activity属性
- android RelativeLayout 布局属性详解
- Android 复习笔记之图解TextView类及其XML相关属性和方法
- Android 下载进度条, 自定义加载进度条,loading动画
- RelativeLayout的常用属性