Compose Desktop 写一个 Android 提效工具

前言

在日常的工作中,很多工作和操作其实都是重复的,这个时候,就会想,能不能通过工具进行一键操作。

由于本人是Android开发,寻找解决方案时发现了compose-multiplatform,于是就写个工具玩一玩。

软件介绍

AdbDevTools 是支持windows和mac的,并且支持浅色模式和暗黑模式,下面的截图都是在暗黑模式下。

  • 目的:都是为了减少重复性工作,节省开发者时间。
  • 简化Hprof文件管理:轻松一键导出、管理和分析Hprof文件,全面支持LeakCanary数据处理。
  • 内存泄漏分析:对 Hprof 文件进行内存泄漏分析,快速定位问题根源。
  • 位图资源管理:提供位图预览、分析和导出功能。
  • Deep Link快速调用:管理和测试Deep Link,提高开发和调试速度。
  • 开发者选项快捷操作:包含多项开发者选项的快捷操作。

功能介绍

内存快照文件管理和分析

常规操作:

  • 打开AS Memory Profiler,dump 出内存快照文件,等待内存快照文件生成,查看泄露的 Activity 或者 Fragment。
  • Android 8以下还可以有个 BitmapPreview 预览 Bitmap,但是每次只能预览一个 Bitmap。
  • 如果重新打开 AS,刚刚生成的 hprof 文件在哪里??
  • 所以如果想保存刚刚生成的 hprof 文件,就得在生成文件后,手动点击把文件保存一下到电脑上。
  • 如果想找到 LeakCanary 生成的文件,得找到对应的文件目录,然后再用 adb pull 一下到电脑上。。

懒人操作:

  • 一键 dump 出内存快照,自动化分析,生成一份报告。
  • Android 8以下的快照文件,可以一键导出所有 Bitmap 实例,方便预览。
  • 通过工具,管理最近打开的 hprof 文件
  • 一键导出 LeakCanary 生成的文件,无需手动操作。

开发者选项快捷操作

在日常的开发工作中,可能要经常打开开发者选项页面,打开某一个开关。

常规操作:打开设置页面,找到开发者选项,点击进入开发者页面,上下滑动,找到某一个开关,进行调整。这一系列的操作,有点繁琐。

懒人操作:在PC软件内,一键操作,直接打开开关。一步到位,不需要在手机里找来找去和点点点。

开发

代码架构设计

github.com/theapache64...,基于这个库,可以使用 Android 的开发方式,去开发一个桌面软件。

简单的这样理解。

对于单个桌面应用,其实就是类似 Android 的 Application。

对于应用内的窗口,其实就是类似 Android 的 Activity。

对于窗口内的各种子页面,其实就是类似 Android 的 Fragment,这边当成一个个的 Component 实现。

Application

  • 基类 Application。提供一个 startActivity 方法,用于打开某个页面。
  • 自定义 MyApplication,继承 Application,在 onCreate 方法里面,执行一些应用初始化操作。
  • 比如 onCreate 的时候,启动 MainActivity。
  • main() 方法,调用 MyApplication 的 onCreate 方法即可。
kotlin 复制代码
open class Application  {

    protected fun startActivity(intent: Intent) {
        val activity = intent.to.java.newInstance()
        activity.intent = intent
        activity.onCreate()
    }

    open fun onCreate() {

    }
}

class MyApplication(args: AppArgs) : Application() {

    override fun onCreate() {
        super.onCreate()
        
        Arbor.d("onCreate")

        val splashIntent = MainActivity.getStartIntent()
        startActivity(splashIntent)
    }
}

fun main() {
    MyApplication(appArgs).onCreate()
}

Activity

  • 自定义 MainActivity,在 onCreate 方法里面,创建和展示 Window 。
kotlin 复制代码
class MainActivity : Activity() {

    companion object {
        fun getStartIntent(): Intent {
            return Intent(MainActivity::class).apply {
                // putExtra
            }
        }
    }

