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 版本。
http://www.github.com/LXD31256949…
填写License key可以激活:9916E3FF-2189-4A8E-B721-94442CDAA215
总结
- 这篇文章,算是对这个软件的一个阶段性总结吧。
- 一个是学习 Compose 相关的知识,以及了解 compose-desktop 相关的桌面组件,并进行开发桌面应用。
- 另一个方面是 Android 这方面的知识学习。
来源:juejin.cn/post/7369838480983490610