轉載請註明出處: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一樣的雙親委派模型
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();
}
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);
}
}
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寫入指定目錄中。關於使用Javassist庫修改class文件的流程基本上就是這些了,該庫的核心就是根據JVM規範自定義了一套編譯器把我們的傳遞進來的字符串編譯成JVM可識別和執行的二進制字節碼,如果想要詳細的瞭解JVM請自行查閱相關文檔。
- 在對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)