- 簡介
- 先學會接入
- 瞭解字節碼
- 瞭解 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插件框架正式開源
真的只是簡單介紹。
先學會接入
瞭解字節碼
瞭解 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, 略過
- 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();
}
- 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;
}
}
- 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();
}
});
}
- 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);
- 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
系統類加載器分三種:BootClassLoader
,PathClassLoader
,DexClassLoader
。
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!");
}
}
-
dexPath
:dex相關文件的路徑集合,多個文件用路徑分割符分割,默認的文件分割符爲 ":"; -
optimizedDirectory
:解壓的dex文件儲存的路徑,這個路徑必須是一個內部儲存路徑,一般情況下使用當錢應用程序的私有路徑/data/data/<Package Name>/...
; -
librarySearchPath
:包含C++庫的路徑集合,多個路徑用文件分割符分割,可以爲null; -
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']
}
}
}