Replugin源码解析之replugin-host-library---hook点

概述

本文相关系统知识点在 上文 系统ClassLoader相关及Application初始化简单分析及总结 中,由以上文章可知:

系统产生出来的PathClassLoader 仅在
1:packageInfo握有其成员变量引用,
2:当前线程的classLoader
故要替换系统的类加载器只需要从这2个方面入手即可**

接下来就接着 Replugin源码解析之replugin-host-library---多进程初始化及通信 ,由以上文章可知:

RePlugin分别定义了RePluginClassLoader及PluginDexClassLoader,来替代主进程中的classloder及用于加载插件apk。

承接上文剩下未分析的 5.3 PMF.init中最后的PatchClassLoaderUtils.patch(application)及
6.0 PMF.callAttach()为入口分析 ,看replugin 是不是只从以上2点入手 及如何用新的classloader加载插件。

源码分析

com.qihoo360.loader.utils.PatchClassLoaderUtils

public class PatchClassLoaderUtils {

    private static final String TAG = "PatchClassLoaderUtils";

    public static boolean patch(Application application) {
        try {
            // 获取Application的BaseContext (来自ContextWrapper)
            Context oBase = application.getBaseContext();
            if (oBase == null) {
                if (LOGR) {
                    LogRelease.e(PLUGIN_TAG, "pclu.p: nf mb. ap cl=" + application.getClass());
                }
                return false;
            }

            // 获取mBase.mPackageInfo
            // 1. ApplicationContext - Android 2.1
            // 2. ContextImpl - Android 2.2 and higher
            // 3. AppContextImpl - Android 2.2 and higher
            Object oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");
            if (oPackageInfo == null) {
                if (LOGR) {
                    LogRelease.e(PLUGIN_TAG, "pclu.p: nf mpi. mb cl=" + oBase.getClass());
                }
                return false;
            }

            // mPackageInfo的类型主要有两种:
            // 1. android.app.ActivityThread$PackageInfo - Android 2.1 - 2.3
            // 2. android.app.LoadedApk - Android 2.3.3 and higher
            if (LOG) {
                Log.d(TAG, "patch: mBase cl=" + oBase.getClass() + "; mPackageInfo cl=" + oPackageInfo.getClass());
            }

            // 获取mPackageInfo.mClassLoader
            ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
            if (oClassLoader == null) {
                if (LOGR) {
                    LogRelease.e(PLUGIN_TAG, "pclu.p: nf mpi. mb cl=" + oBase.getClass() + "; mpi cl=" + oPackageInfo.getClass());
                }
                return false;
            }

            // 外界可自定义ClassLoader的实现,但一定要基于RePluginClassLoader类
            ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);

            // 将新的ClassLoader写入mPackageInfo.mClassLoader
            ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);

            // 设置线程上下文中的ClassLoader为RePluginClassLoader
            // 防止在个别Java库用到了Thread.currentThread().getContextClassLoader()时,“用了原来的PathClassLoader”,或为空指针
            Thread.currentThread().setContextClassLoader(cl);

            if (LOG) {
                Log.d(TAG, "patch: patch mClassLoader ok");
            }
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

简单看下其中的com.qihoo360.replugin.RePluginCallbacks.createClassLoader方法

//即简单创建RePluginClassLoader实例
public RePluginClassLoader createClassLoader(ClassLoader parent, ClassLoader original) {
        return new RePluginClassLoader(parent, original);
    }

先获取Application,再反射获取其中的mPackageInfo,(mPackageInfo的类型主要有两种:a. android.app.ActivityThread$PackageInfo - Android 2.1 - 2.3;
b. android.app.LoadedApk - Android 2.3.3 and higher),再获取其mClassLoader字段,该字段即为系统创建加载主dex的PathClassLoader,然后以此类加载器为父加载器,创建Replugin自己实现的PathClassLoader子类即RePluginClassLoader,然后 将新创建的PathClassLoader,赋值给上文我们需要替换的2个点 即 packageInfo的成员变量及当前线程的classLoader。到此即成功利用自身的RePluginClassLoader替换宿主的PathClassLoader。
我们看RePluginClassLoader的实现.
com.qihoo360.replugin.RePluginClassLoader

public class RePluginClassLoader extends PathClassLoader{

