Android Gradle学习(十三)- 配置读取和文件写入

前言

插件里面有很多日志输出,调试的时候需要看,编译时又不想看到这密密麻麻的日志怎么办?

对于一些重要信息,在编译期如何收集并持久化存储?

Gradle可以通过读取配置的方式来提高代码灵活性,比如读取本地的日志开关来启用日志输出。对于重要信息可以将数据实时写入到本地文件中,以便后续查阅。

配置读取

配置读取有很多种方式,比如读取项目根目录下的gradle.propertieslocal.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

小结

  1. 配置读取有很多种方式
    • 读取项目根目录下的gradle.propertieslocal.properties
    • 配置指定位置的文件,在插件内通过约定的路径读取
    • 通过插件配置扩展参数的方式实现
  2. 扩展参数
    • kotlin创建的class的需要加上open,不然会报错
    • 注意配置读取时机,需要在project afterEvaluate调用之后
  3. 文件写入
    • 构建完成之后,注意关闭写入流
    • clean执行报错文件被占用问题,新建一个任务,在clean之前关闭写入流
相关推荐
古法安卓3 小时前
Android-日志系统源码解析
android·java·android studio
小夏coding4 小时前
从"一把梭"到"精妙拆解" —— 滑动窗口计时框架的设计演进
java·后端
MacroZheng5 小时前
同事问我:"Claude Code经常失忆,不怕它把项目搞炸?",我:"怕,三个Markdown文件给它装个永不丢失的外置大脑!"
java·人工智能·后端
十年Java程序媛6 小时前
深度实战:JDK21 虚拟线程 + HikariCP 生产正确配比、坑点、监控全套
java·spring boot
xiaoqiMikko6 小时前
有人在搜一个不存在的 Tomcat 版本
java·tomcat
用户31268748772010 小时前
ConcurrentHashMap 怎么保证线程安全?从分段锁到 CAS+synchronized
java
vipxieliang10 小时前
ValidX vs Apache Commons Validator:功能与性能对比
java·spring boot
SimonKing10 小时前
升级Spring Boot 4后,从 Jackson 2 到 3,到底有哪些变化
java·后端·程序员
吃饱了得干活11 小时前
一篇讲清楚Spring Boot:自动装配、启动器、过滤器、拦截器、设计模式
java·spring boot·后端
lhldsg12 小时前
AI零售系统实战指南:从架构设计到落地部署全解析
java·人工智能·小程序·uni-app·零售