Android知识体系梳理笔记三:动态代理模式---插件加载机制学习笔记

来源:互联网 发布:手机签字软件 编辑:程序博客网 时间:2024/05/19 23:00

静态代理模式

静态代理模式就是我们常说的代理设计模式,我们采用一个代理类调用原有的方法,且对产生的结果进行控制;举个例子:我们现在在玩一款网络游戏,需要打怪升级;太累就找个代理吧,一觉醒来就会发现我们已经当上CEO,迎娶白富美,天下第一了!

本来我们只能打怪,打怪…,但经过代理类增强,我们不仅可以打怪,还可以升级拿装备。就这样子了!

上代码:
* 同一功能接口

public interface PlayNetGame {    String beatMonster();}
  • 被代理的类
public class PlayNetGameImpl implements PlayNetGame {    @Override    public String beatMonster() {        return "我们要去打怪兽了";    }
  • 代理类
public class PlayNetGameProxy implements PlayNetGame {    private PlayNetGame mPlayNetGame;    public PlayNetGameProxy(PlayNetGame playNetGame){        mPlayNetGame = playNetGame;    }    @Override    public String beatMonster() {        String weapon = "找到屠龙刀";        String beatMonster = mPlayNetGame.beatMonster();        String upGrade = "我升级了";        String wuDi = "当上CEO,迎娶白富美----无敌了";        return weapon+"\n"+"\r"+beatMonster+"\n"+"\r"+upGrade+"\n"+"\r"+wuDi;    }}
  • 使用
public class MainActivity extends AppCompatActivity {    private TextView mTvPlay;    private TextView mTvProxy;    private Button mBtPlay;    private Button mBtProxy;    private PlayNetGame mPlayNetGame;    private PlayNetGame mPlayNetGameProxy;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initView();        initData();    }    private void initData() {        mPlayNetGame = new PlayNetGameImpl();        mPlayNetGameProxy = new PlayNetGameProxy(mPlayNetGame);        mBtPlay.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {            //未被代理时结果                mTvPlay.setText(mPlayNetGame.beatMonster());            }        });        mBtProxy.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {            //代理以后的结果                mTvProxy.setText(mPlayNetGameProxy.beatMonster());            }        });    }    private void initView() {        mTvPlay = (TextView) findViewById(R.id.tv_play);        mTvProxy = (TextView) findViewById(R.id.tv_proxy);        mBtPlay = (Button) findViewById(R.id.bt_play);        mBtProxy = (Button) findViewById(R.id.bt_proxy);    }}

动态代理

  • 写一个代理工具类,让其实现InvocationHandler接口,实现invoke方法,对方法的增强就写在这个方法里
public  class DynamicProxy implements InvocationHandler {    private Object mObject;    public DynamicProxy(Object object) {        mObject = object;    }    /**     * 因为我要增强的方法会返回String字符串,这里Return一个增强的字符串     * @param proxy   proxy:  指代我们所代理的那个真实对象     * @param method  指代的是我们所要调用真实对象的某个方法的Method对象     * @param args args:  指代的是调用真实对象某个方法时接受的参数     * @return     * @throws Throwable     */    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        String weapon = "找到屠龙刀";        String ing=null;        if(method.getName().equals("beatMonster")){             ing = "遇到怪物";            method.invoke(mObject, args);        }else {            method.invoke(mObject, args);        }        String upGrade = "我升级了";        String wuDi = "当上CEO,迎娶白富美----无敌了";        return  weapon+"\n"+"\r"+ing+"\n"+"\r"+ method.invoke(mObject, args)+"\n"+"\r"+upGrade+"\n"+"\r"+wuDi;    }}
  • 使用
        mPlayNetGame = new PlayNetGameImpl();//        mPlayNetGameProxy = new PlayNetGameProxy(mPlayNetGame);        DynamicProxy dynamicProxy = new DynamicProxy(mPlayNetGame);        //CLassLoader loader:类的加载器        //Class<?> interfaces:得到全部的接口        //InvocationHandler h:得到InvocationHandler接口的子类的实例        final PlayNetGame playNetGame = (PlayNetGame) Proxy.newProxyInstance(dynamicProxy.getClass().getClassLoader(),                mPlayNetGame.getClass().getInterfaces(), dynamicProxy);

总结