    @OptIn(ExperimentalComposeUiApi::class)
    override fun onCreate() {
        super.onCreate()

        val lifecycle = LifecycleRegistry()
        val root = NavHostComponent(DefaultComponentContext(lifecycle))

        application {

            val intUiThemes by mainActivityViewModel.intUiThemes.collectAsState()
            val themeDefinition = if (intUiThemes.isDark()) {
                JewelTheme.darkThemeDefinition()
            } else {
                JewelTheme.lightThemeDefinition()
            }

            IntUiTheme(
                themeDefinition,
                styling = ComponentStyling.decoratedWindow(
                    titleBarStyle = when (intUiThemes) {
                        IntUiThemes.Light -> TitleBarStyle.light()
                        IntUiThemes.LightWithLightHeader -> TitleBarStyle.lightWithLightHeader()
                        IntUiThemes.Dark -> TitleBarStyle.dark()
                        IntUiThemes.System -> if (intUiThemes.isDark()) {
                            TitleBarStyle.dark()
                        } else {
                            TitleBarStyle.light()
                        }
                    }
                )
            ) {
                DecoratedWindow(visible = mainWindowVisible,
                    onCloseRequest = {
                        ::exitApplication
                        mainActivityViewModel.exitMainWindow()
                    }, state = rememberWindowState(),
                    title = "${MyApplication.appArgs.appName} (${MyApplication.appArgs.version})",
                    onPreviewKeyEvent = {
                        if (
                            it.key == Key.Escape &&
                            it.type == KeyEventType.KeyDown
                        ) {
                            root.onBackClicked()
                            true
                        } else {
                            false
                        }
                    }
                ) {
                    TitleBarView(intUiThemes)
                    root.render()
                }

            }
            
        }
    }
}

Component

Component:组件,可以是一个窗口,也是可以是窗口中的某一个页面,都可以当成组件处理。

对应单个组件,每个组件封装对应的业务逻辑处理,驱动相应的UI进行显示。

对于业务逻辑的处理,可以采用 Store+Reducer 这种偏前端思想的方式,也可以采用 Android 现在比较流行的 MVI 进行处理。

状态管理容器,只需要提供一些可观察对象就行了,驱动View层进行重组,刷新UI。

组件树:应用中的多个窗口,窗口中的多个页面,可以分别拆分成多个组件,每个组件封装处理各自的逻辑,最后构成一棵组件树的结构。

比如这个应用,被我拆成若干个Componet,分别处理相应的业务逻辑。

kotlin 复制代码
@Singleton
@Component(
    modules = [
        PreferenceModule::class
    ]
)
interface AppComponent {

    fun inject(splashScreenComponent: SplashScreenComponent)

    fun inject(mainScreenComponent: MainScreenComponent)

    fun inject(adbScreenComponent: AdbScreenComponent)

    fun inject(analyzeScreenCompoment: AnalyzeScreenCompoment)

    fun inject(updateScreenComponent: UpdateScreenComponent)

    fun inject(importLeakCanaryComponent: ImportLeakCanaryComponent)
}

ViewModel

  • ViewModel 这个比较简单,只是一个普通的类,用于处理业务逻辑,并维护UI层所需的状态数据。
  • ViewModel 的创建和销毁,这个会利用到 DisposableEffect 这个东西。DisposableEffect 的主要作用是在组合函数的启动和销毁时执行一些清理工作,以确保资源正确释放。
  • 在组合函数启动的时候,创建 ViewModel,并进行初始化。
  • 在组合函数销毁的时候,销毁 ViewModel,释放 ViewModel 的资源,类似 Android 中 ViewModel 的 clear 方法。
kotlin 复制代码
class AnalyzeViewModel @Inject constructor(
    val hprofRepo: HprofRepo
) {

    private lateinit var viewModelScope: CoroutineScope

    fun init(scope: CoroutineScope) {
        this.viewModelScope = scope
    }

    fun analyze(
        heapDumpFile: File, proguardMappingFile: File?
    ) {
        viewModelScope.launch(Dispatchers.IO) {
           //耗时方法,分析文件
        }
    }


    fun dispose() {
        viewModelScope.cancel()
    }
}
kotlin 复制代码
/**
 * 分析内存数据
 */
