转载请注明出处: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)