欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

ComposeDesktop開(kāi)發(fā)桌面端多功能APK工具

 更新時(shí)間:2022年07月25日 09:52:55   作者:樂(lè)翁龍  
這篇文章主要為大家介紹了ComposeDesktop開(kāi)發(fā)桌面端多功能APK工具實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

終于算是忙完了一個(gè)階段!?。?月份開(kāi)始,工作內(nèi)容以及職務(wù)上都進(jìn)行了較大的變動(dòng),最直接的就是從海外項(xiàng)目組調(diào)到了國(guó)內(nèi)項(xiàng)目組。

國(guó)內(nèi)項(xiàng)目組目前有兩個(gè)應(yīng)用在同時(shí)跑著,而且還有幾個(gè)馬甲包也要維護(hù),不知道大家發(fā)版的時(shí)候復(fù)雜不復(fù)雜,反正我們每次發(fā)版的時(shí)候都需要經(jīng)歷--打包、加固、對(duì)齊、重簽名、打渠道包、上傳云存儲(chǔ)、生成渠道推廣鏈接、生成內(nèi)更SQL、上傳Mapping文件等等步驟(xN),簡(jiǎn)直是折磨人啊。

所以首要任務(wù)就是做出一套自動(dòng)化的基礎(chǔ)設(shè)施來(lái),最初直接考慮到的方案是【Jenkins+Docker+360命令行加固+VasDolly+Bugly等】的方案(下一篇文章會(huì)給大家分享該方案),整個(gè)過(guò)程下來(lái)基本能達(dá)到自動(dòng)化的目的。

就這么穩(wěn)定的跑了一個(gè)多月,然而,在5月下旬的時(shí)候360加固發(fā)布了一個(gè)通知,大致內(nèi)容就是免費(fèi)版用戶無(wú)法使用命令行的加固方式了,只能手動(dòng)用工具加固。這就導(dǎo)致最初的方案直接垮掉,我花費(fèi)了個(gè)把月學(xué)習(xí)Linux,Pipeline,Docker,還制作了各種鏡像,結(jié)果突然不能用了,心塞。然而路還是要繼續(xù)走下去的,在盡量不花錢(qián)的前提下,想到了開(kāi)發(fā)桌面端工具的方案。

功能一覽

接下來(lái)先給大家一覽下桌面端工具的基本功能,我的電腦是Windows的,所以都是基于Windows平臺(tái)下的build-tools相關(guān)工具進(jìn)行開(kāi)發(fā)的。首先大部分的功能都是基于jar或exe文件,那么在Java(Kotlin)中我們可以通過(guò)如下方式來(lái)調(diào)用這些外部程序,exec其實(shí)最終也是調(diào)用了ProcessBuilder,整體的原理就是如此:

//方式1
Runtime.getRuntime().exec(cmd)
//方式2
ProcessBuilder(cmd)

多渠道打包

這是該工具最基本的功能,使用VasDolly方案對(duì)APK文件進(jìn)行多渠道打包(當(dāng)然該APK文件需要是簽名好的)。

多渠道包命令行工具即 VasDolly.jar,該文件可以在上述GitHub倉(cāng)庫(kù)中找到,常用的命令如下:

// 獲取指定APK的簽名方式
java -jar VasDolly.jar get -s [源apk地址]
// 獲取指定APK的渠道信息
java -jar VasDolly.jar get -c [源apk地址]
// 刪除指定APK的渠道信息
java -jar VasDolly.jar remove -c [源apk地址]
// 通過(guò)指定渠道字符串添加渠道信息
java -jar VasDolly.jar put -c "channel1,channel2" [源apk地址] [apk輸出目錄](méi)
// 通過(guò)指定某個(gè)渠道字符串添加渠道信息到目標(biāo)APK
java -jar VasDolly.jar put -c "channel1" [源apk地址] [輸出apk地址]
// 通過(guò)指定渠道文件添加渠道信息
java -jar VasDolly.jar put -c channel.txt [源apk地址] [apk輸出目錄](méi)
// 提供了FastMode,生成渠道包時(shí)不進(jìn)行強(qiáng)校驗(yàn),速度可提升10倍以上
java -jar VasDolly.jar put -c channel.txt -f [源apk地址] [apk輸出目錄](méi)