  • 其实所谓代理,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之前起到中介的作用。

  • 代理对象就是把被代理对象包装一层,在其内部做一些额外的工作,比如用户需要上facebook,而普通网络无法直接访问,网络代理帮助用户先翻墙,然后再访问facebook。这就是代理的作用了。

  • 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强

启动没有在AndroidManifest.xml中显式声明的Activity

  • Activity生命周期管理

    Activity,Service等组件是有生命周期的,它们统一由系统服务AMS(ActivityManagerService)管理;

  • Activity启动过程

    1. 首先是Activity类的startActivity方法;
      最后在startActivityForResult里面调用了Instrument对象的execStartActivity方法;接着在这个函数里面调用了ActivityManagerNative类的startActivity方法,我们知道接下来会通过Binder IPC到AMS所在进程调用AMS的startActivity方法;
    2. 这一系列调用最终到达了ActivityStackSupervisor的realStartActivityLocked方法;人如其名,这个方法开始了真正的“启动Activity”:它调用了ApplicationThread(Binder对象,是App所在的进程与AMS所在进程system_server通信的桥梁)的scheduleLaunchActivity方法,开始了真正的Activity对象创建以及启动过程。
    3. AMS与App主线程的通信通过Handler完成,这个Handler存在于ActivityThread类,它的名字很简单就叫H。
    4. 通信过程原理:(1)App进程会委托AMS进程完成Activity生命周期的管理以及任务栈的管理;这个通信过程AMS是Server端,App进程通过持有AMS的client代理ActivityManagerNative完成通信过程;
      (2)AMS进程完成生命周期管理以及任务栈管理后,会把控制权交给App进程,让App进程完成Activity类对象的创建,以及生命周期回调;这个通信过程也是通过Binder完成的,App进程变为server端,Binder对象存在于ActivityThread的内部类ApplicationThread中;AMS所在client通过持有IApplicationThread的代理对象完成对于App进程的通信。
      image

    5. 完成启动:handler类里直接调用了ActivityThread的handleLaunchActivity方法,这个方法做了两件很重要的事情:

(1)使用ClassLoader加载并通过反射创建Activity对象

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();activity = mInstrumentation.newActivity(        cl, component.getClassName(), r.intent);StrictMode.incrementExpectedActivityCount(activity.getClass());r.intent.setExtrasClassLoader(cl);

(2)如果Application还没有创建,那么创建Application对象并回调相应的生命周期方法

Application app = r.packageInfo.makeApplication(false, mInstrumentation);// ... 省略if (r.isPersistable()) {    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);} else {    mInstrumentation.callActivityOnCreate(activity, r.state);}
  1. 盗图总结-0-

    image

