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 方法即可。
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 。
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,分别处理相应的业务逻辑。

@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 方法。
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()
    }
}

/**
 * 分析内存数据
 */
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,列出当前的设备列表,只需要一行代码即可。
val devices: List<Device> = adb.execute(request = ListDevicesRequest())

  • 如果需要监听设备的连接状态变化,可以通过执行 AsyncDeviceMonitorRequest 即可,返回值是一个 Channel 。
val deviceEventsChannel: ReceiveChannel<List<Device>> = adb.execute(
    request = AsyncDeviceMonitorRequest(),
    scope = GlobalScope
)

for (currentDeviceList in deviceEventsChannel) {
    //...
}

  • 安装 apk,执行 StreamingPackageInstallRequest,传入相应的参数即可。
    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
        }
    }

开发者选项控制

打开过度绘制、布局边界

  • 开发者选项里面的很多配置,都是系统属性。关于系统属性的部分原理,可以在这里了解一下。
  • 一部分系统属性,是可以支持 adb 修改,并且可以立马生效的。
  • 比如布局边界的属性是 debug.layout,设置为 true 即可打开开关。
  • 比如过度绘制对应的属性是 debug.hwui.overdraw,设置为 show 即可打开开关。
  • 通过下面几个 adb 命令,转化成相应的代码实现即可。
//读取所有的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 之后能够立马生效。
    /**
     * 修改 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)
    }

跳转到开发者选项页面

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

如果使用命令是这样的。

adb shell am start -a com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS

转化成对应的代码实现。

    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 到电脑上,并删掉原文件。
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 页面。
  • 引入相关依赖
    implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.9.1")
    implementation("org.jetbrains.kotlinx:kotlinx-html:0.9.1")

  • 按照官方文档进行使用,还是挺简单的。
        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 这方面的知识学习。

如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料

在这里插入图片描述
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )

  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以给你一个简单的Compose App示例。这个App将显示一个列表,列表中有一些简单的文本项。你可以点击其中的任何一项,在屏幕底部显示相应的文本。 首先,在build.gradle文件中,添加以下依赖项: ``` dependencies { implementation "androidx.compose.ui:ui:1.0.1" implementation "androidx.compose.material:material:1.0.1" implementation "androidx.compose.runtime:runtime-livedata:1.0.1" implementation "androidx.activity:activity-compose:1.3.0-alpha07" } ``` 然后,创建一个MainActivity类,将其声明为一个@AndroidEntryPoint类,以便使用Compose: ``` @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyList() } } } ``` 现在,我们可以创建MyList组件,它将显示我们的项目列表。对于这个例子,我们将硬编码一些文本项,但在实际应用中,你可以使用一个ViewModel从网络或本地数据库中获取数据。 ``` @Composable fun MyList() { val items = listOf( "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", ) Column(modifier = Modifier.fillMaxSize()) { items.forEach { item -> ListItem(item = item) } } } ``` 接下来,我们创建一个ListItem组件,它将显示每个项目。当用户点击任何一个项目时,我们将使用LiveData在屏幕底部更新相应的文本。 ``` @Composable fun ListItem(item: String) { val selectedItem = remember { mutableStateOf("") } Text( text = item, modifier = Modifier .padding(16.dp) .clickable { selectedItem.value = item } ) if (selectedItem.value == item) { Text( text = "You selected $item", modifier = Modifier .padding(16.dp) .align(Alignment.BottomStart) ) } } ``` 最后,在MainActivity中,我们使用setContent { }将MyList组件设置为内容。 这样,我们就完成了一个简单的Compose App,可以显示项目列表,并在用户点击任何一个项目时,在屏幕底部显示相应的文本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值