前言
插件里面有很多日志输出,调试的时候需要看,编译时又不想看到这密密麻麻的日志怎么办?
对于一些重要信息,在编译期如何收集并持久化存储?
Gradle
可以通过读取配置的方式来提高代码灵活性,比如读取本地的日志开关来启用日志输出。对于重要信息可以将数据实时写入到本地文件中,以便后续查阅。
配置读取
配置读取有很多种方式,比如读取项目根目录下的gradle.properties
和local.properties
。或者配置指定位置的文件,在插件内通过约定的路径读取。也可以通过插件配置扩展参数的方式实现等等。
读取根目录下的properties
kotlin
class Main: Plugin<Project> {
override fun apply(p0: Project) {
settingManager.init()
}
}
kotlin
class SettingManager {
private var gradleProperties : Properties? = null
private var localProperties : Properties? = null
fun init() {
initProperties(project)
}
/**
* 初始化Properties配置
*/
private fun initProperties(project: Project) {
gradleProperties = loadProperties(project, "gradle.properties")
localProperties = loadProperties(project, "local.properties")
}
private fun loadProperties(project: Project, name: String): Properties {
var properties = Properties()
val propertiesFile = File(project.rootDir, name)
if (propertiesFile.isFile && propertiesFile.exists()) {
propertiesFile.inputStream().use { inputStream ->
properties = Properties().apply {
load(inputStream)
}
}
}
return properties
}
fun isLogOpen(): Boolean {
return gradleProperties?.getProperty("privacyPluginLogOpen")?.toBoolean() ?: false
}
}
ini
// gradle.properties
privacyPluginLogOpen=true
读取扩展参数
kotlin
// 定义一个扩展类,用于接收扩展参数,kotlin创建的class的需要加上open,不然会报错
open class PrivacyExtension {
var enable = false
}
kotlin
class SettingManager {
companion object {
private const val PRIVACY_EXTENSION_NAME = "PrivacyExtension"
}
private var privacyExtension: PrivacyExtension? = null
fun init() {
initProperties(project)
initGradleParams(project)
}
/**
* 初始化gradle配置
*/
private fun initGradleParams(project: Project) {
project.extensions.create(PRIVACY_EXTENSION_NAME, PrivacyExtension::class.java)
project.afterEvaluate {
// 注意配置读取时机
privacyExtension = project.extensions.findByType(PrivacyExtension::class.java)
}
}
bash
// app/build.gradle
android {
...
}
PrivacyExtension {
enable = true
}
文件写入
合规api调用写入本地文件
对于隐私合规要求,需要记录哪些类调用了AndroidId
,将结果写入app/build/outputs/privacy/privacy.txt
kotlin
class Main: Plugin<Project> {
override fun apply(p0: Project) {
logManager.init()
project.gradle.buildFinished {
logManager.close()
}
}
}
kotlin
class LogManager(...) {
private var writer: BufferedWriter? = null
private var fileWriter: FileWriter? = null
private val lock = ReentrantLock()
// 缓冲区大小设置为8KB
private val bufferSize = 8 * 1024
fun init() {
initWriter()
}
private fun initWriter() {
try {
logOpen = context.getPrivacyPluginManager().settingManager.isLogOpen()
val logFilePath = context.project.buildDir.absolutePath + File.separator + "outputs/privacy/privacy.txt"
val file = File(logFilePath)
// 确保父目录存在
if (!file.parentFile.exists()) {
file.parentFile.mkdirs()
}
if (file.exists()) {
file.delete()
}
// 以追加模式打开文件
fileWriter = FileWriter(logFilePath, true)
writer = BufferedWriter(fileWriter!!, bufferSize)
} catch (e: Exception) {
throw RuntimeException("初始化日志写入器失败", e)
}
}
fun writerMessage(message: String) {
if (writer == null) {
return
}
lock.lock()
try {
val logLine = "$message\n"
writer?.write(logLine)
// 重要日志可以在这里调用flush()
// writer?.flush()
} catch (e: Exception) {
printLog("日志写入失败: ${e.message}")
} finally {
lock.unlock()
}
}
fun close() {
try {
writer?.flush()
writer?.close()
fileWriter?.close()
printLog("LogManager close()")
} catch (e: Exception) {
printLog("关闭日志写入器失败: ${e.message}")
}
}
}
kotlin
class PrivacyMethodProxyVisitor(
...
) : AdviceAdapter(api, methodVisitor, access, methodName, descriptor) {
override fun visitMethodInsn(
opcode: Int,
owner: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
if (owenr == "android/provider/Settings$Secure" && name == "getString" && descriptor == "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;") {
logManager.writerMessage("$className -> $owner -> $name -> $descriptor -> $isInterface")
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
编译完成会生成文件privacy.txt

bash
androidx/core/app/NotificationManagerCompat -> android/provider/Settings$Secure -> getString -> (Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String; -> false
androidx/core/location/LocationManagerCompat -> android/provider/Settings$Secure -> getString -> (Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String; -> false
com/kongge/plugindemo/PermissionHelper -> android/provider/Settings$Secure -> getString -> (Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String; -> false
clean
提示文件被占用问题
此时clean
项目,会提示privacy.txt
文件无法删除,提示被占用,但是手动删除又可以删除,文件没有被占用。看build
日志
scss
LogManager initWriter()
> Task :app:clean
LogManager close()
发现是先执行了clean
任务,此时执行了删除build
目录的操作,但是此时文件是打开状态,privacy.txt
就是被当前程序占用的。clean
执行完成之后才执行close()
,此时解除了占用,手动删除就是正常的。
所以可以将close()
放到clean
任务之前执行。即创建一个任务,用于执行close()
,并让clean
依赖此任务。
kotlin
class LogManager(...) {
fun init() {
initWriter()
initTask()
}
private fun initTask() {
val cleanTask = context.project.tasks.named("clean", Task::class.java)
val preCleanTask = context.project.tasks.register("preClean") {
it.group = "Cleanup"
it.description = "clean 任务执行前的准备工作"
it.doLast {
close()
}
}
cleanTask.dependsOn(preCleanTask)
}
}
再次执行clean
,发现可以正常删除privacy.txt
了,查看build
日志
ruby
LogManager initWriter()
> Task :app:preClean
LogManager close()
> Task :app:clean
小结
- 配置读取有很多种方式
- 读取项目根目录下的
gradle.properties
和local.properties
- 配置指定位置的文件,在插件内通过约定的路径读取
- 通过插件配置扩展参数的方式实现
- 读取项目根目录下的
- 扩展参数
- kotlin创建的class的需要加上open,不然会报错
- 注意配置读取时机,需要在project afterEvaluate调用之后
- 文件写入
- 构建完成之后,注意关闭写入流
clean
执行报错文件被占用问题,新建一个任务,在clean
之前关闭写入流