插件化开发之坑位的理解(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等。

  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



    
        
            
                

                
            
        
        
    


  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

运行结果:

image

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

你可能感兴趣的:(插件化开发之坑位的理解(Hook))