- 简介
- 先学会接入
- 了解字节码
- 了解 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的输入。
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']
}
}
}
网友评论