    。。。。

    public RePluginClassLoader(ClassLoader parent, ClassLoader orig) {

        // 由于PathClassLoader在初始化时会做一些Dir的处理,所以这里必须要传一些内容进来
        // 但我们最终不用它,而是拷贝所有的Fields
        super("", "", parent);
        mOrig = orig;

        // 将原来宿主里的关键字段,拷贝到这个对象上,这样骗系统以为用的还是以前的东西(尤其是DexPathList)
        // 注意,这里用的是“浅拷贝”
        // Added by Jiongxuan Zhang
        copyFromOriginal(orig);
        //反射获取原ClassLoader中的重要方法用来重写这些方法
        initMethods(orig);
    }

    //反射获取原ClassLoader中的方法
     private void initMethods(ClassLoader cl) {
        Class<?> c = cl.getClass();
        findResourceMethod = ReflectUtils.getMethod(c, "findResource", String.class);
        findResourceMethod.setAccessible(true);
        findResourcesMethod = ReflectUtils.getMethod(c, "findResources", String.class);
        findResourcesMethod.setAccessible(true);
        findLibraryMethod = ReflectUtils.getMethod(c, "findLibrary", String.class);
        findLibraryMethod.setAccessible(true);
        getPackageMethod = ReflectUtils.getMethod(c, "getPackage", String.class);
        getPackageMethod.setAccessible(true);
    }

     //拷贝原ClassLoader中的字段到本对象中
     private void copyFromOriginal(ClassLoader orig) {
        if (LOG && IPC.isPersistentProcess()) {
            LogDebug.d(TAG, "copyFromOriginal: Fields=" + StringUtils.toStringWithLines(ReflectUtils.getAllFieldsList(orig.getClass())));
        }

        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
            // Android 2.2 - 2.3.7,有一堆字段,需要逐一复制
            // 以下方法在较慢的手机上用时:8ms左右
            copyFieldValue("libPath", orig);
            copyFieldValue("libraryPathElements", orig);
            copyFieldValue("mDexs", orig);
            copyFieldValue("mFiles", orig);
            copyFieldValue("mPaths", orig);
            copyFieldValue("mZips", orig);
        } else {
            // Android 4.0以上只需要复制pathList即可
            // 以下方法在较慢的手机上用时:1ms
            copyFieldValue("pathList", orig);
        }
    }

    //重写了ClassLoader的loadClass
    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {

        Class<?> c = null;

        //拦截类的加载过程,判断要加载的类是否存在对应的插件信息,如果有从插件中加载
        c = PMF.loadClass(className, resolve);

        if (c != null) {
            return c;
        }

        try {
            //如果没有在插件中找到该类,使用宿主原来的ClassLoader加载
            c = mOrig.loadClass(className);

            return c;
        } catch (Throwable e) {

        }

        return super.loadClass(className, resolve);
    }


    //重写反射的方法,执行的是原ClassLoader的方法
    @Override
    protected URL findResource(String resName) {
        try {
            return (URL) findResourceMethod.invoke(mOrig, resName);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return super.findResource(resName);
    }

    //省略反射重写的其他方法,都是一样的
    。。。。
}

RePluginClassLoader在构造方法中将宿主原来ClassLoader中的重要字段拷贝到本对象中,用来欺骗系统,接着反射获取原ClassLoader中的重要方法用来实现自身对应这些方法。
最后重写了loadClass方法,首先会通过要加载的类名来查找是否存在对应的插件信息,如果有取出插件信息中的ClassLoader,使用该插件的ClassLoader来加载类,如果没有找到再使用宿主原来的ClassLoader来加载,插件使用的ClassLoader就是Replugin中的另一个ClassLoader,PluginDexClassLoader。
我们再从文初说的剩下6.0 PMF.callAttach()为入口,看下是怎么利用另一个ClassLoader即PluginDexClassLoader来加载插件的。

com.qihoo360.loader2 .PMF

public class PMF {
   static PmBase sPluginMgr;
//即调用上文设值进来的PmBase 的callAttach方法
  public static final void callAttach() {
        sPluginMgr.callAttach();
    }
}

接下来看PmBase.callAttach方法
com.qihoo360.loader2.PmBase

