Android 源码系列之<十九>自定义Gradle Plugin,优雅的解决第三方Jar包中的bug<下>

来源:互联网 发布:京沪高铁 知乎 编辑:程序博客网 时间:2024/04/29 19:54

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





阅读全文
0 0