class AnalyzeScreenCompoment(
    appComponent: AppComponent,
    private val componentContext: ComponentContext,
    private val hprofFile: String,
    private val onBackClicked: () -> Unit,
) : Component, ComponentContext by componentContext {


    init {
        appComponent.inject(this)
    }

    @Inject
    lateinit var analyzeViewModel: AnalyzeViewModel

    @Composable
    override fun render() {
        val scope = rememberCoroutineScope()

        DisposableEffect(analyzeViewModel) {
            //初始化ViewModel
            analyzeViewModel.init(scope)
            //调用ViewModel里面的方法
            analyzeViewModel.analyze(heapDumpFile = File(hprofFile), proguardMappingFile = null)

            onDispose {
                //销毁ViewModel
                analyzeViewModel.dispose()
            }
        }

        //观察ViewModel,实现UI逻辑
        analazeScreen(analyzeViewModel)

    }
}

adb 功能开发

比如 dump 内存快照,安装adb,一部分开发者选项控制,本质上都是可以通过 adb 命令进行设置的。

  • Adb第三方库:malinskiy.github.io/adam/,这个库是 Kotlin 编写的。
  • 库代码主要是协程、Flow、Channel,使用起来挺方便的。
  • 一条 adb 命令就是一个 Request,内置了挺多现成的 Request 可以使用,也可以自定义 Request 编写一些复杂的命令。
  • 比如使用adb devices,列出当前的设备列表,只需要一行代码即可。
css 复制代码
val devices: List<Device> = adb.execute(request = ListDevicesRequest())
  • 如果需要监听设备的连接状态变化,可以通过执行 AsyncDeviceMonitorRequest 即可,返回值是一个 Channel 。
ini 复制代码
val deviceEventsChannel: ReceiveChannel<List<Device>> = adb.execute(
    request = AsyncDeviceMonitorRequest(),
    scope = GlobalScope
)

for (currentDeviceList in deviceEventsChannel) {
    //...
}
  • 安装 apk,执行 StreamingPackageInstallRequest,传入相应的参数即可。
kotlin 复制代码
    suspend fun installApk(file: String, serial: String): Boolean {
        Arbor.d("installApk file:$file,serial:$serial")
        try {
            val result = adb.execute(
                request = StreamingPackageInstallRequest(
                    pkg = File(file),
                    supportedFeatures = listOf(Feature.CMD),
                    reinstall = true,
                    extraArgs = emptyList()
                ),
                serial = serial
            )
            Arbor.d("installApk:$result")
            return result
        } catch (e: Exception) {
            e.printStackTrace()
            return false
        }
    }

开发者选项控制

打开过度绘制、布局边界

  • 开发者选项里面的很多配置,都是系统属性。关于系统属性的部分原理,可以在这里了解一下。

Android 系统属性学习与使用 - 掘金

  • 一部分系统属性,是可以支持 adb 修改,并且可以立马生效的。
  • 比如布局边界的属性是 debug.layout,设置为 true 即可打开开关。
  • 比如过度绘制对应的属性是 debug.hwui.overdraw,设置为 show 即可打开开关。
  • 通过下面几个 adb 命令,转化成相应的代码实现即可。
arduino 复制代码
//读取所有的prop,会输出所有系统属性的key和value
adb shell getprop
//读取key为propName的系统属性
adb shell getprop ${propName}
//修改key为propName的系统属性,新值为propValue
adb shell setprop ${propName} ${propValue}
  • adb shell service call activity 1599295570,这个命令,主要是为了修改 prop 之后能够立马生效。
kotlin 复制代码
    /**
     * 修改 prop 手机配置
     */
    suspend fun changeProp(propName: String, propValue: String, serial: String) {
        adb.execute(request = ShellCommandRequest("setprop $propName $propValue"), serial = serial)
        adb.execute(request = ShellCommandRequest("service call activity 1599295570"), serial = serial)
    }

