利用ComposeDesktop开辟一款桌面端多功能APK工具

开发者 2024-9-2 19:28:29 171 0 来自 中国
媒介

终于算是忙完了一个阶段!!!从4月份开始,工作内容以及职务上都进行了较大的变更,最直接的就是从外洋项目组调到了国内项目组。国内项目组现在有两个应用在同时跑着,而且尚有几个马甲包也要维护,不知道各人发版的时候复杂不复杂,反正我们每次发版的时候都须要履历--打包、加固、对齐、重署名、打渠道包、上传云存储、天生渠道推广链接、天生内更SQL、上传Mapping文件等等步调(xN),简直是折磨人啊。
以是首要使命就是做出一套自动化的底子办法来,最初直接思量到的方案是【Jenkins+Docker+360下令行加固+VasDolly+Bugly等】的方案(下一篇文章会给各人分享该方案),整个过程下来根本能到达自动化的目的。就这么稳固的跑了一个多月,然而,在5月下旬的时候360加固发布了一个关照,大抵内容就是免费版用户无法利用下令行的加固方式了,只能手动用工具加固。这就导致最初的方案直接垮掉,我泯灭了个把月学习Linux,Pipeline,Docker,还制作了各种镜像,结果突然不能用了,心塞。然而路照旧要继承走下去的,在只管不费钱的条件下,想到了开辟桌面端工具的方案。
功能一览

接下来先给各人一览下桌面端工具的根本功能,我的电脑是Windows的,以是都是基于Windows平台下的build-tools干系工具进行开辟的。起首大部分的功能都是基于jar或exe文件,那么在Java(Kotlin)中我们可以通过如下方式来调用这些外部步伐,exec着实终极也是调用了ProcessBuilder,团体的原理就是云云:
//方式1Runtime.getRuntime().exec(cmd)//方式2ProcessBuilder(cmd)多渠道打包

这是该工具最根本的功能,利用VasDolly方案对APK文件进行多渠道打包(固然该APK文件须要是署名好的)。
1.png 多渠道包下令行工具即 VasDolly.jar,该文件可以在上述GitHub堆栈中找到,常用的下令如下:
// 获取指定APK的署名方式java -jar VasDolly.jar get -s [源apk地点]// 获取指定APK的渠道信息java -jar VasDolly.jar get -c [源apk地点]// 删除指定APK的渠道信息java -jar VasDolly.jar remove -c [源apk地点]// 通过指定渠道字符串添加渠道信息java -jar VasDolly.jar put -c "channel1,channel2" [源apk地点] [apk输出目次]// 通过指定某个渠道字符串添加渠道信息到目的APKjava -jar VasDolly.jar put -c "channel1" [源apk地点] [输出apk地点]// 通过指定渠道文件添加渠道信息java -jar VasDolly.jar put -c channel.txt [源apk地点] [apk输出目次]// 提供了FastMode,天生渠道包时不进行强校验,速率可提拔10倍以上java -jar VasDolly.jar put -c channel.txt -f [源apk地点] [apk输出目次]对齐和署名

上传应用市场前,APK文件大部分会被市场要求进行加固,无论是利用腾讯乐固照旧360加固等方式,加固后APK的署名信息总会被粉碎,以是我们须要对加固后的APK文件重新进行署名。
设置署名

起首我们须要准备好应用的署名信息,该工具支持导入署名文件,并生存相应的StorePass、KeyAlias、KeyPass信息,如下:
当选择APK后,步伐会判断选择的APK是否进行了署名,假如没有署名,那么就会弹窗提示用户选择设置好的署名文件进行署名,署名之后才可进行多渠道打包的过程。
注:该功能现已升级,添加署名文件的时候绑定包名,选择apk后会自动获取到包名然后查找到对应的署名文件自动对齐署名处置处罚,无需手动进行选择了。
对齐

