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

来源:互联网 发布:在a标签传随机参数js 编辑:程序博客网 时间:2024/05/17 01:47

       转载请注明出处:http://blog.csdn.net/llew2011/article/details/78540911

       我们在开发中经常用到一些优秀的第三方库,比如okhttp,glide,butterknife等。这些库不仅提高了开发效率而且避免踩坑,假如在应用中这些开源库出现了bug,我们随时可以从GitHub下载源码进行bug修改。但是项目中使用的库不是开源的并且该库又存在bug,由于没有源码也就无法进行bug的修复,一般做法就是给非开源库的作者或组织反馈bug等他们进行修复,如果他们修复的及时还好说,一旦他们更新不及时就会给我们APP造成不好影响(比如用户流失)……这篇文章我就给小伙伴们讲解一下如何自定义Gradle Plugin来彻底解决第三方Jar包中的bug(*^__^*) ……

       入职新公司以来一直把精力放在了新公司原有项目的重构上,与其说是重构还不如说是重写了一遍,重构期间整个Android团队压力还是蛮大的,一方面在开发新需求另一方面在进行项目重构,经过几个月的辛劳重构后,新项目顺利上线了(在这里对整个团队表示感谢),重构后的新版本上线后我一直在Bugly上关注着它的稳定情况,尤其是崩溃率。从Bugly的日志上我发现有几个crash从旧版本到重构后的新版本一直存在着,且这些crash都是发生在第三方Jar包中的,其中一个crash日志如下所示:

     

       日志信息清楚的表明该crash是用户没有授权导致的,由于我们APP集成了企鹅厂家的直播SDK,在用户退出直播间后都会调用SDK的退出直播间方法,也就是说崩溃发生在鹅厂的SDK内部,尽管我们在进入APP中都有向用户申请READ_PHONE_STATE权限,但还是存在部分用户不予授权的情况,以上crash就是用户没有授权导致的。因此我把这个问题反馈给了鹅厂的SDK研发组那里,并问他们能不能做下兼容(就是添加权限的判断),他们很快给予了回复说没法做兼容并给了个解决办法:如果用户没授权就给用户提示然后强制退出APP。如果按照鹅厂的解决办法那就代表着用户不授予权限就不能使用我们APP了,这显然是不可接受的,因为这样很容易造成用户流失……

       既然鹅厂那边不愿意解决该问题,那我自己来解决!!!在解决该问题前我们先看看他们SDK中NetworkHelp类下的getAPInfo()方法和getMobileAPInfo()方法实现逻辑是什么样的,因为从Bugly日志看崩溃前调用了NetworkHelp中的这俩方法。反编译NetworkHelp.class文件,核心代码如下所示:

public class NetworkHelp {    protected static NetworkHelp.APInfo getAPInfo(Context var0) {        NetworkHelp.APInfo var1 = new NetworkHelp.APInfo();        if(var0 == null) {            QLog.e("NetworkHelp", 0, "getAPInfo initial context is null");            return var1;        } else {            ConnectivityManager var2 = (ConnectivityManager)var0.getSystemService("connectivity");            NetworkInfo var3 = var2.getActiveNetworkInfo();            if(var3 != null && var3.isConnected()) {                switch(var3.getType()) {                case 0:                    // 在这里调用了getMobileAPInfo()方法                    var1 = getMobileAPInfo(var0, var3.getSubtype());                    break;                // 省略相关代码......            }            return var1;        }    }    private static NetworkHelp.APInfo getMobileAPInfo(Context var0, int var1) {        TelephonyManager var2 = (TelephonyManager)var0.getSystemService("phone");        NetworkHelp.MobileCarrier var3 = NetworkHelp.MobileCarrier.UNKNOWN;        String var4 = var2.getSubscriberId(); // getSubscriberId()内部抛的异常                // 省略相关代码...        NetworkHelp.APInfo var5 = new NetworkHelp.APInfo();                // 省略相关代码...        return var5;    }    public static class APInfo {        public int apType;        public String apName;        public APInfo() {// 当创建该对象的时候,属性会赋予默认值            this.apType = NetworkHelp.APType.AP_UNKNOWN.value();            this.apName = "AP_UNKNOWN";        }    }    // 省略相关代码...}
       根据反编译的NetworkHelp源码可以看出在getAPInfo()方法中首先判断Context类型的参数var0是否为null,如果为null则直接返回APInfo对象,而APInfo对象在创建的时候会把内部属性赋予默认值,也就是说如果我们在getAPInfo()方法中想办法让参数var0为null不就可以避免那些没有授权的用户发生崩溃了么?这是一个Hook点,简单粗暴(*^__^*) ……我们继续往下读getAPInfo()的代码,如果参数var0不为空且网络是OK的,就会走到getMobileInfo()方法中,在getMobileInfo()方法中调用了TelephoneManager的getSubscriberId()方法,而getSubscriberId()方法内部执行过程中会做权限校验,在未授权的情况下会抛出异常,因此只要在getMobileInfo()方法中调用getSubscriberId()前添加判断是否有权限的代码块就OK了,这又是一个Hook点(*^__^*) ……根据以上两个Hook点,我们对NetworkHelp做Hook后的代码应该是如下的样子:
public class NetworkHelp {    protected static NetworkHelp.APInfo getAPInfo(Context var0) {        // Hook点1,添加如下一句代码,简单粗暴        var0 = null;        NetworkHelp.APInfo var1 = new NetworkHelp.APInfo();        if(var0 == null) {            QLog.e("NetworkHelp", 0, "getAPInfo initial context is null");            return var1;        } else {            // 省略相关代码...            return var1;        }    }    private static NetworkHelp.APInfo getMobileAPInfo(Context var0, int var1) {        // Hook点2,考虑SDK的感受,让他们拿到网络信息(*^__^*)        if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(var0, Manifest.permission.READ_PHONE_STATE)) {            return new NetworkHelp.APInfo();        }        TelephonyManager var2 = (TelephonyManager)var0.getSystemService("phone");        NetworkHelp.MobileCarrier var3 = NetworkHelp.MobileCarrier.UNKNOWN;        String var4 = var2.getSubscriberId(); // getSubscriberId()内部抛的异常        // 省略相关代码...        return var5;    }    // 省略相关代码...}

