Android Shadow 插件窥探(1)基础知识简介
原文地址 www.jianshu.com
- 简介
- 先学会接入
- 了解字节码
- 了解 Javaassist
- 引入依赖
- 基础 Demo
- javapoet
- 依赖引入
- 样例
- 生成样例的代码
- 其他相关,摘自 Github, 略过
- Android 中的 ClassLoader
- BootClassLoader
- PathClassLoader
- DexClassLoader
- Transfrom API 简介
- 简单应用
- 在gradle简单注册
- Gradle 聊一聊
- buildConfigField
- resValue
- 统一版本
- mavenLocal()
- 构建类型
- product flavor
- 过滤变种 variant
- jks 密码存储
- Android 构建
简介
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 >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'] } } }
本文作者:cps666
本文链接:https://www.cnblogs.com/cps666/p/17352737.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步