對(duì)齊和簽名

上傳應(yīng)用市場(chǎng)前,APK文件大部分會(huì)被市場(chǎng)要求進(jìn)行加固,無(wú)論是使用騰訊樂(lè)固還是360加固等方式,加固后APK的簽名信息總會(huì)被破壞,所以我們需要對(duì)加固后的APK文件重新進(jìn)行簽名。

配置簽名

首先我們需要準(zhǔn)備好應(yīng)用的簽名信息,該工具支持導(dǎo)入簽名文件,并保存相應(yīng)的StorePass、KeyAlias、KeyPass信息,如下:

當(dāng)選擇APK后,程序會(huì)判斷選擇的APK是否進(jìn)行了簽名,如果沒(méi)有簽名,那么就會(huì)彈窗提醒用戶選擇配置好的簽名文件進(jìn)行簽名,簽名之后才可進(jìn)行多渠道打包的過(guò)程。

注:該功能現(xiàn)已升級(jí),添加簽名文件的時(shí)候綁定包名,選擇apk后會(huì)自動(dòng)獲取到包名然后查找到對(duì)應(yīng)的簽名文件自動(dòng)對(duì)齊簽名處理,無(wú)需手動(dòng)進(jìn)行選擇了。

對(duì)齊

簽名的過(guò)程則需要用到Android SDK中的兩個(gè)文件,以Windows系統(tǒng)為例,一個(gè)是處理對(duì)齊的【build-tools\版本號(hào)\zipalign.exe】文件,另一個(gè)則是用來(lái)簽名的【build-tools\版本號(hào)\lib\apksigner.jar】文件。

我們先看下zipalign工具的官方說(shuō)明:

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歸檔對(duì)齊工具。它確保存檔中所有未壓縮的文件都與文件的開(kāi)頭對(duì)齊。這允許通過(guò)mmap直接訪問(wèn)這些文件,無(wú)需將這些數(shù)據(jù)復(fù)制到RAM中,并減少應(yīng)用程序的內(nèi)存使用。

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文件分發(fā)給用戶之前,應(yīng)使用zipalign優(yōu)化APK文件。如果您使用Android Studio進(jìn)行構(gòu)建,這將自動(dòng)完成。本文檔面向定制構(gòu)建系統(tǒng)的維護(hù)人員。

Google官方現(xiàn)在要求在使用apksigner對(duì)APK文件進(jìn)行簽名前需要先使用zipalign來(lái)優(yōu)化APK文件,具體命令如下,以Windows下的zipalign.exe文件為例:

//對(duì)齊APK
zipalign.exe -p -f -v 4 [源apk路徑] [輸出apk路徑]
//驗(yàn)證APK是否對(duì)齊
zipalign.exe -c -v 4 [源apk路徑]

其他相關(guān)的內(nèi)容可以參閱官網(wǎng) zipalign 。

簽名

當(dāng)APK文件對(duì)齊后,就可以給對(duì)齊后的APK進(jìn)行簽名操作了,簽名的方法有兩種,我們這里單說(shuō)使用--ks選項(xiàng)指定密鑰庫(kù)的方式,具體命令如下:

java -jar apksigner.jar sign 
    --verbose 
    --ks [KeyStore文件路徑] 
    --ks-pass pass:[KeyStorePass]
    --ks-key-alias [KeyAlias]
    --key-pass pass:[KeyPass]
    --out [輸出apk路徑]
    [源apk路徑]

命令本身很簡(jiǎn)單,別搞錯(cuò)參數(shù)就好,尤其是兩個(gè)密碼的參數(shù),后面需要使用【pass:密碼】。輸入密碼這里還支持其他格式,如果有需要請(qǐng)參閱官網(wǎng) apksigner

加固、對(duì)齊、重簽名后,這個(gè)apk就可以進(jìn)行多渠道打包的處理了,然后即可發(fā)布到相關(guān)市場(chǎng)和渠道。

其他內(nèi)容

