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閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容