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

  • 简介
  • 先学会接入
  • 了解字节码
  • 了解 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的输入。

简单应用

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']
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章