参考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. 预热知识

插件化开发之坑位的理解(Hook)_第1张图片 image

这里需要有关于ClassLoader和Activity启动的知识:
在启动一个新的Activity的时候,AMS会对其进行很多检测,例如是否在AndroidManifest中注册,是否有权限启动等等。如果这些都通过,那么需要判断当前的进程是否存在,不存在需要先调用ActivityThread.main()方法,开启线程循环以及启动Application。最终会通过ActivityThread的Handler发送一条为“BIND_APPLICATION”的消息,通过这个消息,Handler来处理这次Application的创建过程。这里会创建Application、LoadedApk等。

  1. LoadedApk对象是APK文件在内存中的表示,APl文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。注意:这里会创建一个ClassLoader作为类加载器,也就是我们需要Hook的。
LoadedApk.java    public ClassLoader getClassLoader() {        synchronized (this) {            if (mClassLoader == null) {                createOrUpdateClassLoaderLocked(null /*addedPaths*/);            }            return mClassLoader;        }    }
  1. Activity的创建就是通过反射创建的,使用的就是上面提到的ClassLoader,所以我们只需要Hook住这个ClassLoader,通过类的双亲委派机制来实现我们自己的逻辑即可。

Activity启动过程源码分析如下:(ActivityThread发送一条“LAUNCH_ACTIVITY”的消息给对应的Handler,在处理LAUNCH_ACTIVITY的消息类型处执行handleLaunchAvtivity方法,在handlerLaunchActivity中又执行了PerformLaunchActivity()来完成Activity对象的创建和启动过程。)

PerformLaunchActivity这个方法主要完成了五件事

    1. 从ActivityClientRecord中获取待启动的Activity的组件信息
    1. 通过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填坑的时候起到关键作用

    1. 通过LoadedApk的makeApplication方法来尝试创建Application,这里不贴源码了,如果Application已经被创建过了,就不会重复创建,这就意味着一个应用只有一个Application对象,Application对象的创建也是通过Instrumentation来完成的,这个过程和Activity的创建一样,都是通过类加载器来实现的,Application创建完毕后,系统会通过Instrumentation的callApplicationOnCreate来调用Application的onCreate方法。
    1. 创建ContextImpl对象通过Activity的attach方法来完成一些重要数据的初始化。
    1. 调用Activity的onCreate方法。
      上面五个步骤没贴源码的步骤不是hook中的关键所以没贴源码,详情请拜读《Android开发艺术与探索》的p332

3. Hook代码实现

  1. 创建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. 测试

  1. 在Application中进行初始化
public class MyApplication extends Application{    @Override    public void onCreate() {        super.onCreate();        HookUtils.hookClassLoader(this);    }}
  1. 设置坑位,在AndroidManifest注册一个不存在的Activity
<?xml version="1.0" encoding="utf-8"?>                                                                                        
  1. 启动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。

  1. 创建MainActivity3

运行结果:

插件化开发之坑位的理解(Hook)_第2张图片 image

 

可以看到,通过这种方式实现了不在AndroidManifest中注册,但是可以启动Activity的效果。这里可以应用到插件化中,如Replugin,编译时自动注入坑位,运行时进行确定坑位。

更多相关文章

  1. android使用web加载网页的js问题
  2. 【Android开发小记--6】动画--属性动画以及Fragment切换动画(3D)
  3. Android Binder进程间通信-ServiceManager代理对象的获取过程
  4. Activity属性
  5. android RelativeLayout 布局属性详解
  6. Android 复习笔记之图解TextView类及其XML相关属性和方法
  7. Android 下载进度条, 自定义加载进度条,loading动画
  8. RelativeLayout的常用属性

随机推荐

  1. android 获取手机设备信息
  2. Android(安卓)源码阅读之MMS
  3. android Camera模块分析
  4. Android实现对imageview的拖动以及缩放
  5. android 常用代码
  6. Android(安卓)创建与解析XML(五)—— Dom4j
  7. android handle ui 更新
  8. Android(安卓)View.startAnimation()动画
  9. android设置图片变化的四种效果代码
  10. To make the android emulator (AVD) wor