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