Android 源碼系列之自定義Gradle Plugin,優雅的解決第三方Jar包中的bug

       轉載請註明出處: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.extension

public 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.injector

import com.llew.bytecode.fix.extension.BytecodeFixExtension
import com.llew.bytecode.fix.task.BuildJarTask
import com.llew.bytecode.fix.utils.FileUtils
import com.llew.bytecode.fix.utils.Logger
import com.llew.bytecode.fix.utils.TextUtil
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project

import java.util.jar.JarFile
import java.util.zip.ZipFile

public 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)






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章