Android 源碼系列之自定義Gradle Plugin,優雅的解決第三方Jar包中的bug

       轉載請註明出處:http://blog.csdn.net/llew2011/article/details/78628613

       前邊兩篇文章Android 源碼系列之<十七>自定義Gradle Plugin,優雅的解決第三方Jar包中的bug<上>Android 源碼系列之<十八>自定義Gradle Plugin,優雅的解決第三方Jar包中的bug<中>裏主要講解了如何自定義Gradle Plugin,然後利用自定義的Gradle Plugin插件來修復項目中引用的第三方Jar包中的bug的方法,其核心就是利用開源庫Javassist修復第三方Jar包中的class文件,然後在項目打包的時候把修復過的Jar包打包進項目中從而達到修復的目的。如果有小夥伴還沒看過前兩篇文章,強烈建議閱讀一下。這篇文章我們就從源碼的角度深入理解一下Javassist庫是如何修復class文件的(*^__^*) ……

       閱讀開源代碼,一般都是從使用開始,記得在上篇文章中我們是如何使用Javassist庫的麼?首先是初始化了ClassPool對象sClassPool,代碼如下:

public static void init(Project project, String versionName, BytecodeFixExtension extension) {
    sClassPool = ClassPool.default
    sInjector = new BytecodeFixInjector(project, versionName, extension)
}
       在BytecodeFixInjector的init()方法中通過ClassPool的靜態方法getDefault()返回一個ClassPool對象然後賦值給了sClassPool,ClassPool是做什麼工作的?它的職責是什麼?根據名字像是一個對象池,既然是對象池,應該像數據庫連接池一樣能提供對象的哈,這是我第一次接觸它的時候猜測的,我們看一下ClassPool的說明:

       A container of CtClass objects. A CtClass object must be obtained from this object. If get() is called on this object, it searches various sources represented by ClassPath to find a class file and then it creates a CtClass object representing that class file. The created object is returned to the caller.

       【譯】ClassPool是CtClass的容器,每一個CtClass對象都必須從ClassPool中獲取。如果調用了ClassPool的get()方法,那麼ClassPool就會搜索由ClassPath指定的不同資源去找到一個class文件然後ClassPool就會創建一個CtClass對象,該對象就代表着那個.class文件。最後ClassPool創建的CtClass對象會返回給調用者。

       ClassPool objects hold all the CtClasses that have been created so that the consistency among modified classes can be guaranteed. Thus if a large number of CtClasses are processed, the ClassPool will consume a huge amount of memory. To avoid this, a ClassPool object should be recreated, for example, every hundred classes processed. Note that getDefault() is a singleton factory. Otherwise, detach() in CtClass should be used to avoid huge memory consumption.

       【譯】ClassPool持有所有創建的CtClass對象,因此修改類的話,它們之間的一致性可以得到保證。因此,如果處理大量的CtClass類,ClassPool將要消耗大量的內存,爲了避免這種情況,應該重新創建ClassPool對象,例如,每次都要處理成千上百的class類。注意,getDefault()方法是一個單例模式的工廠方法,因此,應該調用detach()方法來避免大量的內存消耗。

       ClassPools can make a parent-child hierarchy as java.lang.ClassLoaders. If a ClassPool has a parent pool, get() first asks the parent pool to find a class file. Only if the parent could not find the class file, get() searches the ClassPaths of the child ClassPool. This search order is reversed if ClassPath.childFirstLookup is true.

       【譯】ClassPool支持像java.lang.ClassLoaders那樣的父子層次結構,如果ClassPool有個父類ClassPool,當調用ClassPool的get()方法時,ClassPool會首先請求父類ClassPool查詢相應的class文件,只有在父類ClassPool找不到的情況下,纔會調用自身的get()方法查詢

       根據ClassPool的說明,我們可以得出一下幾點重要信息:

  • CtClass代表一個.class文件,它必須由ClassPool創建
  • ClassPool可能消耗較大內存,應當及時調用detach()方法
  • ClassPool支持像ClassLoader一樣的雙親委派模型
       理解了ClassPool之後,我們看看ClassPool的getDefault()方法,代碼如下:
public static synchronized ClassPool getDefault() {
    if (defaultPool == null) {
        defaultPool = new ClassPool(null);
        defaultPool.appendSystemPath();
    }

    return defaultPool;
}

public ClassPool(ClassPool parent) {
    this.classes = new Hashtable(INIT_HASH_SIZE);
    this.source = new ClassPoolTail();
    this.parent = parent;
    if (parent == null) {
        CtClass[] pt = CtClass.primitiveTypes;
        for (int i = 0; i < pt.length; ++i)
            classes.put(pt[i].getName(), pt[i]);
    }

    this.cflow = null;
    this.compressCount = 0;
    clearImportedPackages();
}
       getDefault()方法首先判斷defaultPool是否爲null,如果爲null就創建,在創建的ClassPool對象的時候給其構造方法傳遞一個null值進去(傳遞null值表示當前ClassPool是根節點ClassPool)。在ClassPool的構造方法內部初始化了緩存CtClass對象的classes成員變量和ClassPoolTail類型的成員變量source(ClassPoolTail模擬了鏈表的數據結構,它存儲了一個鏈式順序的ClassPath),最後調用source的appendSystemPath()方法,代碼如下:
public ClassPath appendSystemPath() {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return appendClassPath(new LoaderClassPath(cl));
}
       ClassPoolTail的appendSystemPath()方法中先獲取當前線程的ClassLoader對象,然後根據當前線程的ClassLoader對象創建了一個LoaderClassPath對象並傳遞進了重載方法appendClassPath(),代碼如下:
public synchronized ClassPath appendClassPath(ClassPath cp) {
    ClassPathList tail = new ClassPathList(cp, null);
    ClassPathList list = pathList;
    if (list == null)
        pathList = tail;
    else {
        while (list.next != null)
            list = list.next;

        list.next = tail;
    }

    return cp;
}
       appendClassPath()方法就是把傳遞進來ClassPath存儲在鏈表的最後,根據代碼調用順序來看,剛剛創建的LoaderClassPath就是ClassPoolTail中ClassPath鏈表的根節點了,而LoaderClassPath的ClassLoader又是當前線程的ClassLoader,熟悉JVM ClassLoader的結構順序應該清楚,LoaderClassPath包含了當前系統環境變量指定的ClassPath,所以在使用ClassPool的時候默認包含了環境變量配置的那些SDK包。以上就是ClassPool的主要流程,我們接着看一下CtClass的使用,如下所示:
CtClass ctClass = sClassPool.getCtClass(className)
       CtClass實例只能通過ClassPool獲取,ClassPool提供了系列的方法來返回CtClass實例,這些方法最終都是調用get0()方法,代碼如下:
protected synchronized CtClass get0(String classname, boolean useCache) throws NotFoundException {
    CtClass clazz = null;
    if (useCache) {
        clazz = getCached(classname);
        if (clazz != null)
            return clazz;
    }

    if (!childFirstLookup && parent != null) {// 默認情況下parent爲null
        clazz = parent.get0(classname, useCache);
        if (clazz != null)
            return clazz;
    }

    clazz = createCtClass(classname, useCache);
    if (clazz != null) {
        // clazz.getName() != classname if classname is "[L<name>;".
        if (useCache)
            cacheCtClass(clazz.getName(), clazz, false);// 加入緩存

        return clazz;
    }

    if (childFirstLookup && parent != null)// 默認情況下parent爲null
        clazz = parent.get0(classname, useCache);

    return clazz;
}
       get0()方法中首先從緩存中查找,如果緩存中存在就直接返回緩存中的CtClass對象,否則調用createCtClass()方法創建CtClass對象然後根據參數useCache判斷是否緩存新建的CtClass對象,createCtClass()方法代碼如下:
protected CtClass createCtClass(String classname, boolean useCache) {
    // accept "[L<class name>;" as a class name. 【classname可以死[L<class name>;的參數,不過不建議傳遞這種參數】
    if (classname.charAt(0) == '[')
        classname = Descriptor.toClassName(classname);

    if (classname.endsWith("[]")) {
        String base = classname.substring(0, classname.indexOf('['));
        if ((!useCache || getCached(base) == null) && find(base) == null)
            return null;
        else
            return new CtArray(classname, this);
    } else
        if (find(classname) == null)// 調用find()方法來遍歷ClassPathList鏈表從而查詢對應的class文件
            return null;
        else
            return new CtClassType(classname, this);
}
       createCtClass()方法根據條件判斷最後通過find()方法做查找,如果查找到了對應的classname就根據classname創建一個CtClassType並返回(由此可見CtClassType一定是CtClass實現類,之後CtClass的行爲也就是CtClassType的行爲了)。創建了CtClass後就可以進行一系列的操作了,比如添加屬性,添加方法,修改方法等。我們就拿修改方法舉例子。對方法的相關操作必須使用CtMethod對象,它需要從CtClass中獲取,代碼如下:
CtMethod ctMethod = ctClass.getDeclaredMethod(methodName)
       通過調用ctClass的getDeclaredMethod(methodname)方法實際上執行的的是CtClassType的getDeclaredMethod(methodname)方法,我們直接看CtClassType中的getDeclaredMethod()方法實現,代碼如下:
public CtMethod getDeclaredMethod(String name) throws NotFoundException {
    CtMember.Cache memCache = getMembers();
    CtMember mth = memCache.methodHead();
    CtMember mthTail = memCache.lastMethod();
    while (mth != mthTail) {
        mth = mth.next();
        if (mth.getName().equals(name))
            return (CtMethod)mth;
    }

    throw new NotFoundException(name + "(..) is not found in " + getName());
}
       getDeclaredMethod()方法中調用了返回Cache類型的getMembers()方法,getMembers()方法主要功能是解析當前class的屬性和方法並做緩存,然後遍歷當前class的所有方法,當遍歷到的CtMethod的name和傳遞進來的name相等就返回該CtMethod,如果匹配不到就拋異常。
       CtMethod提供了一系列的對方法的操作方法,比如inserBifore(),intsertAfter(),setBody()等衆多方法,我們就看setBody()方法(其它操作流程都是類似的),該方法表示重置方法體,代碼如下:
public void setBody(String src) throws CannotCompileException {
    setBody(src, null, null);
}

public void setBody(String src, String delegateObj, String delegateMethod) throws CannotCompileException {
    CtClass cc = declaringClass;
    cc.checkModify();
    try {
        Javac jv = new Javac(cc);
        if (delegateMethod != null) {
            jv.recordProceed(delegateObj, delegateMethod);
        }

        Bytecode b = jv.compileBody(this, src);
        methodInfo.setCodeAttribute(b.toCodeAttribute());
        methodInfo.setAccessFlags(methodInfo.getAccessFlags() & ~AccessFlag.ABSTRACT);
        methodInfo.rebuildStackMapIf6(cc.getClassPool(), cc.getClassFile2());
        declaringClass.rebuildClassFile();
    } catch (CompileError e) {
        throw new CannotCompileException(e);
    } catch (BadBytecode e) {
        throw new CannotCompileException(e);
    }
}
       CtMethod的setBody()有兩個重載方法,最終都是調用三個參數的的setBody()方法,在setBody()方法中根據CtClass新建了一個Javac對象(javassist包含了一個小的Java編譯器系統,其中Javac就是模擬的JDK中的javac命令,它用來把Java代碼編譯成二進制的class文件)。接着調用Javac的compileBody()方法把傳遞進來的src編譯成二進制字節碼,由於篇幅原因以具體的Javac的編譯細節就不再這裏展開敘述了,如果有小夥伴想詳細的瞭解JVM指令,這裏推薦小夥伴們看一下《Java虛擬機規範》和《深入理解Java虛擬機》這兩本書,書中講解的很詳細,強烈建議閱讀一下。
       通過CtMethod修改了CtClass的方法之後,如果想持久化存儲修復後的class,可以調用CtClass的writeFile()方法,writeFile()源碼如下:
public void writeFile() throws NotFoundException, IOException, CannotCompileException {
    writeFile(".");
}