署名的过程则须要用到Android SDK中的两个文件,以Windows体系为例,一个是处置处罚对齐的【build-tools\版本号\zipalign.exe】文件,另一个则是用来署名的【build-tools\版本号\lib\apksigner.jar】文件。
我们先看下zipalign工具的官方阐明:
zipalign is a zip archive alignment tool. It ensures that all uncompressed files in the archive are aligned relative to the start of the file. This allows those files to be accessed directly via mmap(2), removing the need to copy this data in RAM and reducing your app's memory usage. zipalign是一种zip归档对齐工具。它确生存档中全部未压缩的文件都与文件的开头对齐。这答应通过mmap直接访问这些文件,无需将这些数据复制到RAM中,并镌汰应用步伐的内存利用。
zipalign should be used to optimize your APK file before distributing it to end-users. This is done automatically if you build with Android Studio. This documentation is for maintainers of custom build systems. 在将APK文件分发给用户之前,应利用zipalign优化APK文件。假如您利用Android Studio进行构建,这将自动完成。本文档面向定制构建体系的维护职员。
Google官方现在要求在利用apksigner对APK文件进行署名前须要先利用zipalign来优化APK文件,具体下令如下,以Windows下的zipalign.exe文件为例:
//对齐APKzipalign.exe -p -f -v 4 [源apk路径] [输出apk路径]//验证APK是否对齐zipalign.exe -c -v 4 [源apk路径]其他干系的内容可以参阅官网 zipalign 。
署名

当APK文件对齐后,就可以给对齐后的APK进行署名操纵了,署名的方法有两种,我们这里单说利用--ks选项指定密钥库的方式,具体下令如下:
java -jar apksigner.jar sign     --verbose     --ks [KeyStore文件路径]     --ks-pass pass:[KeyStorePass]    --ks-key-alias [KeyAlias]    --key-pass pass:[KeyPass]    --out [输出apk路径]    [源apk路径]下令本身很简单,别搞错参数就好,尤其是两个暗码的参数,后面须要利用【pass:暗码】。输入暗码这里还支持其他格式,假如有须要请参阅官网 apksigner 。
加固、对齐、重署名后,这个apk就可以进行多渠道打包的处置处罚了,然后即可发布到干系市场和渠道。
其他内容

在项目中尚有许多其他的干系设置,比如发版的时候须要对APP进行应用内的更新关照。那么就须要我们填写发版的干系信息,版本名、版本号、更新日志等等内容都须要完满(可根据APK文件的定名来获取部分信息),然后通过这些信息天生应用内部更新的SQL语句,发送钉钉关照给干系配景职员处置处罚。关照这一步又用到了钉钉的SDK,该工具支持设置钉钉呆板人Webhook地点以及须要艾特的职员信息。
打出来的这些包都须要同一上传到云存储上面,这一步利用了AWS的云存储SDK,可以设置云存储桶地点等信息,免除人工手动上传apk的烦恼。上传完毕后会根据文件名天生相应的下载链接并关照到钉钉群,以便市场职员获取到渠道最新的推广链接等。
4.png 桌面端开辟

接下来就说下桌面端的开辟过程,至于Compose MultiPlatform的先容,请参阅官网地点。本文主要就形貌下一些针对桌面端的干系需求。
弹窗

关于弹窗,ComposeDesktop同样提供了Dialog可组合函数:
@Composable public fun Dialog(    onCloseRequest: () -> kotlin.Unit,     state: androidx.compose.ui.window.DialogState,     visible: kotlin.Boolean,     title: kotlin.String,     icon: androidx.compose.ui.graphics.painter.Painter?,     undecorated: kotlin.Boolean,     transparent: kotlin.Boolean,     resizable: kotlin.Boolean,     enabled: kotlin.Boolean,     focusable: kotlin.Boolean,     onPreviewKeyEvent: (androidx.compose.ui.input.key.KeyEvent) -> kotlin.Boolean,     onKeyEvent: (androidx.compose.ui.input.key.KeyEvent) -> kotlin.Boolean,     content: @Composable() (DialogWindowScope.() -> kotlin.Unit)    ): kotlin.Unit { /* compiled code */ }大部分的参数都可以直接看出他的作用,主要看一下state参数,该参数可以控制弹窗的位置及巨细,比方我们设置一个在屏幕中心,宽高为500*300dp的弹窗,那么示例代码如下:
state = DialogState(            position = WindowPosition(Alignment.Center),            size = DpSize(500.dp, 300.dp),        )不外这个弹窗没有阴影,假如想添加的话可以内部套一层Surface来做出阴影结果:
Surface(    modifier = Modifier.fillMaxSize().padding(20.dp),    elevation = 10.dp,    shape = RoundedCornerShape(16.dp))文件选择器

