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 这方面的知识学习。
相关推荐
帅得不敢出门5 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了7 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任9 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山9 小时前
Android“引用们”的底层原理
android·java
迃-幵9 小时前
力扣:225 用队列实现栈
android·javascript·leetcode
大风起兮云飞扬丶10 小时前
Android——从相机/相册获取图片
android
Rverdoser10 小时前
Android Studio 多工程公用module引用
android·ide·android studio
aaajj10 小时前
[Android]从FLAG_SECURE禁止截屏看surface
android
@OuYang10 小时前
android10 蓝牙(二)配对源码解析
android
Liknana10 小时前
Android 网易游戏面经
android·面试