public void writeFile(String directoryName) throws CannotCompileException, IOException {
    DataOutputStream out = makeFileOutput(directoryName);
    try {
        toBytecode(out);
    } finally {
        out.close();
    }
}
       CtClass的writeFile()方法同樣有兩個重載方法,無參數的writeFile()方法表示把CtClass直接存儲在當前目錄下的clsass文件中,帶有參數的writeFile(String dir)表示可以把修改後的CtClass寫入指定目錄中。
       以上就是CtClass的一般操作流程,爲了方便查看CtClass的流程,下面我畫了一張javassist庫的部分結構圖,如下所示:

       關於使用Javassist庫修改class文件的流程基本上就是這些了,該庫的核心就是根據JVM規範自定義了一套編譯器把我們的傳遞進來的字符串編譯成JVM可識別和執行的二進制字節碼,如果想要詳細的瞭解JVM請自行查閱相關文檔。
       在上篇文章中我寫了一個插件BytecodeFixer插件,並給小夥伴們講解了如何使用該插件,如果不清楚該插件的使用請閱讀上篇文章:Android 源碼系列之<十八>自定義Gradle Plugin,優雅的解決第三方Jar包中的bug<中> ,這裏再補充一下使用BytecodeFixer插件的注意事項:
  • 在對CtClass的操作中除了基本類型外,其他任何類型都要使用類的全路徑
  • 操作方法時$0表示this關鍵字,$1表示第一個參數,$2表示第二個參數,以此類推
  • 爲了保證Mac和Windows系統下路徑的兼容性,一定要使用File.separator進行路徑的拼接
  • 如果待修復的Jar包中需要引用主項目的類,可以在dependencies配置項依賴添加getAppClassesDir()方法,如下所示:
    apply plugin: 'com.llew.bytecode.fix'
    
    bytecodeFixConfig {
    
        enable = true
    
        logEnable = true
    
        keepFixedJarFile = true
    
        keepFixedClassFile = true
    
        dependencies = [
                getAppClassesDir()
        ]
    
        fixConfig = [
                'com.tencent.av.sdk.NetworkHelp##getMobileAPInfo(android.content.Context,int)##if(android.content.pm.PackageManager.PERMISSION_GRANTED != $1.checkPermission(android.Manifest.permission.READ_PHONE_STATE, android.os.Process.myPid(), android.os.Process.myUid())){return new com.tencent.av.sdk.NetworkHelp.APInfo();}##0',
                'com.umeng.qq.tencent.h##a(android.app.Activity,android.content.Intent,int)##try{$2.putExtra("key_request_code", $3);$1.startActivityForResult($0.a($1, $2), $3);} catch(Exception e) {e.printStackTrace();};##-1',
                'com.umeng.qq.tencent.h##a(android.app.Activity,int,android.content.Intent,java.lang.Boolean)##try{android.content.Intent var5 = new android.content.Intent($1.getApplicationContext(), com.umeng.qq.tencent.AssistActivity.class);if($4.booleanValue()){var5.putExtra("is_qq_mobile_share", true);}var5.putExtra("openSDK_LOG.AssistActivity.ExtraIntent", $3);$1.startActivityForResult(var5, $2);}catch(Exception e){e.printStackTrace();};##-1'
        ]
    }
    
    String getAppClassesDir() {
    
        android.applicationVariants.all { variant ->
    
            def variantOutput = variant.outputs.first()
            def variantName = variant.name
            def variantData = variant.variantData
            def buildType   = variant.buildType.name
    
            def str = new StringBuffer().append(project.rootDir.absolutePath)
                    .append(File.separator).append("app")
                    .append(File.separator).append("build")
                    .append(File.separator).append("intermediates")
                    .append(File.separator).append("classes")
                    .append(File.separator).append(variantName.subSequence(0, buildType.length()))
                    .append(File.separator).append(buildType)
                    .append(File.separator).toString()
            return str
        }
    
        return new StringBuffer().append(project.rootDir.absolutePath)
            .append(File.separator).append("app")
            .append(File.separator).append("build")
            .append(File.separator).append("intermediates")
            .append(File.separator).append("classes")
            .append(File.separator).append("dev")
            .append(File.separator).append("debug")
            .append(File.separator).toString()
    }

       好了,到這裏有關自定義Gradle Plugin來解決第三方Jar包中的bug就要搞一段落了,感謝小夥伴們的收看(*^__^*) ……


       BytecodeFixer地址:https://github.com/llew2011/BytecodeFixer

    (歡迎fork and star)





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章