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)






注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com