       好了,清楚了对NetworkHelp的Hook点后,接下来就是考虑如何对NetworkHelp.class类进行修改了,要修改class文件就要清楚JVM指令,因为class文件最后都是通过类加载器加载运行在JVM上的,只有清楚JVM指定才能按照JVM规范来修改class文件。好在这个世界总有那么一些神一般存在的大神,对于class文件的修改操作(添加方法,修改方法、字段等)已经有位日本的大学教授封装好了面向Java API编程的字节码操作库Javassist,该库目前已收录于jboss开源项目中。使用Javassist库就可以避免和JVM指令打交道,从而让修改class文件变得简单Happy起来。【注意:】这篇文章仅仅介绍Javassist的简单用法,后续文章将介绍它的详细用法以及带领小伙伴来读一下Javassist的源码,敬请期待……

       有了操作class文件的Javassist库,就可以对class文件进行修改了,修改流程是:首先解压第三方Jar文件,拿到要修复的class文件,其次利用Javassist库进行class文件修改,修改完打包成Jar文件后覆盖原有的Jar文件,之后项目打包也就直接把修复过的Jar文件打包进去了。这个流程理论上是没有问题的,但是当我们引用的第三方Jar包是通过Gradle来配置的话就会存在冗余工作的问题,例如ConstraintLayout包:

compile 'com.android.support.constraint:constraint-layout:1.0.2'

       Gradle文件中添加如上配置后,它就会从JCenter仓库下载相应配置的Jar文件保存在以版本号命名的本地文件夹中,若按照上述流程进行class文件的修复是没有问题的,但是当ConstraintLayout进行了版本升级,这个时候Gradle就又会下载最新包到本地,这个时候我们又得从新执行以上的修复流程,这样十分繁琐,作为程序员一定要记住:能用机器去做的就坚决不要让人去做,自动化一切可以自动化的。所以我们的目标的是write once, run anywhere,无论依赖的Jar包今后升级与否,只写一遍代码就能统统搞定(*^__^*) ……

       既然要实现一次编码就能满足今后所依赖的Jar文件升级与否的功能,我们就要在项目打包成APK前找到一个节点,这个节点一定是在Jar包被转换成Dex文件前,因为在Gradle打包流程中一旦Jar包被转换成Dex文件后,我们再对Jar文件进行处理就已经失去意义了,好在Gradle plugin在1.5.0版本后给我们提供了API Transform,该Transform的作用如官方所说:

       Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files. (The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)

       也就是说,Gradle Plugin 在1.5.0之后的版本中,Gradle Plugin提供了Transform API,该Transform API 允许第三方插件在class文件被转换成Dex文件之前有机会处理到这些class文件。

       现在我们有了对class处理的Javassist库,也找到了对class文件的处理时机,接下来就是自定义Gradle Plugin来实现对Jar文件的修复了。首先在我们项目中创建一个Android Library Module取名为plugin,该plugin就是用来开发Gradle插件的。然后清空plugin下的其他文件,只保留build.gradle和src/main目录,在build.gradle中添加如下配置:

apply plugin: 'groovy'repositories {    jcenter()}dependencies {    compile gradleApi()    compile localGroovy()    compile 'com.android.tools.build:gradle:2.3.3'    compile 'org.javassist:javassist:3.20.0-GA'}