跳转到开发者选项页面

有些开关还是得手动去设置的,所以提供了这样的一个按钮,点击直接跳转到开发者选项页面。

如果使用命令是这样的。

css 复制代码
adb shell am start -a com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS

转化成对应的代码实现。

kotlin 复制代码
    suspend fun startDevelopActivity(serial: String){
        adb.execute(
            request = ShellCommandRequest("am start -a com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS"),
            serial = serial
        )
    }

内存分析

  • 这里就不细讲了,主要是使用 shark 库进行解析 Hprof 文件,然后分析内存泄露问题。
  • 使用shark库解析Hprof文件:juejin.cn/post/704375...
  • 过程挺简单的,就是通过 adb dump 出内存快照文件,然后 pull 到电脑上,并删掉原文件。
lua 复制代码
1、识别本地所有应用的 packageName
2、adb shell ps | grep packageName 查看应用 pid
3、adb shell am dumpheap <PID>  <HEAP-DUMP-FILE-PATH>  开始 dump pid 进程的 hprof 文件到 path
4、adb pull 命令
  • 另一种情况,如果你有使用 LeakCanary,但是 LeakCanary App是运行在手机上的,在手机上查看泄露引用链,其实不是那么方便。
  • 后面分析了一下,LeakCanary 生成的文件,都放在了 /storage/emulated/0/Download 的目录下,所以搞个命令一键拉取到电脑上,在软件里面进行分析即可。

Html 文件生成

根据内存分析结果,生成一份 html 格式的文件报告,方便在浏览器中进行预览。

  • 尴尬的是,自己不太会写 html,另一个是,这个软件是纯 Kotlin 开发,要引入 js 貌似也不太方便。
  • github.com/Kotlin/kotl...
  • 刚好官方有个 kotlinx-html 库,可以使用 Kotlin 来开发 HTML 页面。
  • 引入相关依赖
scss 复制代码
    implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.9.1")
    implementation("org.jetbrains.kotlinx:kotlinx-html:0.9.1")
  • 按照官方文档进行使用,还是挺简单的。
python 复制代码
        val html = createHTML().html {
            head {
                title { +"My HTML File" }
            }
            body {
                h1 { +"Memory Analysis Report" }
                h2 { +"Basic Info" }
                p { +"Heap dump file path: ${hprofFile}" }

                p { +"Build.VERSION.SDK_INT: ${androidMetadataMap?.get("Build.VERSION.SDK_INT")}" }
                p { +"Build.MANUFACTURER: ${androidMetadataMap?.get("Build.MANUFACTURER")}" }
                p { +"App process name: ${androidMetadataMap?.get("App process name")}" }

                h2 { +"Memory leaks" }
            }
        }

下载地址

现在只有 mac 版本,没有 windows 版本。

www.github.com/LXD31256949...

填写License key可以激活:9916E3FF-2189-4A8E-B721-94442CDAA215

总结

  • 这篇文章,算是对这个软件的一个阶段性总结吧。
  • 一个是学习 Compose 相关的知识,以及了解 compose-desktop 相关的桌面组件,并进行开发桌面应用。
  • 另一个方面是 Android 这方面的知识学习。
相关推荐
2401_8979078623 分钟前
10天学会flutter DAY2 玩转dart 类
android·flutter
m0_748233641 小时前
【PHP】部署和发布PHP网站到IIS服务器
android·服务器·php
Yeats_Liao2 小时前
Spring 定时任务:@Scheduled 注解四大参数解析
android·java·spring
雾里看山4 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
水瓶丫头站住12 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch13 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch16 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛17 小时前
编译Android平台使用的FFmpeg库
android
浩宇软件开发17 小时前
Android开发,待办事项提醒App的设计与实现(个人中心页)
android·android studio·android开发
ac-er888818 小时前
Yii框架中的多语言支持:如何实现国际化
android·开发语言·php