美文网首页Android Dev
Android Shadow 插件窥探(1)基础知识简介

Android Shadow 插件窥探(1)基础知识简介

作者: 鸡蛋绝缘体 | 来源:发表于2020-08-11 14:58 被阅读0次
    • 简介
    • 先学会接入
    • 了解字节码
    • 了解 Javaassist
      • 引入依赖
      • 基础 Demo
    • javapoet
      • 依赖引入
      • 样例
      • 生成样例的代码
      • 其他相关,摘自 Github, 略过
    • Android 中的 ClassLoader
      • BootClassLoader
      • PathClassLoader
      • DexClassLoader
    • Transfrom API 简介
      • 简单应用
      • 在gradle简单注册
    • Gradle 聊一聊
      • buildConfigField
      • resValue
      • 统一版本
      • mavenLocal()
      • 构建类型
      • product flavor
      • 过滤变种 variant
      • jks 密码存储
      • Android 构建

    [TOC]

    简介

    Tencent Shadow—零反射全动态Android插件框架正式开源

    真的只是简单介绍。

    官文:Shadow的全动态设计原理解析

    官文:Shadow对插件包管理的设计

    先学会接入

    接入指南

    了解字节码

    字节码

    了解 Javaassist

    Javaassist 就是一个用来 处理 Java 字节码的类库。

    Getting Started with Javassist

    引入依赖

     implementation 'org.javassist:javassist:3.22.0-GA'
    

    基础 Demo

    package com.music.lib
    
    import javassist.*
    import javassist.bytecode.Descriptor
    
    /**
     * @author Afra55
     * @date 2020/8/5
     * A smile is the best business card.
     * 没有成绩,连呼吸都是错的。
     */
    internal object TextJava {
        @JvmStatic
        fun main(args: Array<String>) {
            println(System.getenv("PUBLISH_RELEASE"))
    
    //        createUserClass()
    
            changeCurrentClass()
        }
    
        /**
         * 基础使用方法
         */
        @JvmStatic
        fun createUserClass(){
            // 获得一个ClassPool对象,该对象使用Javassist控制字节码的修改
            val classPoll = ClassPool.getDefault()
    
            // 创建一个类
            val cc = classPoll.makeClass("com.oh.my.god.User")
    
            // 创建一个属性 private String name;
            val nameField = CtField(classPoll.get(java.lang.String::class.java.name), "name", cc)
            // 修饰符
            nameField.modifiers = Modifier.PRIVATE
            // 把属性添加到类中
            cc.addField(nameField, CtField.Initializer.constant("Afra55"))
    
            // 添加 get set 方法
            cc.addMethod(CtNewMethod.setter("setName", nameField))
            cc.addMethod(CtNewMethod.setter("getName", nameField))
    
            // 无参数构造函数
            val cons = CtConstructor(arrayOf<CtClass>(), cc)
            // 设置函数内容, name 是上面添加的属性
            cons.setBody("{name = \"无参构造\";}")
            // 把构造函数添加到类中
            cc.addConstructor(cons)
    
            // 一个参数的构造函数
            val cons1 = CtConstructor(arrayOf<CtClass>(classPoll.get(java.lang.String::class.java.name)), cc)
            // $0=this / $1,$2,$3... 代表方法参数
            cons1.setBody("{$0.name = $1;}")
            // 把构造函数添加到类中
            cc.addConstructor(cons1)
    
            // 创建一个 singASong 方法, CtMethod(返回类型,方法名,参数)
            val myMethod = CtMethod(CtClass.voidType, "singASong", arrayOf<CtClass>(), cc)
            myMethod.modifiers = Modifier.PUBLIC
            myMethod.setBody("{System.out.println(name);}")
            cc.addMethod(myMethod)
    
            // 创建 .class 文件,可传入路径
            cc.writeFile()
    
            // toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
            // cc.toClass()
    
            // 冻结一个类,使其不可修改;
            // cc.freeze()
    
            // 删除类不必要的属性
            // cc.prune()
    
            //  解冻一个类,使其可以被修改
            // cc.defrost()
    
            // 将该class从ClassPool中删除
            // cc.detach()
    
    
        }
    
        /**
         * 对已有类的修改, 这个类得先存在
         */
        @JvmStatic
        fun changeCurrentClass() {
            val pool = ClassPool.getDefault()
            val cc = pool.get("com.music.lib.MyGirl")
    
            System.out.println(cc.name)
            System.out.println(MyGirl::class.java.name)
    
            val myMethod = cc.getDeclaredMethod("play")
            myMethod.insertBefore("System.out.println(\"insertBefore\");")
            myMethod.insertAfter("System.out.println(\"insertAfter\");")
    
            val classMap = ClassMap()
            classMap[Descriptor.toJvmName("com.music.lib.MyGirl")] = Descriptor.toJvmName("com.oh.my.girl.Wife")
    
            cc.replaceClassName(classMap)
    
            cc.toClass()
    
            cc.writeFile()
    
        }
    }
    

    javapoet

    javapoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。
    https://github.com/square/javapoet

    依赖引入

    implementation 'com.squareup:javapoet:1.11.1'

    样例

    package com.example.helloworld;
    
    import static com.music.lib.MyGirl.*;
    
    import java.lang.Exception;
    import java.lang.RuntimeException;
    import java.lang.String;
    import java.lang.System;
    import java.util.Date;
    
    /**
     * Author: "Afra55"
     * Date: "2020.8.6"
     * Desc: "你说一,我说一,大家都来说一个"
     * Version: 1.0
     */
    public final class HelloWorld {
      private final String greeting;
    
      private final String version = "Afra55-" + 1.0;
    
      public HelloWorld() {
        this.greeting = "90909";
      }
    
      public HelloWorld(String greeting) {
        this.greeting = greeting;
      }
    
      public static void main(String[] args) {
        System.out.println("Hello, JavaPoet!");
        int total = 0;
        for(int i = 0; i < 10; i++) {
          total += I;
        }
      }
    
      public int add(int number, int sub) {
        for(int i = 0; i < 10; i++) {
          number += i + sub;
        }
        if (number > 10) {
          number *= 20;
        } if (number > 5) {
          number -= 10;
        } else {
          System.out.println("Ok, time still moving forward \"$ @@");
          System.out.println("12345");
        }
        return number;
      }
    
      void catchMethod() {
        try {
          throw new Exception("Failed");
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
      }
    
      Date today() {
        return new Date();
      }
    
      Date tomorrow() {
        return new Date();
      }
    
      void staticTestMethod() {
        System.out.println(A);
      }
    
      char hexDigit(int i) {
        return (char) (i < 10 ? i + '0' : i - 10 + 'a');
      }
    
      String byteToHex(int b) {
        char[] result = new char[2];
        result[0] = hexDigit((b >>> 4) & 0xf);
        result[1] = hexDigit(b & 0xf);
        return new String(result);
      }
    }
    
    

    生成样例的代码

     /**
         * 使用工具生成类
         */
        @JvmStatic
        fun javapoetTestClass() {
            // 创建一个方法 main
            val main = MethodSpec.methodBuilder("main")
                // 创建修饰符 public static
                .addModifiers(
                    javax.lang.model.element.Modifier.PUBLIC,
                    javax.lang.model.element.Modifier.STATIC
                )
                // 返回类型
                .returns(Void.TYPE)
                // 参数
                .addParameter(Array<String>::class.java, "args")
                // 添加内容
                .addStatement("\$T.out.println(\$S)", System::class.java, "Hello, JavaPoet!")
                .addStatement("int total = 0")
                // 代码块条件语句
                .beginControlFlow("for(int i = 0; i < 10; i++)")
                // 代码块内容
                .addStatement("total += I")
                // 代码块结束
                .endControlFlow()
                .build()
    
            // 创建方法 add, 注意下面的 $S 代表字符串会被引号扩起来并被转义, $T 代表类型, $L 代表参数不会被转义为字符串
            val addMethod = MethodSpec.methodBuilder("add")
                .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                .returns(Integer.TYPE)
                .addParameter(Integer.TYPE, "number")
                .addParameter(Integer.TYPE, "sub")
                .beginControlFlow("for(int i = \$L; i < \$L; i++)", 0, 10)
                .addStatement("number += i + sub")
                .endControlFlow()
                .beginControlFlow("if (number > 10)")
                .addStatement("number *= \$L", 20)
                .nextControlFlow("if (number > 5)")
                .addStatement("number -= 10")
                .nextControlFlow("else")
                .addStatement(
                    "\$T.out.println(\$S)",
                    System::class.java,
                    "Ok, time still moving forward \"\$ @@"
                )
                .addStatement(
                    "\$T.out.println(\$S)",
                    System::class.java,
                    12345
                )
                .endControlFlow()
                .addStatement("return number")
                .build()
    
            val catchMethod = MethodSpec.methodBuilder("catchMethod")
                .beginControlFlow("try")
                .addStatement("throw new Exception(\$S)", "Failed")
                .nextControlFlow("catch (\$T e)", Exception::class.java)
                .addStatement("throw new \$T(e)", RuntimeException::class.java)
                .endControlFlow()
                .build()
    
            // 返回 Date 对象的方法
            val today: MethodSpec = MethodSpec.methodBuilder("today")
                .returns(Date::class.java)
                .addStatement("return new \$T()", Date::class.java)
                .build()
    
            val hoverboard: ClassName = ClassName.get("java.util", "Date")
            val tomorrow: MethodSpec = MethodSpec.methodBuilder("tomorrow")
                .returns(hoverboard)
                .addStatement("return new \$T()", hoverboard)
                .build()
    
            // 生成一个 hexDigit 方法
            val hexDigit = MethodSpec.methodBuilder("hexDigit")
                .addParameter(Int::class.javaPrimitiveType, "I")
                .returns(Char::class.javaPrimitiveType)
                .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
                .build()
    
            // 通过 $N 来引用 hexDigit() 方法
            val byteToHex = MethodSpec.methodBuilder("byteToHex")
                .addParameter(Int::class.javaPrimitiveType, "b")
                .returns(String::class.java)
                .addStatement("char[] result = new char[2]")
                .addStatement("result[0] = \$N((b >>> 4) & 0xf)", hexDigit)
                .addStatement("result[1] = \$N(b & 0xf)", hexDigit)
                .addStatement("return new String(result)")
                .build()
    
            // 静态变量, 通过 JavaFile 添加静态引用,详情往下看
            val girl = ClassName.get("com.music.lib", "MyGirl")
            val staticTestMethod = MethodSpec.methodBuilder("staticTestMethod")
                .addStatement(
                    "\$T.out.println(\$T.A)",
                    System::class.java,
                    girl
                )
                .build()
    
            // 创建一个空参构造函数, $N 引用已声明的属性
            val constructor: MethodSpec = MethodSpec.constructorBuilder()
                .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                .addStatement("this.\$N = \$S", "greeting", 90909)
                .build()
    
            // 创建一个带参构造函数
            val constructor1: MethodSpec = MethodSpec.constructorBuilder()
                .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                .addParameter(String::class.java, "greeting")
                .addStatement("this.\$N = \$N", "greeting", "greeting")
                .build()
    
            // javadoc
            val map = linkedMapOf<String, Any>()
            map["author"] = "Afra55"
            map["date"] = "2020.8.6"
            map["desc"] = "你说一,我说一,大家都来说一个"
            map["version"] = 1.0
    
    
            // 创建类HelloWorld
            val helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(
                    javax.lang.model.element.Modifier.PUBLIC,
                    javax.lang.model.element.Modifier.FINAL
                )
                // 添加 javadoc
                .addJavadoc(
                    CodeBlock.builder().addNamed(
                        "Author: \$author:S\nDate: \$date:S\nDesc: \$desc:S\nVersion: \$version:L",
                        map
                    )
                        .build()
                )
                .addJavadoc("\n")
    
                // 添加一个属性 greeting
                .addField(
                    String::class.java,
                    "greeting",
                    javax.lang.model.element.Modifier.PRIVATE,
                    javax.lang.model.element.Modifier.FINAL
                )
    
                // 添加一个初始化值的属性 version
                .addField(
                    FieldSpec.builder(String::class.java, "version")
                        .addModifiers(
                            javax.lang.model.element.Modifier.PRIVATE,
                            javax.lang.model.element.Modifier.FINAL
                        )
                        // 初始化值
                        .initializer("\$S + \$L", "Afra55-", 1.0)
                        .build()
                )
    
                // 添加方法
                .addMethod(constructor)
                .addMethod(constructor1)
                .addMethod(main)
                .addMethod(addMethod)
                .addMethod(catchMethod)
                .addMethod(today)
                .addMethod(tomorrow)
                .addMethod(staticTestMethod)
                .addMethod(hexDigit)
                .addMethod(byteToHex)
                .build()
    
            val javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
                // 添加静态引用
                .addStaticImport(girl, "*")
                .build()
    
            // 输出到控制台
            javaFile.writeTo(System.out)
    
            // 输出到文件
            javaFile.writeTo(File("/Users/victor/Program/Android/Demo/testJavaLib/lib/src/main/java/"))
        }
    

    其他相关,摘自 Github, 略过

    1. Interface:
    TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
        .addModifiers(Modifier.PUBLIC)
        .addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
            .initializer("$S", "change")
            .build())
        .addMethod(MethodSpec.methodBuilder("beep")
            .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
            .build())
        .build();
    

    output:

    public interface HelloWorld {
      String ONLY_THING_THAT_IS_CONSTANT = "change";
    
      void beep();
    }
    
    1. Enums
    TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
        .addModifiers(Modifier.PUBLIC)
        .addEnumConstant("ROCK")
        .addEnumConstant("SCISSORS")
        .addEnumConstant("PAPER")
        .build();
    

    output:

    public enum Roshambo {
      ROCK,
    
      SCISSORS,
    
      PAPER
    }
    

    带参枚举:

    TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
        .addModifiers(Modifier.PUBLIC)
        .addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("$S", "fist")
            .addMethod(MethodSpec.methodBuilder("toString")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addStatement("return $S", "avalanche!")
                .returns(String.class)
                .build())
            .build())
        .addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace")
            .build())
        .addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat")
            .build())
        .addField(String.class, "handsign", Modifier.PRIVATE, Modifier.FINAL)
        .addMethod(MethodSpec.constructorBuilder()
            .addParameter(String.class, "handsign")
            .addStatement("this.$N = $N", "handsign", "handsign")
            .build())
        .build();
    

    output:

    public enum Roshambo {
      ROCK("fist") {
        @Override
        public String toString() {
          return "avalanche!";
        }
      },
    
      SCISSORS("peace"),
    
      PAPER("flat");
    
      private final String handsign;
    
      Roshambo(String handsign) {
        this.handsign = handsign;
      }
    }
    
    1. Anonymous Inner Classes
    TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
        .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
        .addMethod(MethodSpec.methodBuilder("compare")
            .addAnnotation(Override.class)
            .addModifiers(Modifier.PUBLIC)
            .addParameter(String.class, "a")
            .addParameter(String.class, "b")
            .returns(int.class)
            .addStatement("return $N.length() - $N.length()", "a", "b")
            .build())
        .build();
    
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
        .addMethod(MethodSpec.methodBuilder("sortByLength")
            .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
            .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
            .build())
        .build();
    

    output:

    void sortByLength(List<String> strings) {
      Collections.sort(strings, new Comparator<String>() {
        @Override
        public int compare(String a, String b) {
          return a.length() - b.length();
        }
      });
    }
    
    1. Annotations
    MethodSpec toString = MethodSpec.methodBuilder("toString")
        .addAnnotation(Override.class)
        .returns(String.class)
        .addModifiers(Modifier.PUBLIC)
        .addStatement("return $S", "Hoverboard")
        .build();
    

    output:

    @Override
      public String toString() {
        return "Hoverboard";
      }
    

    带参注解:

    MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .addAnnotation(AnnotationSpec.builder(HeaderList.class)
            .addMember("value", "$L", AnnotationSpec.builder(Header.class)
                .addMember("name", "$S", "Accept")
                .addMember("value", "$S", "application/json; charset=utf-8")
                .build())
            .addMember("value", "$L", AnnotationSpec.builder(Header.class)
                .addMember("name", "$S", "User-Agent")
                .addMember("value", "$S", "Square Cash")
                .build())
            .build())
        .addParameter(LogRecord.class, "logRecord")
        .returns(LogReceipt.class)
        .build();
    

    output:

    @HeaderList({
        @Header(name = "Accept", value = "application/json; charset=utf-8"),
        @Header(name = "User-Agent", value = "Square Cash")
    })
    LogReceipt recordEvent(LogRecord logRecord);
    
    1. javadoc
    MethodSpec dismiss = MethodSpec.methodBuilder("dismiss")
        .addJavadoc("Hides {@code message} from the caller's history. Other\n"
            + "participants in the conversation will continue to see the\n"
            + "message in their own history unless they also delete it.\n")
        .addJavadoc("\n")
        .addJavadoc("<p>Use {@link #delete($T)} to delete the entire\n"
            + "conversation for all participants.\n", Conversation.class)
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .addParameter(Message.class, "message")
        .build();
    

    output:

     /**
       * Hides {@code message} from the caller's history. Other
       * participants in the conversation will continue to see the
       * message in their own history unless they also delete it.
       *
       * <p>Use {@link #delete(Conversation)} to delete the entire
       * conversation for all participants.
       */
      void dismiss(Message message);
    

    Android 中的 ClassLoader

    系统类加载器分三种:BootClassLoaderPathClassLoaderDexClassLoader

    BootClassLoader

    预加载常用类。

    PathClassLoader

    只能加载已经安装的apk的dex文件(dex文件在/data/dalvik-cache中)。

    DexClassLoader

    支持加载外部 apk,jar,dex 文件。

    package dalvik.system;
    
    import java.io.File;
    
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
            super((String)null, (File)null, (String)null, (ClassLoader)null);
            throw new RuntimeException("Stub!");
        }
    }
    
    
    1. dexPath:dex相关文件的路径集合,多个文件用路径分割符分割,默认的文件分割符为 ":";
    2. optimizedDirectory:解压的dex文件储存的路径,这个路径必须是一个内部储存路径,一般情况下使用当钱应用程序的私有路径/data/data/<Package Name>/...;
    3. librarySearchPath:包含C++库的路径集合,多个路径用文件分割符分割,可以为null;
    4. parent:父加载器;

    基本使用方法:

     val loader = DexClassLoader("dex 路径", "输出路径", null, javaClass.classLoader)
    
            val cls = loader.loadClass("某个Class")
            if (cls != null) {
                val obj = cls.newInstance()
                val method =  cls.getDeclaredMethod("某个方法")
                // 执行方法
                val result = method.invoke(obj, "某些参数")
            }
    

    获取 resource 资源:

        val archiveFilePath = "插件APK路径"
       val packageManager = hostAppContext.packageManager
            packageArchiveInfo.applicationInfo.publicSourceDir = archiveFilePath
            packageArchiveInfo.applicationInfo.sourceDir = archiveFilePath
            packageArchiveInfo.applicationInfo.sharedLibraryFiles = hostAppContext.applicationInfo.sharedLibraryFiles
            try {
                return packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo)
            } catch (e: PackageManager.NameNotFoundException) {
                throw RuntimeException(e)
            }
    

    Transfrom API 简介

    TransformAPI,允许第三方插件在class文件被转为dex文件之前对class文件进行处理。每个Transform都是一个Gradle的task,多个Transform可以串联起来,上一个Transform的输出作为下一个Transform的输入。

    Transfrom

    简单应用

    package com.music.lib
    
    import com.android.build.api.transform.*
    import com.android.build.gradle.internal.pipeline.TransformManager
    import com.android.utils.FileUtils
    
    /**
     * @author Afra55
     * @date 2020/8/7
     * A smile is the best business card.
     * 没有成绩,连呼吸都是错的。
     */
    class AsmClassTransform : Transform() {
        /**
         * Returns the unique name of the transform.
         *
         *
         * This is associated with the type of work that the transform does. It does not have to be
         * unique per variant.
         */
        override fun getName(): String {
            // 指定 Transform 任务的名字,区分不同的 Transform 任务
            return this::class.simpleName!!
        }
    
        /**
         * Returns the type(s) of data that is consumed by the Transform. This may be more than
         * one type.
         *
         * **This must be of type [QualifiedContent.DefaultContentType]**
         */
        override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
            // 指定 Transform 处理文件的类型,一般 返回 CONTENT_CLASS 即Class文件
            return TransformManager.CONTENT_CLASS
        }
    
        /**
         * Returns whether the Transform can perform incremental work.
         *
         *
         * If it does, then the TransformInput may contain a list of changed/removed/added files, unless
         * something else triggers a non incremental run.
         */
        override fun isIncremental(): Boolean {
            // 是否支持增量编译
            return false
        }
    
        /**
         * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
         */
        override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
            // 表示 Transform 作用域,SCOPE_FULL_PROJECT 表示整个工程的 class 文件,包括子项目和外部依赖
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
    
        override fun transform(transformInvocation: TransformInvocation?) {
            super.transform(transformInvocation)
            // 整个类的核心, 获取输入的 class 文件,对 class 文件进行修改,最后输出修改后的 class 文件
            transformInvocation?.inputs?.forEach{input ->
                // 遍历文件夹
                input.directoryInputs.forEach {dirInput ->
                    // 修改字节码
                    // ...
                    // 获取输出路径
                    val outputLocationDir = transformInvocation.outputProvider.getContentLocation(
                        dirInput.name,
                        dirInput.contentTypes,
                        dirInput.scopes,
                        Format.DIRECTORY
                    )
                    // 把 input 文件夹复制到 output 文件夹,以便下一级Transform处理
                    FileUtils.copyDirectory(dirInput.file, outputLocationDir)
    
                }
    
    
                // 遍历 jar 包
                input.jarInputs.forEach {jarInput ->
                    // 修改字节码
                    // ...
                    // 获取输出路径
                    val outputLocationDir = transformInvocation.outputProvider.getContentLocation(
                        jarInput.name,
                        jarInput.contentTypes,
                        jarInput.scopes,
                        Format.JAR
                    )
                    // 把 input jar 包复制到 output 文件夹,以便下一级transform处理
                    FileUtils.copyDirectory(jarInput.file, outputLocationDir)
    
                }
            }
    
        }
    }
    

    在gradle简单注册

    class ApmPlugin implements Plugin<Project>{
    
        /**
         * Apply this plugin to the given target object.
         *
         * @param target The target object
         */
        @Override
        void apply(Project target) {
            val appExtension = target.extensions.findByType(AppExtension::class.java)
            appExtension?.registerTransform(AsmClassTransform())
        }
    }
    apply plugin: ApmPlugin
    

    Gradle 聊一聊

    gradle 文件位置: 


    settings 文件在初始化阶段被执行,定义了哪些模块应该构建。可以使用 includeBuild 'projects/sdk/core' 把另一个 Project 构建包含进来。

    打印所有可用任务列表包括描述./gradlew tasks

    • assemble: 为每个构建版本创建一个 apk;
    • clean:删除所有构建的内容;
    • check:运行 Lint 检查同时生成一份报告,包括所有警告,错误,详细说明,相关文档链接,并输出在 app/build/reports 目录下,名称为 lint-results.html,如果发现一个问题则停止构建, 并生成一份 lint-results-fatal.html 报告;
    • build:同时运行 assemble 和 check;
    • connectedCheck:在连接设备或模拟器上运行测试;
    • installDebug或installRelease:在连接的设备或模拟器上安装特定版本;
    • uninstall(...):卸载相关版本;

    在  gradle.properties 配置:

    org.gradle.parallel=true
    

    Gradle会基于可用的CPU内核,来选择正确的线程数量。

    buildConfigField

    在 BuildConfig 中添加字段:

       buildTypes {
            debug {
                buildConfigField("String", "API_URL", "\"http://afra55.github.io\"")
                buildConfigField("boolean", "IS_DEBUG", "true")
            }
            release {
                buildConfigField("boolean", "IS_DEBUG", "false")
                buildConfigField("String", "API_URL", "https://afra55.github.io")
            }
        }
    

    BuildConfig 会自动生成:

      // Fields from build type: debug
      public static final String API_URL = "https://afra55.github.io";
      public static final boolean IS_DEBUG = true;
    

    resValue

    配置资源值:

      resValue("string", "APP_ID", "balalalal_debug")
    

    会自动生成对应资源:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <!-- Automatically generated file. DO NOT MODIFY -->
    
        <!-- Values from build type: debug -->
        <item name="APP_ID" type="string">balalalal_debug</item>
    
    </resources>
    

    统一版本

    在跟 build.gradle 添加额外属性, 与buildscript 平级:

    ext {
        targetSdkVersion = 29
        versionCode = 1
        versionName = "1.0.0"
        constraintlayout = "1.1.3"
    }
    

    使用方法:

            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
            
            
            
            implementation "androidx.constraintlayout:constraintlayout:$rootProject.ext.constraintlayout"
    

    还可以在 ext 中动态构建属性:

    ext {
        targetSdkVersion = 29
        versionCode = 1
        versionName = "1.0.0"
        constraintlayout = "1.1.3"
    
        task printProperties() {
            println 'From ext property'
            println propertiesFile
            println project.name
    
            if (project.hasProperty('constraintlayout')){
                println constraintlayout
                constraintlayout = "1.1.2"
                println constraintlayout
            }
    
        }
    }
    

    其中 propertiesFile 是在 gradle.properties 文件:
    propertiesFile = Your Custom File.gradle
    输出:

    > Configure project :
    From ext property
    Your Custom File.gradle
    testJavaLib
    1.1.3
    1.1.2
    
    CONFIGURE SUCCESSFUL in 669ms
    
    

    mavenLocal()

    本地 Maven 仓库是已经使用的所有依赖的本地缓存,在Mac电脑的 ~/.m2 中.

    也可以指定本地仓库的路径:

    repositories {
        maven{
            url "../where"
        }
    }
    

    也可以用 flatDir 添加一个仓库:

    repositories {
        flatDir {
            dirs 'where'
        }
    }
    

    构建类型

      buildTypes {
            debug {
                buildConfigField("String", "API_URL", "\"http://afra55.github.io\"")
                buildConfigField("boolean", "IS_DEBUG", "true")
                resValue("string", "APP_ID", "balalalal_debug")
            }
            release {
                resValue("string", "APP_ID", "balalalal_release")
                buildConfigField("boolean", "IS_DEBUG", "false")
                buildConfigField("String", "API_URL", "http://afra55.github.io")
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
            afra {
                applicationIdSuffix ".afra" // 包名加后缀
                versionNameSuffix "-afra" // 版本名加后缀
                resValue("string", "APP_ID", "balalalal_release")
                buildConfigField("boolean", "IS_DEBUG", "true")
                buildConfigField("String", "API_URL", "https://www.jianshu.com/u/2e9bda9dc932")
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    

    可以使用另一个构建来初始化属性:

            afra.initWith(buildTypes.debug) // 复制 debug 构建的所有属性到新的构建类型中
            afra {
                applicationIdSuffix ".afra" // 包名加后缀
                versionNameSuffix "-afra" // 版本名加后缀
            }
    

    product flavor

    经常用来创建不同的版本。

    
    android {
        compileSdkVersion 29
        buildToolsVersion "29.0.3"
    
        defaultConfig {
            applicationId "com.music.testjavalib"
            minSdkVersion 21
            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
            flavorDimensions "man", "price"
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
    
        productFlavors{
            lover {
                flavorDimensions "man"
                applicationId 'com.flavors.lover'
                versionCode 1
            }
    
            beauty {
                flavorDimensions "man"
                applicationId 'com.flavors.beauty'
                versionCode 2
            }
    
            free {
                flavorDimensions "price"
                applicationId 'com.flavors.free'
                versionCode 2
            }
    
            buy  {
                flavorDimensions "price"
                applicationId 'com.flavors.buy'
                versionCode 2
            }
        }
        ...
     }
    

    flavorDimensions 用来创建维度,当结合两个flavor时,它们可能定义了相同的属性或资源。在这种情况下,flavor维度数组的顺序就决定了哪个flavor配置将被覆盖。在上一个例子中,man 维度覆盖了 price 维度。该顺序也决定了构建variant的名称。

    过滤变种 variant

    在 build.gradle 里添加代码:

    android.variantFilter { variant ->
        // 检查构建类型是否有  afra
        if (variant.buildType.name == 'afra'){
            // 检查所有 flavor
            variant.getFlavors().each() { flavor ->
                if (flavor.name == 'free'){
                    // 如果 flavor 名字等于 free,则忽略这一变体
                    variant.setIgnore(true)
                }
            }
        }
    
    }
    

    buidType 是 afra, flavor 是 free 这一变体就会被忽略:


    jks 密码存储

    创建一个 private.properties 文件, 这个文件不会被发布,并把信息填写在里面:

    release.storeFile = test.jks
    release.password = 111111
    release.keyAlias = test
    

    配置 signingConfigs:

        def pw = ''
        def mKeyAlias = ''
        def storeFilePath = ''
        if (rootProject.file('private.properties').exists()){
            Properties properties = new Properties()
            properties.load(rootProject.file('private.properties').newDataInputStream())
            pw = properties.getProperty('release.password')
            mKeyAlias = properties.getProperty('release.keyAlias')
            storeFilePath = properties.getProperty('release.storeFile')
    
        }
        if (!storeFilePath?.trim()){
            throw new GradleException("Please config your jks file path in private.properties!")
        }
        if (!pw?.trim()){
            throw new GradleException('Please config your jks password in private.properties!')
        }
        if (!mKeyAlias?.trim()){
            throw new GradleException("Please config your jks keyAlias in private.properties!")
        }
    
        signingConfigs {
            release{
                storeFile file(storeFilePath)
                storePassword pw
                keyAlias mKeyAlias
                keyPassword pw
    
                v1SigningEnabled true
                v2SigningEnabled true
            }
        }
    

    Android 构建

    遍历应用的所有构建:

    android.applicationVariants.all{ variant ->
        println('============')
        println(variant.name)
    
    }
    

    通过variant可以访问和操作属性,如果是依赖库的话,就得把 applicationVariants 换成 libraryVariants.

    可以修改生成的 apk 名字:

    android.applicationVariants.all{ variant ->
        println('============')
        println(variant.name)
    
        variant.outputs.each { output ->
            def file = output.outputFile
            output.outputFileName = file.name.replace(".apk", "-afra55-${variant.versionName}-${variant.versionCode}.apk")
            println(output.outputFileName)
    
        }
    }
    

    可以在 Task 中使用 adb 命令:

    task adbDevices {
        doFirst {
            exec {
                executable = 'adb'
                args = ['devices']
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:Android Shadow 插件窥探(1)基础知识简介

        本文链接:https://www.haomeiwen.com/subject/apqydktx.html