在項(xiàng)目中還有很多其他的相關(guān)配置,比如發(fā)版的時(shí)候需要對(duì)APP進(jìn)行應(yīng)用內(nèi)的更新通知。那么就需要我們填寫(xiě)發(fā)版的相關(guān)信息,版本名、版本號(hào)、更新日志等等內(nèi)容都需要完善(可根據(jù)APK文件的命名來(lái)獲取部分信息),然后通過(guò)這些信息生成應(yīng)用內(nèi)部更新的SQL語(yǔ)句,發(fā)送釘釘通知給相關(guān)后臺(tái)人員處理。通知這一步又用到了釘釘?shù)腟DK,該工具支持配置釘釘機(jī)器人Webhook地址以及需要艾特的人員信息。

打出來(lái)的這些包都需要統(tǒng)一上傳到云存儲(chǔ)上面,這一步使用了AWS的云存儲(chǔ)SDK,可以配置云存儲(chǔ)桶地址等信息,免去人工手動(dòng)上傳apk的煩惱。上傳完畢后會(huì)根據(jù)文件名生成相應(yīng)的下載鏈接并通知到釘釘群,以便市場(chǎng)人員獲取到渠道最新的推廣鏈接等。

桌面端開(kāi)發(fā)

接下來(lái)就說(shuō)下桌面端的開(kāi)發(fā)過(guò)程,至于Compose MultiPlatform的介紹,請(qǐng)參閱官網(wǎng)地址。本文主要就描述下一些針對(duì)桌面端的相關(guān)需求。

彈窗

關(guān)于彈窗,ComposeDesktop同樣提供了Dialog可組合函數(shù):

@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 */ }

大部分的參數(shù)都可以直接看出他的作用,主要看一下state參數(shù),該參數(shù)可以控制彈窗的位置及大小,例如我們配置一個(gè)在屏幕中央,寬高為500*300dp的彈窗,那么示例代碼如下:

state = DialogState(
            position = WindowPosition(Alignment.Center),
            size = DpSize(500.dp, 300.dp),
        )

不過(guò)這個(gè)彈窗沒(méi)有陰影,如果想添加的話可以?xún)?nèi)部套一層Surface來(lái)做出陰影效果:

Surface(
    modifier = Modifier.fillMaxSize().padding(20.dp),
    elevation = 10.dp,
    shape = RoundedCornerShape(16.dp)
)

文件選擇器

關(guān)于文件選擇器這一塊目前Compose還沒(méi)有專(zhuān)門(mén)的函數(shù),但是我們還是可以使用原有的方案:

javax.swing.JFileChooser

java.awt.FileDialog

個(gè)人還是更偏向于使用JFileChooser,因?yàn)槭褂玫诙N方案的話,在頁(yè)面重組的情況下總是會(huì)莫名的彈出選擇框來(lái)。一個(gè)簡(jiǎn)單的文件選擇器如下所示:

private fun showFileSelector(
    suffixList: Array<String>,
    onFileSelected: (String) -> Unit
) {
    JFileChooser().apply {
        //設(shè)置頁(yè)面風(fēng)格
        try {
            val lookAndFeel = UIManager.getSystemLookAndFeelClassName()
            UIManager.setLookAndFeel(lookAndFeel)
            SwingUtilities.updateComponentTreeUI(this)
        } catch (e: Throwable) {
            e.printStackTrace()
        }
        fileSelectionMode = JFileChooser.FILES_ONLY
        isMultiSelectionEnabled = false
        fileFilter = FileNameExtensionFilter("文件過(guò)濾", *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)
        }
    }
}

該方式在使用的過(guò)程中也有一定的缺陷,就是每次打開(kāi)文件彈窗總是會(huì)卡頓一下,所以后續(xù)也是有了尋找其他高效選擇文件方式的想法。

文件拖拽

選擇文件除了上面的彈窗選擇方式,還有另一種神奇的方式 - 拖拽選擇,本來(lái)也是沒(méi)有頭緒,然而在Slack閑逛的時(shí)候發(fā)現(xiàn)了Jim Sproch推薦了一篇相關(guān)的文章:dev.to/tkuenneth/f… ??赐旰笠彩腔腥淮笪?,但是在Compose Desktop中,window是整個(gè)窗口,如何讓某一個(gè)指定的區(qū)域響應(yīng)我們的文件拖拽事件呢?

