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

来源:互联网 发布:软件就业前景 编辑:程序博客网 时间:2024/05/16 15:34

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

       在上篇文章Android 源码系列之<十七>自定义Gradle Plugin,优雅的解决第三方Jar包中的bug<上>中由于篇幅原因我们主要讲解了如何创建自定义Gradle Plugin以及修复第三方Jar包中的bug的思路,如果你还没看过上篇文章,强烈建议阅读一下。这篇文章就带领小伙伴们借助Javassist开源库实现对class文件的修改。

        上篇文章中我们讲到了修改第三方Jar包的时机是在BytecodeFixTransform的transform()方法中,也就是在待修改Jar包在被拷贝目标文件夹之前先做修改,修改完成之后我们直接把修改过的Jar包拷贝进目标文件夹而不是原来的Jar包。既然要修改Jar包里的class文件,我们就要知道是哪一个class需要修复,然后还要是class里边的哪一个方法需要修复,还要清楚要修复的内容是什么等等,因此我定义一个BytecodeFixExtension类来表示修复配置,如下所示:

package com.llew.bytecode.fix.extensionpublic class BytecodeFixExtension {    /**     * 字节码修复插件是否可用,默认可用     */    boolean enable = true    /**     * 是否开启日志功能,默认开启     */    boolean logEnable = true    /**     * 是否保留修复过的jar文件,默认保留     */    boolean keepFixedJarFile = true    /**     * 时候保留修复过的class文件,默认保留     */    boolean keepFixedClassFile = true    /**     * 构建字节码所依赖的第三方包绝对路径,默认包含了Android.jar文件     */    ArrayList<String> dependencies = new ArrayList<String>()    /**     * 配置文件集合,配置格式:className##methodName(param1,param2...paramN)##injectValue##injectLine     */    ArrayList<String> fixConfig = new ArrayList<>();    // 省略了setters and getters 方法}

       在BytecodeFixExtension中需要注意dependencies和fixConfig的配置。dependencies表示在利用Javassist修复class文件时所依赖的Jar包,例如修复上篇文章中提到的getMobileAPInfo()方法就需要引入ContextCompat类,因此需要添加ContextCompat所在Jar包的绝对路径。fixConfig表示修复信息集合,它的格式是固定的,必须以##做分隔符,格式如下:className##methodName(param1,param2...paramN)##injectValue##injectLine,具体字段说明如下所示:

  • className:表示全类名
            例如:com.tencent.av.sdk.NetworkHelp
  • methodName(param1,param2...paramN):表示方法名及相关参数,参数只写类型且必须以逗号(,)分隔,非基础数据类型要写全路径
            例如:getAPInfo(android.content.Context)
            例如:getAPInfo(android.content.Context, int)
  • injectValue:表示待插入代码块,注意代码块要有分号(;),其中$0表示this;$1表示第一个参数;$2表示第二个参数;以此类推
            例如:$1 = null;System.out.println("I have hooked this method by BytecodeFixer Plugin !!!");
                       $1 = null;就是表示把第一个参数置空;接着是打印一句日志
           【注意:】如果injectValue为{}表示给原有方法添加try-catch操作
  • injectLine:表示插在方法中的哪一行,该参数可选,如果省略该参数则默认把injectValue插在方法的最开始处
            injectLine > 0 插入具体行数
            injectLine = 0 插入方法最开始处
            injectLine < 0 替换方法体
       上边讲解了配置文件的规则,接下来就是实现具体的Jar包的修改了,创建BytecodeFixInjector类,该类的职责就是进行Jar包文件的修复,然后把修复后的Jar包返回给调用者。代码如下:
package com.llew.bytecode.fix.injectorimport com.llew.bytecode.fix.extension.BytecodeFixExtensionimport com.llew.bytecode.fix.task.BuildJarTaskimport com.llew.bytecode.fix.utils.FileUtilsimport com.llew.bytecode.fix.utils.Loggerimport com.llew.bytecode.fix.utils.TextUtilimport javassist.ClassPoolimport javassist.CtClassimport javassist.CtMethodimport org.gradle.api.Projectimport java.util.jar.JarFileimport java.util.zip.ZipFilepublic class BytecodeFixInjector {    private static final String INJECTOR  = "injector"    private static final String JAVA      = ".java"    private static final String CLASS     = ".class"    private static final String JAR       = ".jar"    private static ClassPool sClassPool    private static BytecodeFixInjector sInjector    private Project mProject    private String mVersionName    private BytecodeFixExtension mExtension    private BytecodeFixInjector(Project project, String versionName, BytecodeFixExtension extension) {        this.mProject = project        this.mVersionName = versionName        this.mExtension = extension        appendClassPath()    }    public static void init(Project project, String versionName, BytecodeFixExtension extension) {        sClassPool = ClassPool.default        sInjector = new BytecodeFixInjector(project, versionName, extension)    }    public static BytecodeFixInjector getInjector() {        if (null == sInjector) {            throw new IllegalAccessException("init() hasn't bean called !!!")        }        return sInjector    }    public synchronized File inject(File jar) {        File destFile = null        if (null == mExtension) {            Logger.e("can't find bytecodeFixConfig in your app build.gradle !!!")            return destFile        }        if (null == jar) {            Logger.e("jar File is null before injecting !!!")            return destFile        }        if (!jar.exists()) {            Logger.e(jar.name + " not exits !!!")            return destFile        }        try {            ZipFile zipFile = new ZipFile(jar)            zipFile.close()            zipFile = null        } catch (Exception e) {            Logger.e(jar.name + " not a valid jar file !!!")            return destFile        }        def jarName = jar.name.substring(0, jar.name.length() - JAR.length())        def baseDir = new StringBuilder().append(mProject.projectDir.absolutePath)                .append(File.separator).append(INJECTOR)                .append(File.separator).append(mVersionName)                .append(File.separator).append(jarName).toString()        File rootFile = new File(baseDir)        FileUtils.clearFile(rootFile)        rootFile.mkdirs()        File unzipDir = new File(rootFile, "classes")        File jarDir   = new File(rootFile, "jar")        JarFile jarFile = new JarFile(jar)        mExtension.fixConfig.each { config ->            if (!TextUtil.isEmpty(config.trim())) {                // com.tencent.av.sdk.NetworkHelp##getAPInfo(android.content.Context)##if(Boolean.TRUE.booleanValue()){$1 = null;System.out.println("i have hooked this method !!!");}##0                def configs = config.trim().split("##")                if (null != configs && configs.length > 0) {                    if (configs.length < 3) {                        throw new IllegalArgumentException("参数配置有问题")                    }                    def className   = configs[0].trim()                    def methodName  = configs[1].trim()                    def injectValue = configs[2].trim()                    def injectLine  = 0                    if (4 == configs.length) {                        try {                            injectLine  = Integer.parseInt(configs[3])                        } catch (Exception e) {                            throw new IllegalArgumentException("行数配置有问题")                        }                    }                    if (TextUtil.isEmpty(className)) {                        Logger.e("className invalid !!!")                        return                    }                    if (TextUtil.isEmpty(methodName)) {                        Logger.e("methodName invalid !!!")                        return                    }                    if (TextUtil.isEmpty(injectValue)) {                        Logger.e("inject value invalid !!!")                        return                    }                    def methodParams = new ArrayList<String>()                    if (methodName.contains("(") && methodName.contains(")")) {                        def tempMethodName = methodName                        methodName = tempMethodName.substring(0, tempMethodName.indexOf("(")).trim()                        def params = tempMethodName.substring(tempMethodName.indexOf("(") + 1, tempMethodName.indexOf(")")).trim()                        if (!TextUtil.isEmpty(params)) {                            if (params.contains(",")) {                                params = params.split(",")                                if (null != params && params.length > 0) {                                    params.each { p ->                                        methodParams.add(p.trim())                                    }                                }                            } else {                                methodParams.add(params)                            }                        }                    }                    if (className.endsWith(JAVA)) {                        className = className.substring(0, className.length() - JAVA.length()) + CLASS                    }                    if (!className.endsWith(CLASS)) {                        className += CLASS                    }                    def contain = FileUtils.containsClass(jarFile, className)                    if (contain) {                        // 1、判断是否进行过解压缩操作                        if (!FileUtils.hasFiles(unzipDir)) {                            FileUtils.unzipJarFile(jarFile, unzipDir)                        }                        // 2、开始注入文件,需要注意的是,appendClassPath后边跟的根目录,没有后缀,className后完整类路径,也没有后缀                        sClassPool.appendClassPath(unzipDir.absolutePath)                        // 3、开始注入,去除.class后缀                        if (className.endsWith(CLASS)) {                            className = className.substring(0, className.length() - CLASS.length())                        }                        CtClass ctClass = sClassPool.getCtClass(className)                        if (!ctClass.isInterface()) {                            CtMethod ctMethod                            if (methodParams.isEmpty()) {                                ctMethod = ctClass.getDeclaredMethod(methodName)                            } else {                                CtClass[] params = new CtClass[methodParams.size()]                                for (int i = 0; i < methodParams.size(); i++) {                                    String param = methodParams.get(i)                                    params[i] = sClassPool.getCtClass(param)                                }                                ctMethod = ctClass.getDeclaredMethod(methodName, params)                            }                            if (injectLine > 0) {                                ctMethod.insertAt(injectLine, injectValue)                            } else if (injectLine == 0) {                                ctMethod.insertBefore(injectValue)                            } else {                                if (!injectValue.startsWith("{")) {                                    injectValue = "{" + injectValue                                }                                if (!injectValue.endsWith("}")) {                                    injectValue = injectValue + "}"                                }                                ctMethod.setBody(injectValue)                            }                            ctClass.writeFile(unzipDir.absolutePath)                            ctClass.detach()                        } else {                            Logger.e(className + " is interface and can't inject code !!!")                        }                    }                }            }        }        // 4、循环体结束,判断classes文件夹下是否有文件        if (FileUtils.hasFiles(unzipDir)) {            BuildJarTask buildJarTask = mProject.tasks.create("BytecodeFixBuildJarTask", BuildJarTask)            buildJarTask.baseName = jarName            buildJarTask.from(unzipDir.absolutePath)            buildJarTask.doLast {                // 进行文件的拷贝                def stringBuilder = new StringBuilder().append(mProject.projectDir.absolutePath)                        .append(File.separator).append("build")                        .append(File.separator).append("libs")                        .append(File.separator).append(jar.name).toString()                if (!jarDir.exists()) {                    jarDir.mkdirs()                }                destFile = new File(jarDir, jar.name)                FileUtils.clearFile(destFile)                destFile.createNewFile()                File srcFile = new File(stringBuilder)                com.android.utils.FileUtils.copyFile(srcFile, destFile)                FileUtils.clearFile(srcFile)                if (null != mExtension && !mExtension.keepFixedClassFile) {                    FileUtils.clearFile(unzipDir)                }            }            // FIXME buildJarTask sometimes has bug            // buildJarTask.execute()            destFile = new File(jarDir, jar.name)            FileUtils.clearFile(destFile)            FileUtils.zipJarFile(unzipDir, destFile)            if (null != mExtension && !mExtension.keepFixedClassFile) {                FileUtils.clearFile(unzipDir)            }        } else {            FileUtils.clearFile(rootFile)        }        jarFile.close()        return destFile    }    private void appendClassPath() {        if (null == mProject) return        def androidJar = new StringBuffer().append(mProject.android.getSdkDirectory())                .append(File.separator).append("platforms")                .append(File.separator).append(mProject.android.compileSdkVersion)                .append(File.separator).append("android.jar").toString()        File file = new File(androidJar);        if (!file.exists()) {            androidJar = new StringBuffer().append(mProject.rootDir.absolutePath)                    .append(File.separator).append("local.properties").toString()            Properties properties = new Properties()            properties.load(new File(androidJar).newDataInputStream())            def sdkDir = properties.getProperty("sdk.dir")            androidJar = new StringBuffer().append(sdkDir)                    .append(File.separator).append("platforms")                    .append(File.separator).append(mProject.android.compileSdkVersion)                    .append(File.separator).append("android.jar").toString()            file = new File(androidJar)        }        if (file.exists()) {            sClassPool.appendClassPath(androidJar);        } else {            Logger.e("couldn't find android.jar file !!!")        }        if (null != mExtension && null != mExtension.dependencies) {            mExtension.dependencies.each { dependence ->                sClassPool.appendClassPath(dependence)            }        }    }}
       以上就是BytecodeFixInjector的全部代码了,在BytecodeFixInjector中我们定义了静态变量sClassPool和sInjector,并在创建该对象的时候初始化了相关的非静态属性值,并调用appendClassPath()方法把默认的android.jar文件以及BytecodeFixExtension中配置的依赖包添加到sClassPool中去,这样可以防止在进行class文件操作由于找不到引用而发生错误的问题。BytecodeFixInjector的核心代码是inject()方法,该方法接收原始的Jar包,如果该原始Jar包需要做修复,则进行修复并把修复后的Jar包返回给调用者;如果该原始Jar包不需要做修改则返回一个null值,null就表示告诉调用者原始Jar包不需要做修改。
       BytecodeFixInjector定义完之后在BytecodeFixTransform的transfrom()方法做接入,代码如下:
public class BytecodeFixTransform extends Transform {    private static final String DEFAULT_NAME = "BytecodeFixTransform"    private BytecodeFixExtension mExtension;    BytecodeFixTransform(Project project, String versionName, BytecodeFixExtension extension) {        this.mExtension = extension        Logger.enable = extension.logEnable        BytecodeFixInjector.init(project, versionName, mExtension)    }    @Override    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {                // 省略相关代码...        for (TransformInput input : inputs) {            if (null == input) continue;            for (DirectoryInput directoryInput : input.directoryInputs) {                if (directoryInput) {                    if (null != directoryInput.file && directoryInput.file.exists()) {                        // ClassInjector.injector.inject(directoryInput.file.absolutePath, mPackageName.replaceAll("\\.", File.separator));                        File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);                        FileUtils.copyDirectory(directoryInput.file, dest);                    }                }            }            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);                        }                        // 在这里jar文件进行动态修复,这里是重点                        File injectedJarFile = null                        if (null != mExtension && mExtension.enable) {                            injectedJarFile = BytecodeFixInjector.injector.inject(jarInput.file)                        }                        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();                            }                            // 校验injectedJarFile是否做过修改,如果做过修改则直接把injectedJarFile拷贝到目的文件夹中                            // 然后根据mExtension的配置是否保留修复过的injectedJarFile文件                            if (null != injectedJarFile && injectedJarFile.exists()) {                                FileUtils.copyFile(injectedJarFile, dest)                                Logger.e(jarInput.file.name + " has successful hooked !!!")                                if (null != mExtension && !mExtension.keepFixedJarFile) {                                    injectedJarFile.delete()                                }                            } else {                                FileUtils.copyFile(jarInput.file, dest)                            }                        }                    }                }            }        }    }}
       到这里修复第三方Jar包的核心逻辑已经完成了,由于篇幅原因,具体细节就不贴出了,我把该插件起名为BytecodeFixer并开源到了GitHub上然后又上传到了Jcenter上,GitHub地址:https://github.com/llew2011/BytecodeFixer 欢感兴趣的请自行阅读源码,另外欢迎小伙伴们fork and star(*^__^*) ……
       该插件使用如下:
       1、在工程根目录的build.gradle文件中添加如下配置:
dependencies {    classpath 'com.android.tools.build:gradle:2.3.3'    classpath 'com.jakewharton:butterknife-gradle-plugin:8.6.0'    // 添加如下配置    classpath 'com.llew.bytecode.fix.gradle:BytecodeFixer:1.0.2'}
       2、在主工程的build.gradle文件末尾添加如下配置:
apply plugin: 'com.llew.bytecode.fix'bytecodeFixConfig {    enable true    logEnable = true    keepFixedJarFile = true    keepFixedClassFile = true    dependencies = []    fixConfig = [            'com.tencent.av.sdk.NetworkHelp##getAPInfo(android.content.Context)##$1 = null;System.out.println("I have hooked this method by BytecodeFixer !!!");##0',            'com.tencent.av.sdk.NetworkHelp##getMobileAPInfo(android.content.Context,int)##$1 = null;System.out.println("I have hooked this method by BytecodeFixer !!!");return new com.tencent.av.sdk.NetworkHelp.APInfo();##-1',    ]}
       配置完成之后,运行项目,这时候会在主工程的目录下生成修复过的class文件,如下所示:

       好了,运行项目后,通过bytecodeFixConfig的配置,目标Jar文件就会被自动修复,是不是很方便,顿时这个世界是那么的美好,从此以后不管用户授予没有授予相关权限,都可以让用户愉快的玩耍我们APP了,并且做到了一切第三方的Jar文件都在我们的掌控中,看哪一个方法不顺眼就可以使用BytecodeFixer插件做修复,是不是很爽?因此在Java的世界里,如果你精通反射,代理,再加上Javassist利器,你可以做很多事情……
       到这里大致介绍完了BytecodeFixer插件,感谢小伙伴们的观看,我将在下篇文章Android 源码系列之<十九>自定义Gradle Plugin,优雅的解决第三方Jar包中的bug<下>给小伙伴们讲解一下Javassist的具体语法,并讲解一下其源码实现,敬请期待(*^__^*) ……


        BytecodeFixer地址:https://github.com/llew2011/BytecodeFixer 
       (欢迎fort and star)






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