关于文件选择器这一块现在Compose还没有专门的函数,但是我们照旧可以利用原有的方案:

  • javax.swing.JFileChooser
  • java.awt.FileDialog
个人照旧更方向于利用JFileChooser,由于利用第二种方案的话,在页面重组的情况下总是会莫名的弹出选择框来。一个简单的文件选择器如下所示:
private fun showFileSelector(    suffixList: Array<String>,    onFileSelected: (String) -> Unit) {    JFileChooser().apply {        //设置页面风格        try {            val lookAndFeel = UIManager.getSystemLookAndFeelClassName()            UIManager.setLookAndFeel(lookAndFeel)            SwingUtilities.updateComponentTreeUI(this)        } catch (e: Throwable) {            e.printStackTrace()        }        fileSelectionMode = JFileChooser.FILES_ONLY        isMultiSelectionEnabled = false        fileFilter = FileNameExtensionFilter("文件过滤", *suffixList)        val result = showOpenDialog(ComposeWindow())        if (result == JFileChooser.APPROVE_OPTION) {            val dir = this.currentDirectory            val file = this.selectedFile            println("Current apk dir: ${dir.absolutePath} ${dir.name}")            println("Current apk name: ${file.absolutePath} ${file.name}")            onFileSelected(file.absolutePath)        }    }}该方式在利用的过程中也有肯定的缺陷,就是每次打开文件弹窗总是会卡顿一下,以是后续也是有了探求其他高效选择文件方式的想法。
文件拖拽

选择文件除了上面的弹窗选择方式,尚有另一种神奇的方式 - 拖拽选择,原来也是没有头绪,然而在Slack闲逛的时候发现了Jim Sproch保举了一篇干系的文章:dev.to/tkuenneth/f… 。看完后也是名顿开,但是在Compose Desktop中,window是整个窗口,怎样让某一个指定的地区相应我们的文件拖拽事故呢?
还记得在Android上有ComposeView吧,用来嵌套原来的那一套View体系。那么在这里我也是采取了雷同的这么一种方式,实例一个空的JPanel控件然后给它安排到window中去。具体位置及巨细的设置呢,在Compose中可以通过 onPlaced(onPlaced: (LayoutCoordinates) -> Unit) 修饰符来获取到,示例代码如下所示:
@OptIn(ExperimentalComposeUiApi::class)@Composablefun DropBoxPanel(    modifier: Modifier,    window: ComposeWindow,    component: JPanel = JPanel(),    onFileDrop: (List<String>) -> Unit) {    val dropBoundsBean = remember {        mutableStateOf(DropBoundsBean())    }    Box(        modifier = modifier.onPlaced {            dropBoundsBean.value = DropBoundsBean(                x = it.positionInWindow().x,                y = it.positionInWindow().y,                width = it.size.width,                height = it.size.height            )        }) {        LaunchedEffect(true) {            component.setBounds(                dropBoundsBean.value.x.roundToInt(),                dropBoundsBean.value.y.roundToInt(),                dropBoundsBean.value.width,                dropBoundsBean.value.height            )            window.contentPane.add(component)            val target = object : DropTarget(component, object : DropTargetAdapter() {                override fun drop(event: DropTargetDropEvent) {                    event.acceptDrop(DnDConstants.ACTION_REFERENCE)                    val dataFlavors = event.transferable.transferDataFlavors                    dataFlavors.forEach {                        if (it == DataFlavor.javaFileListFlavor) {                            val list = event.transferable.getTransferData(it) as List<*>                            val pathList = mutableListOf<String>()                            list.forEach { filePath ->                                pathList.add(filePath.toString())                            }                            onFileDrop(pathList)                        }                    }                    event.dropComplete(true)                }            }) {            }        }        SideEffect {            component.setBounds(                dropBoundsBean.value.x.roundToInt(),                dropBoundsBean.value.y.roundToInt(),                dropBoundsBean.value.width,                dropBoundsBean.value.height            )        }        DisposableEffect(true) {            onDispose {                window.contentPane.remove(component)            }        }    }}实际运行结果如下,个人感觉根本照旧能到达目的的。
数据的生存

最开始的时候,功能很少,每个设置的数据都是利用了txt文件来一行行生存,但是到了厥后功能越来越复杂,单纯的按行来处置处罚貌似有点左支右绌了,以是思量利用json来生存复杂的范例数据。
json数据的处置处罚从原生JSON到FastJson,Gson,Moshi等都已经体验过了,于是乎便采取了之前未利用过的Jackson。然而不得不说,就现在为止,jackson是我用过最轻巧、优雅的一款剖析库。
假如我有一个List范例的列表数据,那么当我要把这个数据存储到文件的时候只需:
jacksonObjectMapper().writeValue(File, List<String>)而从文件中读取数据也是简单的狠啊:
//方式1val list = jacksonObjectMapper().readValue<List<String>>(jsonFile)//方式2val list : List<String> = jacksonObjectMapper().readValue(jsonFile)这种轻巧真的是深入我心。继承深入相识下Jackson,你会发现它的可扩展性以及可定制性都很强,简直相知恨晚啊。之前也是在一个惬意圈待风俗了,这次自动跳出来居然有了意想不到的劳绩。
但是呢,每个框架也会有它本身的注意点,比如jackson,属性定名不可以是is开头,否则序列化等就会报错。这点似乎在阿里巴巴JAVA手册中似乎也有提到,具体缘故起因请各人自行百度(Google)。
资源的拷贝

当我们利用[java -jar xxx.jar]下令实行jar文件的时候,须要明确指定 jar文件的地点,但是在Compose Desktop中我们要怎么存放并读取这个jar文件呢 ?我们可以从Compose Desktop中读取并展示图片的干系代码中得到启发,假如有一个sample.svg图标文件存放到了项目的 resources 文件夹下,那么我们在引用这张图片的时候就可以利用:
painterResource("sample.svg")我们点进去这个方法看下:
@OptIn(ExperimentalComposeUiApi::class)@Composablefun painterResource(    resourcePath: String): Painter = painterResource(    resourcePath,    ResourceLoader.Default)@ExperimentalComposeUiApi@Composablefun painterResource(    resourcePath: String,    loader: ResourceLoader): Painter = when (resourcePath.substringAfterLast(".")) {    "svg" -> rememberSvgResource(resourcePath, loader)    "xml" -> rememberVectorXmlResource(resourcePath, loader)    else -> rememberBitmapResource(resourcePath, loader)}内里居然有个ResourceLoader类,这名字一听就有戏啊,大概率就是我们须要的内容,而通报的默认参数是ResourceLoader.Default,那么就看下Default的源码吧:
//==========Resources.desktop.kt文件==========@ExperimentalComposeUiApiinterface ResourceLoader {    companion object {        /**         * Resource loader which is capable to load resources from `resources` folder in an application's         * project. Ability to load from dependent modules resources is not guaranteed in the future.         * Use explicit `ClassLoaderResourceLoader` instance if such guarantee is needed.         */        @ExperimentalComposeUiApi        val Default = ClassLoaderResourceLoader()    }    fun load(resourcePath: String): InputStream}@ExperimentalComposeUiApiclass ClassLoaderResourceLoader : ResourceLoader {    override fun load(resourcePath: String): InputStream {        // TODO(https://github.com/JetBrains/compose-jb/issues/618): probably we shouldn't use        //  contextClassLoader here, as it is not defined in threads created by non-JVM        val contextClassLoader = Thread.currentThread().contextClassLoader!!        val resource = contextClassLoader.getResourceAsStream(resourcePath)            ?: (::ClassLoaderResourceLoader.javaClass).getResourceAsStream(resourcePath)        return requireNotNull(resource) { "Resource $resourcePath not found" }    }}//==========ClassLoader类==========public InputStream getResourceAsStream(String name) {    Objects.requireNonNull(name);    URL url = getResource(name);    try {        return url != null ? url.openStream() : null;    } catch (IOException e) {        return null;    }}public URL getResource(String name) {    Objects.requireNonNull(name);    URL url;    if (parent != null) {        url = parent.getResource(name);    } else {        url = BootLoader.findResource(name);    }    if (url == null) {        url = findResource(name);    }    return url;}上述源码的整个逻辑根本上就是两步,根据资源文件名获取到资源文件,然后获取资源文件的输入流。看到这里着实我们已经有两种方案了:

  • 方案一:直接拿到文件的URL然后获取到文件的路径
  • 方案二:根据文件的输入流,将文件重新生存到本机干系目次
然而事变并没有这么简单,假如我们利用方案一,那么在编译运行的时候完全没有题目,全部的资源文件会被生存到【\build\processedResources\jvm】下,此时我们直接可以通过文件的URL获取到文件路径,然后调用即可。但是,当我们打包成安装包后,比方在Windows下利用packageMsi下令打包出msi文件并安装到电脑上后,运行步伐,这时候你就会发现资源文件所在的路径就很奇怪,比方我的工程下是【C:\Program Files\工程名\app\工程名-jvm-1.0-SNAPSHOT-xxxxxx.jar!/资源文件名】,也就是说全部的资源文件被打包进了这个快照文件,假如此时直接利用该路径运行java -jar 等下令,那么肯定就会报错了。
以是最稳妥的方式照旧利用方案二,利用ResourceLoader获取到资源文件流然后重新生存到本机上的干系目次就好了,伪代码如下:
ResourceLoader.Default.load(resourcesPath)    .use { inputStream ->        val fos = FileOutputStream(file)        val buffer = ByteArray(1024)        var len: Int            while (((inputStream.read(buffer).also { len = it })) != -1) {                fos.write(buffer, 0, len)                }          fos.flush()              inputStream.close()              fos.close()          }打包MSI

在Windows情况下打包Msi格式安装包的时候,有一个downloadWix的Task,该Task涉及到了Wix资源的下载,如下 :
Task :downloadWix Download github.com/wixtoolset/…
在IDEA中下载大概会非常的迟钝,此时我们可以复制上述地点,登上梯子,然后直接去GitHub下载。下载完毕后直接放入【/build/wixToolset】目次下即可,再次编译速率就会腾飞了。
总结

简直没想到啊,作为一个Android开辟者,现在借助Compose Desktop开辟起桌面端居然能这么的得心应手,我对Compose真是越来越喜好了。
别的呢,跳出业务这一段时间来处置处罚这些东西也让我对干预APK的打包等过程从理论迈出了实践的一步,同时对市场和运营同砚的工作也有了更多相识,通过该工具资助其处置处罚了部分重复呆板式的工作,部分间的感情也得到了进一步的增温(狗头风趣)。
就编到这吧,桌面工具还须要一连的维护跟优化,根本是面向市场和运营同事编程了。关于开头说的Jenkins那一套着实早就写好了,是鄙人少有的万字长文,但是中心变故太大,不绝也没发布出来,接下来会重新整理下并发布,还请各人多多指正。
作者:乐翁龙
链接:https://juejin.cn/post/7122645579439538183
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2025-2-23 00:07, Processed in 0.191041 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表