    final void callAttach() {
        //
        mClassLoader = PmBase.class.getClassLoader();

        // 挂载
        for (Plugin p : mPlugins.values()) {
            p.attach(mContext, mClassLoader, mLocal);
        }

        // 加载默认插件
        if (PluginManager.isPluginProcess()) {
            if (!TextUtils.isEmpty(mDefaultPluginName)) {
                //
                Plugin p = mPlugins.get(mDefaultPluginName);
                if (p != null) {
                    boolean rc = p.load(Plugin.LOAD_APP, true);
                    if (!rc) {
                        if (LOG) {
                            LogDebug.d(PLUGIN_TAG, "failed to load default plugin=" + mDefaultPluginName);
                        }
                    }
                    if (rc) {
                        mDefaultPlugin = p;
                        mClient.init(p);
                    }
                }
            }
        }
    }

调用Plugin的load方法com.qihoo360.loader2.Plugin

class Plugin {
        final boolean load(int load, boolean useCache) {
        PluginInfo info = mInfo;
      //调用loadLocked方法
        boolean rc = loadLocked(load, useCache);
      ....
}

-----------------------------------
 private boolean loadLocked(int load, boolean useCache) {
  ....
//调用doload方法
boolean rc = doLoad(logTag, context, parent, manager, load);
...


}
------------------------------------------------------
  private final boolean doLoad(String tag, Context context, ClassLoader parent, 
   PluginCommImpl manager, int load) {
        if (mLoader == null) {
       ...
      mLoader = new Loader(context, mInfo.getName(), mInfo.getPath(), this);
            if (!mLoader.loadDex(parent, load)) {
                return false;
            }
       ...
     }
  }
}

看下Loader的loadDex方法
com.qihoo360.loader2.Loader

class Loader {
    final boolean loadDex(ClassLoader parent, int load) {
      ...
      if (BuildConfig.DEBUG) {
                    // 因为Instant Run会替换parent为IncrementalClassLoader,所以在DEBUG环境里
                    // 需要替换为BootClassLoader才行
                    // Added by yangchao-xy & Jiongxuan Zhang
                    parent = ClassLoader.getSystemClassLoader();
                } else {
                    // 线上环境保持不变
                    parent = getClass().getClassLoader().getParent(); // TODO: 这里直接用父类加载器
                }
                String soDir = mPackageInfo.applicationInfo.nativeLibraryDir;
                mClassLoader = RePlugin.getConfig().getCallbacks().createPluginClassLoader(mPluginObj.mInfo, mPath, out, soDir, parent);
      ...
   }
}

调用RePluginCallbacks的createPluginClassLoader方法创建classloader
com.qihoo360.replugin.RePluginCallbacks