還記得在Android上有ComposeView吧,用來(lái)嵌套原來(lái)的那一套View體系。那么在這里我也是采用了類(lèi)似的這么一種方式,實(shí)例一個(gè)空的JPanel控件然后給它安排到window中去。具體位置及大小的設(shè)置呢,在Compose中可以通過(guò) onPlaced(onPlaced: (LayoutCoordinates) -> Unit) 修飾符來(lái)獲取到,示例代碼如下所示:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun 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)
            }
        }
    }
}

實(shí)際運(yùn)行效果如下,個(gè)人感覺(jué)基本還是能達(dá)到目的的:

數(shù)據(jù)的保存

最開(kāi)始的時(shí)候,功能很少,每個(gè)配置的數(shù)據(jù)都是使用了txt文件來(lái)一行行保存,但是到了后來(lái)功能越來(lái)越復(fù)雜,單純的按行來(lái)處理貌似有點(diǎn)捉襟見(jiàn)肘了,所以考慮使用json來(lái)保存復(fù)雜的類(lèi)型數(shù)據(jù)。

json數(shù)據(jù)的處理從原生JSON到FastJson,Gson,Moshi等都已經(jīng)體驗(yàn)過(guò)了,于是乎便采用了之前未使用過(guò)的Jackson。然而不得不說(shuō),就目前為止,jackson是我用過(guò)最簡(jiǎn)潔、優(yōu)雅的一款解析庫(kù)。

假如我有一個(gè)List類(lèi)型的列表數(shù)據(jù),那么當(dāng)我要把這個(gè)數(shù)據(jù)存儲(chǔ)到文件的時(shí)候只需:

jacksonObjectMapper().writeValue(File, List<String>)

而從文件中讀取數(shù)據(jù)也是簡(jiǎn)單的狠?。?/p>

//方式1
val list = jacksonObjectMapper().readValue<List<String>>(jsonFile)
//方式2
val list : List<String> = jacksonObjectMapper().readValue(jsonFile)

這種簡(jiǎn)潔真的是深入我心。繼續(xù)深入了解下Jackson,你會(huì)發(fā)現(xiàn)它的可擴(kuò)展性以及可定制性都很強(qiáng),簡(jiǎn)直相見(jiàn)恨晚啊。之前也是在一個(gè)舒適圈待習(xí)慣了,這次主動(dòng)跳出來(lái)居然有了意想不到的收獲。

但是呢,每個(gè)框架也會(huì)有它自己的注意點(diǎn),比如jackson,屬性命名不可以是is開(kāi)頭,否則序列化等就會(huì)報(bào)錯(cuò)。這點(diǎn)似乎在阿里巴巴JAVA手冊(cè)中好像也有提到,具體原因請(qǐng)大家自行百度(Google)。

資源的拷貝

當(dāng)我們使用[java -jar xxx.jar]命令執(zhí)行jar文件的時(shí)候,需要明確指定 jar文件的地址,但是在Compose Desktop中我們要怎么存放并讀取這個(gè)jar文件呢 ?我們可以從Compose Desktop中讀取并展示圖片的相關(guān)代碼中得到啟發(fā),假如有一個(gè)sample.svg圖標(biāo)文件存放到了項(xiàng)目的 resources 文件夾下,那么我們?cè)谝眠@張圖片的時(shí)候就可以使用:

painterResource("sample.svg")

我們點(diǎn)進(jìn)去這個(gè)方法看下:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun painterResource(
    resourcePath: String
): Painter = painterResource(
    resourcePath,
    ResourceLoader.Default
)
@ExperimentalComposeUiApi
@Composable
fun painterResource(
    resourcePath: String,
    loader: ResourceLoader
): Painter = when (resourcePath.substringAfterLast(".")) {
    "svg" -> rememberSvgResource(resourcePath, loader)
    "xml" -> rememberVectorXmlResource(resourcePath, loader)
    else -> rememberBitmapResource(resourcePath, loader)
}