    • 瞒天过海——启动不在AndroidManifest.xml中声明的Activity
  2. 占坑:在AndroidManifest.xml里面声明一个替身Activity
 <!-- 替身Activity, 用来欺骗AMS  -->        <activity android:name=".StubActivity"/>
  1. 启动目标Activity
startActivity(new Intent(MainActivity.this, TargetActivity.class));

ps1: 这个TargetActivity继承的是Activity,不是AppCompatActivity,因为startActivity()是调用的Activity内的方法

ps2:TargetActivity和StubActivity这俩个类文件要放在根包下,不要放到分包里,若把他们放在根目录下新建的包内(会替换不了)

这里写图片描述

经过debug调试,替代类包名路径和目标类包名路径不同,会替换失败?但是我改成下图就会成功

这里写图片描述

3.使用替身Activity绕过AMS:由于AMS进程会对Activity做显式声明验证,因此在
启动Activity的控制权转移到AMS进程之前,我们需要想办法临时把TargetActivity替换成替身StubActivity;

/**     * 主要完成的操作是  "把真正要启动的Activity临时替换为在AndroidManifest.xml中声明的替身Activity"     */    public static void hookActivityManagerNative(){        try {            Class<?> mActivityManagerNative = Class.forName("android.app.ActivityManagerNative");            Field gDefaultField = mActivityManagerNative.getDeclaredField("gDefault");            gDefaultField.setAccessible(true);            Object gDefault = gDefaultField.get(null);            // gDefault是一个 android.util.Singleton对象; 我们取出这个单例里面的字段            Class<?> mSingletion = Class.forName("android.util.Singleton");            Field mInstanceField = mSingletion.getDeclaredField("mInstance");            mInstanceField.setAccessible(true);            // ActivityManagerNative 的gDefault对象里面原始的 IActivityManager对象            Object mIActivityManager = mInstanceField.get(gDefault);            // 创建一个这个对象的代理对象, 然后替换这个字段, 让我们的代理对象帮忙干活            Class<?> iActivityManagerInterfance = Class.forName("android.app.IActivityManager");            Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),                    new Class<?>[]{iActivityManagerInterfance}, new IActivityManagerHandler(mIActivityManager));            mInstanceField.set(gDefault,proxyInstance);        } catch (Exception e) {            e.printStackTrace();        }    }

动态代理类IActivityManagerHandler

class IActivityManagerHandler implements InvocationHandler {    private static final String TAG = "IActivityManagerHandler";    private Object mBase;    public IActivityManagerHandler(Object base) {        mBase = base;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        // 只拦截这个方法        // 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱        // API 23:        // public final Activity startActivityNow(Activity parent, String id,        // Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,        // Activity.NonConfigurationInstances lastNonConfigurationInstances) {        // 找到参数里面的第一个Intent 对象        if ("startActivity".equals(method.getName())) {            Intent raw;            int index = 0;            for (int i = 0; i < args.length; i++) {                if (args[i] instanceof Intent) {                    index = i;                    break;                }            }            raw = (Intent) args[index];            Intent newIntent = new Intent();            // 替身Activity的包名, 也就是我们自己的包名            String stubPackage = "com.example.happyghost.proxystudy";            // 这里我们把启动的Activity临时替换为 StubActivity            ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());            newIntent.setComponent(componentName);            // 把我们原始要启动的TargetActivity先存起来            newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);            // 替换掉Intent, 达到欺骗AMS的目的            args[index] = newIntent;            Log.d(TAG, "hook success");            return method.invoke(mBase, args);        }        return method.invoke(mBase, args);    }}
  1. 拦截Callback从而启动目标类:因为在上面做的步骤都是绕过AMS中的检查,在这之后就是要告诉APP进程你可以开启这个Activity了,AMS进程转移到App进程也是通过Binder调用完成的,承载这个功能的Binder对象是IApplicationThread;在App进程它是Server端,在Server端接受Binder远程调用的是Binder线程池,Binder线程池通过Handler将消息转发给App的主线程;而我们只要Hook掉这个消息,然后把我们的目标类信息在这里替换掉就可以了。

(1)Handler是如何处理接收到的Message的

1. 如果传递的Message本身就有callback,那么直接使用Message对象的callback方法;2. 如果Handler类的成员变量mCallback存在,那么首先执行这个mCallback回调;3. 如果mCallback的回调返回true,那么表示消息已经成功处理;直接结束。4. 如果mCallback的回调返回false,那么表示消息没有处理完毕,会继续使用Handler类的handleMessage方法处理消息。

(2)Handler.Callback是一个接口,我们可以使用动态代理或者普通代理完成Hook,这里我们使用普通的静态代理方式;创建一个自定义的Callback类:

class ActivityThreadHandlerCallback implements Handler.Callback {    Handler mBase;    public ActivityThreadHandlerCallback(Handler base) {        mBase = base;    }    @Override    public boolean handleMessage(Message msg) {        switch (msg.what) {            // ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100            // 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码            case 100:                handleLaunchActivity(msg);                break;        }        mBase.handleMessage(msg);        return true;    }    private void handleLaunchActivity(Message msg) {        // 这里简单起见,直接取出TargetActivity;        Object obj = msg.obj;        // 根据源码:        // 这个对象是 ActivityClientRecord 类型        // 我们修改它的intent字段为我们原来保存的即可./*        switch (msg.what) {/             case LAUNCH_ACTIVITY: {/                 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");/                 final ActivityClientRecord r = (ActivityClientRecord) msg.obj;//                 r.packageInfo = getPackageInfoNoCheck(/                         r.activityInfo.applicationInfo, r.compatInfo);/                 handleLaunchActivity(r, null);*/        try {            // 把替身恢复成真身            Field intent = obj.getClass().getDeclaredField("intent");            intent.setAccessible(true);            Intent raw = (Intent) intent.get(obj);            Intent target = raw.getParcelableExtra(HookHelper.EXTRA_TARGET_INTENT);            raw.setComponent(target.getComponent());        } catch (NoSuchFieldException e) {            e.printStackTrace();        } catch (IllegalAccessException e) {            e.printStackTrace();        }    }}
  1. 我们已经对启动消息做出了处理,我们需要把ActivityThread里面处理消息的Handler类H的的mCallback修改为自定义callback类的对象:
/**     * 由于之前我们用替身欺骗了AMS; 现在我们要换回我们真正需要启动的Activity     * <p/>     * 不然就真的启动替身了, 狸猫换太子...     * <p/>     * 到最终要启动Activity的时候,会交给ActivityThread 的一个内部类叫做 H 来完成     * H 会完成这个消息转发; 最终调用它的callback     */    public static void hookActivityThreadHandler() throws Exception {        // 先获取到当前的ActivityThread对象        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");        Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");        currentActivityThreadField.setAccessible(true);        Object currentActivityThread = currentActivityThreadField.get(null);        // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH        Field mHField = activityThreadClass.getDeclaredField("mH");        mHField.setAccessible(true);        Handler mH = (Handler) mHField.get(currentActivityThread);        // 设置它的回调, 根据源码:        // 我们自己给他设置一个回调,就会替代之前的回调;        //        public void dispatchMessage(Message msg) {        //            if (msg.callback != null) {        //                handleCallback(msg);        //            } else {        //                if (mCallback != null) {        //                    if (mCallback.handleMessage(msg)) {        //                        return;        //                    }        //                }        //                handleMessage(msg);        //            }        //        }        Field mCallBackField = Handler.class.getDeclaredField("mCallback");        mCallBackField.setAccessible(true);        mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));    }
  1. 使用这两个方法,在attachBaseContext()方法里调用这两个方法
@Override    protected void attachBaseContext(Context newBase) {        super.attachBaseContext(newBase);        try {            AMSHookHelper.hookActivityManagerNative();            AMSHookHelper.hookActivityThreadHandler();        } catch (Throwable throwable) {            throw new RuntimeException("hook failed", throwable);        }    }

这样就能在一个Module里启动一个并没有在AndroidManifest.xml中显示声明的Activity

插件加载(Activity加载)

将插件的dex或者apk文件告诉『合适的』DexClassLoader,借助它完成插件类的加载
Activity的创建过程的代码

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);StrictMode.incrementExpectedActivityCount(activity.getClass());r.intent.setExtrasClassLoader(cl);
  • ClassLoader对象的获取

    1. ClasssLoader对象通过r.packageInfo对象的getClassLoader()方法得到,r.packageInfo是一个LoadedApk类的对象;
    2. LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。
    3. getPackageInfo方法里判断了调用方和获取App信息的一方是不是同一个userId,如果是同一个user,那么可以共享缓存数据(要么缓存的代码数据,要么缓存的资源数据),在获取共享缓存时,如果没有找到缓存数据,才通过LoadedApk的构造函数创建了LoadedApk对象;创建成功之后,如果是同一个uid还放入了缓存。
  • 加载插件类的第一种方案:Hook掉ClassLoader,自己操刀

    1. 在获取LoadedApk的过程中使用了一份缓存数据;这个缓存数据是一个Map,从包名到LoadedApk的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接找到缓存!进而使用我们添加进去的LoadedApk的ClassLoader来加载这个特定的Activity类!这样我们就能接管我们自己插件类的加载过程了!

    2. 这个缓存对象mPackages存在于ActivityThread类中

// 先获取到当前的ActivityThread对象Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");currentActivityThreadMethod.setAccessible(true);Object currentActivityThread = currentActivityThreadMethod.invoke(null);// 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");mPackagesField.setAccessible(true);Map mPackages = (Map) mPackagesField.get(currentActivityThread);
  1. 我们把插件的信息塞进这个map里面,以便系统在查找的时候能找到插件缓存。填充这个Map我们出了需要包名之外,还需要一个LoadedApk对象(可以直接反射调用它的构造函数直接创建出需要的对象,不严谨)

    (1)我们在这里选择使用 **getPackageInfoNoCheck(ApplicationInfo ai,
    CompatibilityInfo compatInfo)** 获取LoadedApk信息。为了调用这个函数,我们需要构造两个参数。其一是ApplicationInfo,其二是CompatibilityInfo;第二个参数顾名思义,代表这个App的兼容性信息,比如targetSDK版本等等,这里我们只需要提取出app的信息,因此直接使用默认的兼容性即可;在CompatibilityInfo类里面有一个公有字段DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息

    (2) 获取这个ApplicationInfo信息,这个类就是AndroidManifest.xml里面的 这个标签下面的信息;这个AndroidManifest.xml无疑是一个标准的xml文件,因此我们完全可以自己使用parse来解析这个信息。
    (3) 使用PackageParser类的 generateApplicationInfo(Package p, int flags, PackageUserState state) 方法获取ApplicationInfo信息,反射调用其

Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");// 首先拿到我们得终极目标: generateApplicationInfo方法// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!// public static ApplicationInfo generateApplicationInfo(Package p, int flags,//    PackageUserState state) {// 其他Android版本不保证也是如此.Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",        packageParser$PackageClass,        int.class,                packageUserStateClass);

要成功调用这个方法,还需要三个参数;因此接下来我们需要一步一步构建调用此函数的参数信息。

  • 构建PackageParser.Package :这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;因此,要获取这个类,肯定需要解析整个apk文件。PackageParser中解析apk的核心方法是parsePackage,这个方法返回的就是一个Package类型的实例,因此我们调用这个方法即可;使用反射代码如下:
// 首先, 我们得创建出一个Package对象出来供这个方法调用// 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到// 创建出一个PackageParser对象供使用Object packageParser = packageParserClass.newInstance();// 调用 PackageParser.parsePackage 解析apk的信息Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);// 实际上是一个 android.content.pm.PackageParser.Package 对象Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);

这样,我们就得到了generateApplicationInfo的第一个参数;第二个参数是解析包使用的flag,我们直接选择解析全部信息,也就是0;

  • 构建PackageUserState:代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可
// 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可Object defaultPackageUserState = packageUserStateClass.newInstance();
  • 我们把解析的apk文件的路径设置一下(ClassLoader依赖dex文件以及apk的路径):
// 万事具备!!!!!!!!!!!!!!ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,        packageObj, 0, defaultPackageUserState);String apkPath = apkFile.getPath();applicationInfo.sourceDir = apkPath;applicationInfo.publicSourceDir = apkPath;
  • 自此已经拿到了getPackageInfoNoCheck这个方法中至关重要的第一个参数applicationInfo;上文提到第二个参数CompatibilityInfo代表设备兼容性信息,直接使用默认的值即可;因此,两个参数都已经构造出来,我们可以调用getPackageInfoNoCheck获取LoadedApk:
// android.content.res.CompatibilityInfoClass<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");defaultCompatibilityInfoField.setAccessible(true);Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);
  • 构造出了LoadedAPK, 接下来我们需要替换其中的ClassLoader,然后把它添加进ActivityThread的mPackages中:
String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");mClassLoaderField.setAccessible(true);mClassLoaderField.set(loadedApk, classLoader);// 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.sLoadedApk.put(applicationInfo.packageName, loadedApk);WeakReference weakReference = new WeakReference(loadedApk);mPackages.put(applicationInfo.packageName, weakReference);
  • 通过以上步骤大体已经完成,但是,我们还要欺骗一下PMS,让系统觉得插件已经安装在系统上了
private static void hookPackageManager() throws Exception {    // 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装    // 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");    currentActivityThreadMethod.setAccessible(true);    Object currentActivityThread = currentActivityThreadMethod.invoke(null);    // 获取ActivityThread里面原始的 sPackageManager    Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");    sPackageManagerField.setAccessible(true);    Object sPackageManager = sPackageManagerField.get(currentActivityThread);    // 准备好代理对象, 用来替换原始的对象    Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");    Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),            new Class<?>[] { iPackageManagerInterface },            new IPackageManagerHookHandler(sPackageManager));    // 1. 替换掉ActivityThread里面的 sPackageManager 字段    sPackageManagerField.set(currentActivityThread, proxy);}
  • 加载插件类的第二种方案:委托系统,让系统帮忙加载

    1. 我们可以通过反射,代理等技术让宿主的ClasLoader获得加载插件类的能力;
    2. 由类加载机制的『双亲委派』特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。
    3. 我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader。
    4. 应用程序使用的ClassLoader都是PathClassLoader类的实例,而在其父类BaseDexClassLoader 中有findClass方法会调用DexPathList类的findClass方法:遍历dexElements数组查找Class
    5. 把插件的相关信息放入这个数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载!
    6. 实现代码
public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)        throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {    // 获取 BaseDexClassLoader : pathList    Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");    pathListField.setAccessible(true);    Object pathListObj = pathListField.get(cl);    // 获取 PathList: Element[] dexElements    Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");    dexElementArray.setAccessible(true);    Object[] dexElements = (Object[]) dexElementArray.get(pathListObj);    // Element 类型    Class<?> elementClass = dexElements.getClass().getComponentType();    // 创建一个数组, 用来替换原始的数组    Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);    // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数    Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);    Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));    Object[] toAddElementArray = new Object[] { o };    // 把原始的elements复制进去    System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);    // 插件的那个element复制进去    System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);    // 替换    dexElementArray.set(pathListObj, newElements);}

两种方案优劣点

  • 优点
    1. 第一种方案是多ClassLoader构架,每一个插件都有一个自己的ClassLoader,因此类的隔离性非常好——如果不同的插件使用了同一个库的不同版本,它们相安无事!可以真正完成代码的热加载!如果插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可。
    2. 第二种方案则简单得多(虽然原理也不简单),不仅代码很少,而且Hook的地方也不多;
  • 缺点
    1. 第一种方案比较麻烦,从代码量和分析过程就可以看出来,这种机制异常复杂;而且在解析apk的时候我们使用的PackageParser的兼容性非常差,我们不得不手动处理每一个版本的apk解析api;另外,它Hook的地方也有点多:不仅需要Hook AMS和H,还需要Hook ActivityThread的mPackages和PackageManager!
    2. 第二种方案是单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,虽然代码简单,但是鲁棒性很差;一旦插件之间甚至插件与宿主之间使用的类库有冲突,那么直接GG;如果插件需要升级,单ClassLoader的话实现非常麻烦,有可能需要重启进程。

推荐一位大神的博客,大神写的binder理解和插件加载机制都是读了好多遍的!真的是干货满满啊,膜拜中-0-

拼搏在技术道路上的一只小白And成长之路

阅读全文
0 0
原创粉丝点击