 public PluginDexClassLoader createPluginClassLoader(PluginInfo pi, String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        return new PluginDexClassLoader(pi, dexPath, optimizedDirectory, librarySearchPath, parent);
    }

至此第二个classloader即PluginDexClassLoader(用于加载插件)的类加载器出现。
我们看下其实现

public class PluginDexClassLoader extends DexClassLoader {

//构造方法
public PluginDexClassLoader(PluginInfo pi, String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {

    super(dexPath, optimizedDirectory, librarySearchPath, parent);

    //处理多dex
    installMultiDexesBeforeLollipop(pi, dexPath, parent);

    //获取宿主的原始ClassLoader
    mHostClassLoader = RePluginInternal.getAppClassLoader();

    //反射获取原ClassLoader中的loadClass方法
    initMethods(mHostClassLoader);
}

//重写了ClassLoader的loadClass
 @Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    // 插件自己的Class。采用正常的双亲委派模型流程,读到了就直接返回
    Class<?> pc = null;
    ClassNotFoundException cnfException = null;
    try {
        pc = super.loadClass(className, resolve);
        if (pc != null) {

            return pc;
        }
    } catch (ClassNotFoundException e) {
        // Do not throw "e" now
        cnfException = e;
    }

    // 若插件里没有此类,则会从宿主ClassLoader中找,找到了则直接返回
    // 注意:需要读取isUseHostClassIfNotFound开关。默认为关闭的??刹渭每氐乃得?    if (RePlugin.getConfig().isUseHostClassIfNotFound()) {
        try {
            return loadClassFromHost(className, resolve);
        } catch (ClassNotFoundException e) {
            // Do not throw "e" now
            cnfException = e;
        }
    }

    // At this point we can throw the previous exception
    if (cnfException != null) {
        throw cnfException;
    }
    return null;
}

//通过在构造方法中反射原宿主的ClassLoader中的loadClass方法去从宿主中查找
private Class<?> loadClassFromHost(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> c;
    try {
        c = (Class<?>) sLoadClassMethod.invoke(mHostClassLoader, className, resolve);

    } catch (IllegalAccessException e) {

        throw new ClassNotFoundException("Calling the loadClass method failed (IllegalAccessException)", e);
    } catch (InvocationTargetException e) {

        throw new ClassNotFoundException("Calling the loadClass method failed (InvocationTargetException)", e);
    }
    return c;
}

//。。。省略处理多dex文件的代码,原理和上文描述Google的MultiDex的做法一样

这里就比较简单了,因为插件是依赖于宿主生存的,这里只需要将要查找的类找到并返回就可以了,至于其他的操作已经由上面的RePluginClassLoader来处理了,这里还处理了如果插件中早不到类,会去宿主中查找,这里会有一个开关,默认是关闭的,可以通过RePluginConfig的setUseHostClassIfNotFound方法设置。

总结

Replugin通过Hook住系统的PathClassLoader并重写了loadClass方法来实现拦截类的加载过程,并且每一个插件apk都设置了一个PluginDexClassLoader,在加载类的时候先使用这个PluginDexClassLoader去加载,加载到了直接返回否则再通过持有系统或者说是宿主原有的PathClassLoader去加载,这样就保证了不管是插件类、宿主类、还是系统类都可以被加载到。

那么说到思想,Replugin这么做的思想是什么?其实我觉得是破坏了ClassLoader的双亲委派模型,或者说叫打破这种模型,为什么这样说?首先双亲委派模型是层层向上委托的树形加载,而Replugin在收到类加载请求时直接先使用了插件ClassLoader来尝试加载,这样的加载模式应该算是网状加载,所以说Replugin是通过Hook系统ClassLoader来做到破坏了ClassLoader的双亲委派模型,我们再回想一下上一章我们分析过的Replugin框架代码中,Replugin将所以插件apk封装成一个Plugin对象统一在插件管理进程中管理,而每一个插件apk都有属于自己的ClassLoader,在类被加载的时候首先会使用插件自己的ClassLoader去尝试加载,这样做的好处是,可以精确的加载到需要的那个类,而如果使用双亲委派只要找到一个同路径的类就返回,那么这个被返回的类有可能并不是我们需要的那个类。

举个例子,例如两个插件apk中有一个路径和名字完全相同的类,如果使用这种网状加载可以精确的加载到这个类,因为每一个插件apk都有自己的类加载器。而如果还是使用双亲委派模型的话,那么只要找到限定名完全相同的类就会返回,那么这个返回的类并不能保证就是我们需要的那个。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容