       添加以上配置后同步一下代码就OK了,这段配置代码主要是声明plugin插件使用的Gradle SDK和Groovy SDK并添加gradle和javassist API的依赖。

       然后进入plugin目录下的main目录创建groovy目录,因为Gradle插件是基于Groovy语法的,因此我们开发的插件相当于一个Groovy项目,所以需要在main目录下创建groovy目录。这时plugin目录如下所示:


       由于groovy是基于JVM的DSL语言,在groovy中能完美调用Java API。因此创建groovy文件和创建Java文件是类似的,在groovy包下新建包名:com.llew.bytecode.fix.plugin,然后在plugin包下新建BytecodeFixPlugin.groovy文件,因为要创建Gradle插件就必须要实现Gradle包中的org.gradle.api.Plugin接口,所以BytecodeFixPlugin.groovy内容如下所示:

package com.llew.bytecode.fix.plugin;import org.gradle.api.Plugin;import org.gradle.api.Project;public class BytecodeFixPlugin implements Plugin<Project> {    @Override    void apply(Project project) {        println "this is a gradle plugin, (*^__^*)……"    }}
       插件定义好之后我们要告诉Gradle哪一个类是我们定义的插件类,因此需要在main目录下创建resources目录,然后在resources目录下创建META-INF目录,接着在META-INF目录下创建gradle-plugins目录,gradle-plugins目录是自定义Gradle插件的必备目录,然后在该目录下创建一个properties文件,文件名为com.llew.bytecode.fix.properties,这个文件名是有技巧的,当起完名字后如果要使用插件,就可以这样:apply plugin 'com.llew.bytecode.fix';起完名字后还不可以使用该插件,还要告诉Gradle自定义插件的具体实现类是哪一个,在com.llew.bytecode.fix.properties文件中添加如下内容:
implementation-class=com.llew.bytecode.fix.plugin.BytecodeFixPlugin
       这样就告诉了Gradle插件的实现类是com.llew.bytecode.fix.plugin.BytecodeFixPlugin,定义完了以上配置后,还需要把插件打包到Maven仓库后才可以使用,为了简单起见,我们直接把插件打包到本地Maven仓库,在plugin的build.gradle中完整配置如下:
apply plugin: 'groovy'apply plugin: 'maven'repositories {    jcenter()    mavenCentral()}dependencies {    compile gradleApi()    compile localGroovy()    compile 'com.android.tools.build:gradle:2.3.3'}group   = 'com.llew.bytecode.fix'version = '1.0.0'uploadArchives {    repositories {        mavenDeployer {            repository(url: uri("../repository"))        }    }}
       配置好plugin的build.gradle后就是进行打包了,这时候点击Android Studio的gradle工具在plugin下有一个uploadArchives Task,这时候点击运行该task,就会在plugin的同级目录下生成一个repository文件夹,该文件夹就是plugin的仓库,如下图所示:


       plugin打包到本地仓库后就可以在主项目的build.gradle中使用我们自定义的插件了,在根目录的build.gradle文件中添加Maven的本地依赖,代码如下所示:

buildscript {        repositories {        jcenter()        maven {// 添加Maven的本地依赖            url uri('./repository')        }    }    dependencies {        classpath 'com.android.tools.build:gradle:2.3.3'        // 添加如下配置,格式为:groupName:moduleName:version        classpath 'com.llew.bytecode.fix:plugin:1.0.0'    }}
       在根目录的build.gradle配置完成之后,就可以只用我们自定义的插件了,在主项目的build.gradle文件末尾中添加如下依赖:
apply plugin: 'com.llew.bytecode.fix'
       主项目的build.gradle配置完成后重新clean下代码,重新clean下代码,重新clean下代码,重要的事情说三遍,然后点击Android Studio的make project按钮,Gradle输出窗口就会打印如下日志:


       经过一系列的操作,我们自定义的插件终于可以使用了,顿时感觉好Happy呀,哈哈,接下来就可以在我们自定义的插件BytecodeFixPlugin中注入Transform了,创建BytecodeFixTransform并继承Transform类,然后重写相关方法,代码如下:

package com.llew.bytecode.fix.transformpublic class BytecodeFixTransform extends Transform {    private static final String DEFAULT_NAME = "BytecodeFixTransform"    BytecodeFixTransform() {    }    @Override    public String getName() {        return DEFAULT_NAME    }    @Override    public Set<QualifiedContent.ContentType> getInputTypes() {        return TransformManager.CONTENT_CLASS    }    @Override    public Set<? super QualifiedContent.Scope> getScopes() {        return TransformManager.SCOPE_FULL_PROJECT    }    @Override    public boolean isIncremental() {        return false    }    @Override    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {    }}
       BytecodeFixTransform重写了部分方法,相关方法解释如下:
  • getName()
           该方法表示当前Transform在task列表中的名字,返回值最终经过一系列的拼接,具体拼接实现在TransformManager的getTaskNamePrefix()方法中,拼接格式:transform${InputType1}And${InputType2}And${InputTypeN}And${name}For${flavor}${BuildType}
  • getInputTypes()
           该方法表示指定输入类型,这里我们指定CONTENT_CLASS类型
  • getScopes()
           该方法表示当前Transform的作用范围,这里我们指定SCOPE_FULL_PROJECT
  • isIncremental()
           该方法表示当前Transform是否支持增量编译
  • transform()
           该方法是重点,它接收上一个Transform的输出,并把处理后的结果作为下一个Transform的输入,如下所示:

       创建完BytecodeFixTransform后需要把它添加到Android的编译流程中,修改BytecodeFixPlugin的apply()方法,如下所示:

@Overridevoid apply(Project project) {    def android = project.extensions.findByType(AppExtension.class)    def versionName = android.defaultConfig.versionName    android.registerTransform(new BytecodeFixTransform(project, versionName))}
       现在BytecodeFixTransform已经添加到打包流程中了,如果运行项目会失败的,因为在整个运行流程中上一个Transform的输入进入了BytecodeFixTransform的transfrom()方法中,但该方法没有输出,所以要在transform()方法中做资源的输出,代码如下所示:
@Overridepublic void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {    if (null == transformInvocation) {        throw new IllegalArgumentException("transformInvocation is null !!!")    }    Collection<TransformInput> inputs = transformInvocation.inputs    if (null == inputs) {        throw new IllegalArgumentException("TransformInput is null !!!")    }    TransformOutputProvider outputProvider = transformInvocation.outputProvider;    if (null == outputProvider) {        throw new IllegalArgumentException("TransformInput is null !!!")    }    for (TransformInput input : inputs) {        if (null == input) continue;        // 把项目中的class文件拷贝到指定目录        for (DirectoryInput directoryInput : input.directoryInputs) {            if (directoryInput) {                if (null != directoryInput.file && directoryInput.file.exists()) {                    // directoryInput.file就是我们项目中编译过的class文件                    // 获取指定目录,固定写法                    File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);                    FileUtils.copyDirectory(directoryInput.file, dest);                }            }        }        // 把依赖的第三方Jar文件拷贝到指定目录        for (JarInput jarInput : input.jarInputs) {            if (jarInput) {                if (jarInput.file && jarInput.file.exists()) {                    String jarName = jarInput.name;                    String md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath);                    if (jarName.endsWith(".jar")) {                        jarName = jarName.substring(0, jarName.length() - 4);                    }                    // jarInput.file文件就是依赖的一些第三方Jar包,修复第三方的Jar包就是在这里进行处理的                    // 获取指定目录,固定写法                    File dest = outputProvider.getContentLocation(DigestUtils.md5Hex(jarName + md5Name), jarInput.contentTypes, jarInput.scopes, Format.JAR);                    if (dest) {                        if (dest.parentFile) {                            if (!dest.parentFile.exists()) {                                dest.parentFile.mkdirs();                            }                        }                        if (!dest.exists()) {                            dest.createNewFile();                        }                        FileUtils.copyFile(jarInput.file, dest);                    }                }            }        }    }}

       在transform()方法添加输出后,项目就可以运行起来了。目前transform()方法仅仅是把输入文件集合拷贝到目的文件夹中,而这些输入文件集合和输出文件路径是通过TransformInput和TransformOutputProvider提供的。我们修改第三方Jar包的时机就是在这里进行的,先把输入进来的第三方Jar包文件做修改,修改之后就直接把修改过的Jar包拷贝进目标文件夹中就行了,实现思路就是这样,很简单,有木有(*^__^*) ……

       到这里我们自定义Gradle Plugin已经实现了,接下来就是实现对Jar包文件内容的修改,由于篇幅原因,我把对Jar包文件的内容修复讲解放在了下篇文章Android 源码系列之<十八>自定义Gradle Plugin,优雅的解决第三方Jar包中的bug<中>中,点击链接可跳转阅读。



       插件已开源GitHub:https://github.com/llew2011/BytecodeFixer;下篇文章有讲解详细用法



阅读全文
0 0
原创粉丝点击