里面居然有個(gè)ResourceLoader類(lèi),這名字一聽(tīng)就有戲啊,大概率就是我們需要的內(nèi)容,而傳遞的默認(rèn)參數(shù)是ResourceLoader.Default,那么就看下Default的源碼吧:

//==========Resources.desktop.kt文件==========
@ExperimentalComposeUiApi
interface 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
}
@ExperimentalComposeUiApi
class 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類(lèi)==========
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;
}

上述源碼的整個(gè)邏輯基本上就是兩步,根據(jù)資源文件名獲取到資源文件,然后獲取資源文件的輸入流??吹竭@里其實(shí)我們已經(jīng)有兩種方案了:

  • 方案一:直接拿到文件的URL然后獲取到文件的路徑
  • 方案二:根據(jù)文件的輸入流,將文件重新保存到本機(jī)相關(guān)目錄

然而事情并沒(méi)有這么簡(jiǎn)單,如果我們使用方案一,那么在編譯運(yùn)行的時(shí)候完全沒(méi)有問(wèn)題,所有的資源文件會(huì)被保存到【\build\processedResources\jvm】下,此時(shí)我們直接可以通過(guò)文件的URL獲取到文件路徑,然后調(diào)用即可。

但是,當(dāng)我們打包成安裝包后,例如在Windows下使用packageMsi命令打包出msi文件并安裝到電腦上后,運(yùn)行程序,這時(shí)候你就會(huì)發(fā)現(xiàn)資源文件所在的路徑就很奇怪,例如我的工程下是【C:\Program Files\工程名\app\工程名-jvm-1.0-SNAPSHOT-xxxxxx.jar**!/**資源文件名】,也就是說(shuō)所有的資源文件被打包進(jìn)了這個(gè)快照文件,如果此時(shí)直接使用該路徑運(yùn)行java -jar 等命令,那么肯定就會(huì)報(bào)錯(cuò)了。

所以最穩(wěn)妥的方式還是使用方案二,使用ResourceLoader獲取到資源文件流然后重新保存到本機(jī)上的相關(guān)目錄就好了,偽代碼如下:

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環(huán)境下打包Msi格式安裝包的時(shí)候,有一個(gè)downloadWix的Task,該Task涉及到了Wix資源的下載,如下 :

Task :downloadWix Download 點(diǎn)擊下載

在IDEA中下載可能會(huì)非常的緩慢,此時(shí)我們可以復(fù)制上述地址,登上梯子,然后直接去GitHub下載。下載完畢后直接放入【/build/wixToolset】目錄下即可,再次編譯速度就會(huì)起飛了。

總結(jié)

簡(jiǎn)直沒(méi)想到啊,作為一個(gè)Android開(kāi)發(fā)者,現(xiàn)在借助Compose Desktop開(kāi)發(fā)起桌面端居然能這么的輕車(chē)熟路,我對(duì)Compose真是越來(lái)越喜歡了。

另外呢,跳出業(yè)務(wù)這一段時(shí)間來(lái)處理這些東西也讓我對(duì)干預(yù)APK的打包等過(guò)程從理論邁出了實(shí)踐的一步,同時(shí)對(duì)市場(chǎng)和運(yùn)營(yíng)同學(xué)的工作也有了更多了解,通過(guò)該工具幫助其處理了部分重復(fù)機(jī)械式的工作,部門(mén)間的感情也得到了進(jìn)一步的增溫(狗頭滑稽)。

就編到這吧,桌面工具還需要持續(xù)的維護(hù)跟優(yōu)化,基本是面向市場(chǎng)和運(yùn)營(yíng)同事編程了。關(guān)于開(kāi)頭說(shuō)的Jenkins那一套其實(shí)早就寫(xiě)好了,是鄙人少有的萬(wàn)字長(zhǎng)文,但是中間變故太大,一直也沒(méi)發(fā)布出來(lái),接下來(lái)會(huì)重新整理下并發(fā)布,更多關(guān)于ComposeDesktop桌面端APK的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論