為 Compose MultiPlatform 添加 C/C++ 支持(3):實戰 Desktop、Android、iOS 調用同一個 C/C++ 代碼


theme: serene-rose

前言

在本系列的前兩篇文章中我們已經學會了如何在 kotlin native 平臺(iOS)使用 cinterop 調用 C/C++ 代碼。以及在 jvm 平臺(Android、Desktop)使用 jni 調用 C/C++ 代碼,并且知道了如何自動編譯 Android 端使用的 jni 代碼給 Desktop 使用。

那么,我們還猶豫什么呢,是時候把它們都組合在一起,完成真正的 Compose MultiPlatform 全平臺調用 C/C++ 了。

在本文中我們將以我去年使用 Compose 寫的安卓端的一個簡單的小游戲 demo 舉例。(終于換項目舉例了,哈哈哈)

項目地址:life-game-compose 。

準備工作

在正式開始之前,我們需要先簡單的了解一下這個項目,可以看我之前寫的這兩篇介紹文章:基于 jetpack compose,使用MVI架構+自定義布局實現的康威生命游戲 、使用compose實現康威生命游戲之二:我是如何將計算速度縮減將近十倍的 。

簡單介紹一下就是這是個模擬"細胞演化"的小游戲,隨著時間的推移,其中的細胞會因為周圍的環境(周圍細胞數量)而死亡或繼續存活。

而在我這個 demo 中,關于細胞狀態的邏輯運算使用的是 C 語言寫的,因為這部分是動畫的幀間計算,對運算速度的要求比較高,使用 C 來實現速度將大幅提升。

當然,這只是我去年測試時的情況,今年當我重新測試時,發現其實在一般設備上直接使用 kotlin 運算的速度和調用 C 運算的速度實際上相差無幾,甚至使用 kotlin 比調用 C 更快。

這并不是因為 kotlin 速度真的比 C 快,只是因為如果想要調用 C 的話,需要首先將 kotlin 的數據類型轉為 C 中可用的數據類型,時間大多數耗在了數據類型轉換上。說句不好聽的,有轉數據的這個功夫,我直接拿 kotlin 算都已經算出結果來了。

也許你也會說,那我不轉數據可以嗎?

欸,還真可以。只是對于我們這篇文章要講的情況不可以,因為我們想要實現的是同一套 C 代碼,提供給不同的平臺使用,顯然 cinterop 和 jni 對于 kotlin 的數據類型與 C 之間的數據類型映射不一樣,因此我們為了實現通用性,只能在將數據傳遞給 C 之前先轉為通用的數據類型。

(以上表述比較片面,也不太準確,后文會有詳細的解釋)

上面說了這么多,只是想疊個 buff ,那就是我們這里使用的這個項目用于舉例并不是很恰當,因為這反而會讓程序的性能下降。

但是用于理解如何實現在 KMP 中調用 C/C++ 還是非常有用的,權當是拋磚引玉。

最后,上面提到過,原本這個項目只是個 Android 項目,所以在開始我們今天的修改之前,需要先把它移植為 KMP 項目,這里就不再贅述了,因為之前很多篇文章都有說過怎么移植,需要的可以自行翻閱我之前的歷史文章。

開始修改

項目整體結構

開始之前,我們先來看看修改完成之后的項目整體結構,需要重點關注的是圖中圈起來的部分:

1.jpg

其中 圈1 是項目的 nativelib 模塊,這個模塊實際上是一個 Android native library 模塊,但是我們這里把核心運算代碼直接放到這個模塊里面了。

game.h 文件即我們需要的算法的具體實現。

nativelib.cpp 是提供給 jni (Android、Desktop)調用的入口函數。

圈 2 是 Desktop 實現調用上述 C++ 代碼編譯成的二進制庫的 kotlin 代碼。

圈 3 是 iOS 使用 cinterop 實現調用上述 C++ 算法的入口函數。

接下來,我們挨個看它們的具體代碼和需要注意的地方。

核心算法實現

nativelib 模塊下的 game.h 文件是整個項目的細胞狀態運算核心代碼,所有平臺最終都是調用其中的 updateStep(int **board, int len1, int len2) 函數實現對細胞狀態的計算,代碼如下:

#ifndef LIFEGAME_GAME_H
#define LIFEGAME_GAME_H/*
* 該算法來源如下:
*
* 作者:Time-Limit
* 鏈接:https://leetcode.cn/problems/game-of-life/solution/c-wei-yun-suan-yuan-di-cao-zuo-ji-bai-shuang-bai-b/
* 來源:力扣(LeetCode)
* 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
*/
void updateStep(int **board, int len1, int len2) {int dx[] = {-1,  0,  1, -1, 1, -1, 0, 1};int dy[] = {-1, -1, -1,  0, 0,  1, 1, 1};for(int i = 0; i < len1; i++) {for(int j = 0 ; j < len2; j++) {int sum = 0;for(int k = 0; k < 8; k++) {int nx = i + dx[k];int ny = j + dy[k];if(nx >= 0 && nx < len1 && ny >= 0 && ny < len2) {sum += (board[nx][ny]&1); // 只累加最低位}}if(board[i][j] == 1) {if(sum == 2 || sum == 3) {board[i][j] |= 2;  // 使用第二個bit標記是否存活}} else {if(sum == 3) {board[i][j] |= 2; // 使用第二個bit標記是否存活}}}}for(int i = 0; i < len1; i++) {for(int j = 0; j < len2 ; j++) {board[i][j] >>= 1; //右移一位,用第二bit覆蓋第一個bit。}}
}#endif //LIFEGAME_GAME_H

可以看到,這是個純粹的 C 代碼,沒有參雜任何涉及到平臺相關的東西,因此它也是我們的共享代碼部分。

具體的運算邏輯這里就不說了,我們只需要知道我們項目中的細胞狀態使用一個二維整數數組存放,數組索引即為游戲中對應位置細胞的狀態,其中數組內容為 0 時表示該位置細胞已死亡,1 表示該位置細胞處于存活狀態。

所以該函數接收三個參數 boardlen1len2 ,其中 board 即表示當前狀態的二維數組,該函數會在運算中直接修改該數組的內容;len1len2 分別表示數組的行長度和列長度。

了解了核心運算函數,接下來我們看看各個平臺如何使用它。

jvm 使用 jni 調用核心算法

在 Android 和 Desktop 將使用 jni 調用上一小節中的 updateStep 函數。

這里我們的 Android 和 Desktop 雖然編譯方式不同,但是依舊可以直接復用同一個入口函數,即 nativelib 模塊中的 nativelib.cpp 文件的 Java_com_equationl_nativelib_NativeLib_stepUpdate 函數。

該函數簽名如下:

extern "C" JNIEXPORT jobjectArray JNICALL
Java_com_equationl_nativelib_NativeLib_stepUpdate(JNIEnv* env,jobject,jobjectArray lifeList)

可以看到,該函數接收 3 個參數: JNIEnv* envjobjectjobjectArray lifeList ,其中前兩個是 jni 的固定參數,最后一個 jobjectArray 類型的參數 lifeList 才是我們實際需要傳遞的參數,該函數返回的數據類型也是一個 jobjectArray

接下來,我們看下在 kotlin 中調用這個函數的簽名:

external fun stepUpdate(lifeList: Array<IntArray>): Array<IntArray>

可以看到,C 中的 jobjectArray 被映射為了 kotlin 中的 Array<IntArray> 類型。

事實上,jobjectArray 并不是 C 的類型,而是 jni 定義的一個自定義數據類型:

class _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
typedef _jobjectArray*  jobjectArray;

而我們的核心運算代碼使用的是純粹的 C 類型,所以這里就需要對數據類型進行一個轉換。

我們來看 Java_com_equationl_nativelib_NativeLib_stepUpdate 函數的完整代碼:

#include <jni.h>
#include <string>#include <valarray>#include "game.h"extern "C" JNIEXPORT jobjectArray JNICALL
Java_com_equationl_nativelib_NativeLib_stepUpdate(JNIEnv* env,jobject,jobjectArray lifeList) {// jni 數據轉為 c 數據int len1 = env -> GetArrayLength(lifeList);auto dim =  (jintArray)env->GetObjectArrayElement(lifeList, 0);int len2 = env -> GetArrayLength(dim);int **board;board = new int *[len1];for(int i=0; i<len1; ++i){auto oneDim = (jintArray)env->GetObjectArrayElement(lifeList, i);jint *element = env->GetIntArrayElements(oneDim, JNI_FALSE);board[i] = new int [len2];for(int j=0; j<len2; ++j) {board[i][j]= element[j];}// 釋放數組env->ReleaseIntArrayElements(oneDim, element, JNI_ABORT);// 釋放引用env->DeleteLocalRef(oneDim);}// 實際的處理邏輯updateStep(board, len1, len2);// C 數據轉回 jni 數據jclass cls = env->FindClass("[I");jintArray iniVal = env->NewIntArray(len2);jobjectArray result = env->NewObjectArray(len1, cls, iniVal);for (int i = 0; i < len1; i++){jintArray inner = env->NewIntArray(len2);env->SetIntArrayRegion(inner, 0, len2, (jint*)board[i]);env->SetObjectArrayElement(result, i, inner);env->DeleteLocalRef(inner);}return result;
}

可以看到,在這個代碼中,僅僅只有一句 updateStep(board, len1, len2); 是真正的運算代碼,而剩余的代碼都只是在做數據轉換。

其實這里如果我們不是為了能夠復用運算邏輯代碼,我們完全不需要轉換數據,直接在運算時使用 jni 自定義的數據類型就可以了。

我們來看這里的轉換代碼,有一點需要額外注意的是,在開頭將 jobjectArray 轉為 int **board 時,有兩行我添加了注釋的代碼:

// 釋放數組
// ① 模式 0 : 刷新 Java 數組 , 釋放 C/C++ 數組
// ② 模式 1 ( JNI_COMMIT ) : 刷新 Java 數組 , 不釋放 C/C ++ 數組
// ③ 模式 2 ( JNI_ABORT ) : 不刷新 Java 數組 , 釋放 C/C++ 數組
env->ReleaseIntArrayElements(oneDim, element, JNI_ABORT);
// 釋放引用
env->DeleteLocalRef(oneDim);

這兩行代碼簡單理解就是在我們已經完成了將 kotlin 中傳過來的 jobjectArray lifeList 復制到了 C 的 int **board 中后就釋放掉 lifeList 的內存。

這里一開始我不小心把釋放內存的代碼寫到了復制完成之前,也就是寫成了:

env->ReleaseIntArrayElements(oneDim, element, JNI_ABORT);
env->DeleteLocalRef(oneDim);
board[i] = new int [len2];
for(int j=0; j<len2; ++j) {board[i][j]= element[j];
}

而我在寫這段代碼時使用的是 Windows 系統,所以我當時并沒有發現不妥,運行代碼也沒有報錯,運行結果也符合預期。

只是當我在 macOS 上測試時卻發現,無論我傳值是什么,返回結果始終為全是 0 的數組。

為此我還調試了很久,最終在 B 大佬的幫助下才發現原來是我把釋放內存寫在了復制完成之前。

那么問題來了,為什么同樣的代碼在 Windows 上運行卻沒有任何問題?

我們猜測可能是 Windows 和 macOS 的內存回收策略不同,在 macOS 回收更為激進,所以在我調用釋放內存后,它的內存就被立即回收了,而 Windows 回收沒有這么激進,所以甚至能允許我復制完了都還沒有被回收。

記住這個內存回收的問題,在后面我們還會被這個坑一次。

kotlin native 使用 cinterop 調用核心算法

在 iOS 中我們將使用 cinterop 調用核心運算函數 updateStep()

在正式開始之前,我們插個題外話,我們先寫一個更簡單的 demo 來演示一下上文中提到過的內存回收策略問題的坑。

看下面的代碼,

C:

#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDEDint** get_message(int** name, int row, int col) {for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {printf("current value from c: name[%d][%d]=%d\n", i, j, name[i][j]);}}// 隨便改一個值name[1][1] = 114514;return name;
}#endif

kt:

@OptIn(ExperimentalForeignApi::class)
fun main() {println("Hello, Kotlin/Native!")val list1 = listOf(intArrayOf(0, 1, 2), intArrayOf(4, 5, 6))val row = list1.sizeval col = list1[0].sizeval passList  = list1.map { it.pin() }val list2 = passList.map { it.addressOf(0) }val newList = get_message(list2.toCValues(), row, col)for (i in 0 until row) {for (j in 0 until col) {println("current value from kotlin: result[$i][$j] = ${newList?.get(i)?.get(j)}")}}
}

無獎競猜,將上述代碼在 Desktop native 中運行,輸出結果是什么?

哈哈,不賣關子了,在 Windows 上運行輸出如下:

2.jpg

在 macOS 中輸出如下:

4.png

顯然,和我們上文說的內存回收策略有關。

我們先來看以上的 kt 代碼,為了把 kt 的數據傳遞給 C ,我們使用:

val passList  = list1.map { it.pin() }val list2 = passList.map { it.addressOf(0) }val newList = get_message(list2.toCValues(), row, col)

首先使用 pin() 函數將 kt 的 Array 中的 IntArray 地址固定以方便傳遞給 C 使用,然后使用 addressOf(0) 獲取到 IntArray 第一個元素的地址,最后使用 toCValues() 將其轉為指針引用傳遞給 C 的 get_message() 函數。

咋一看好像沒有問題是吧?

那么,我們來看看文檔中關于 toCValues() 的解釋:

3.jpg

重點在于第一段最后一句 In this case the sequence is passed “by value”, i.e., the C function receives the pointer to the temporary copy of that sequence, which is valid only until the function returns.

換句話說就是,我們在上面代碼中傳遞給 C 的數據只是一個拷貝的“副本”,并且這個副本數據的有效期只有函數的運行時間這么短,只要函數運行結束,這個“副本”就會被銷毀。

這就不難解釋為什么在 macOS 上這段代碼運行結果會是這樣,至于為什么同樣的代碼在 Windwos 上運行沒有出現問題,我想還是和上一節說的原因一樣,就是 Windows 和 macOS 的內存回收策略不同,在 Windows 上內存回收沒有 macOS 激進,所以得以在數據本應該已經被回收了還能繼續使用。

也就是說,其實我們上面代碼的寫法是錯誤的,雖然可以在 Windows 上運行,但是也是極其不可靠的,保不準數據量大時、內存緊張時、甚至在不知道的情況下隨機就會出現問題。

那么,怎么解決這個問題呢?根據文檔,我們需要使用 nativeHeap.alloc() 或者 nativeHeap.allocArray() 在 kotlin 代碼中事先分配一段內存,然后將該內存指針傳給 C 使用,此時這個內存就不會被自動釋放,需要我們手動釋放 nativeHeap.free()

當然,也可以直接使用 memScoped { ... } 語句,在該語句中分配的內存會自動在該語句結束時釋放。

現在,讓我們回到我們的項目中來。

首先看看 iOS 調用核心運算函數的入口 sahred 模塊下的 iosMain 包中的 /nativeinterop/cinterop/nativelib.h 文件。

在該文件使用 #include "../../../../../../nativelib/src/main/cpp/game.h" 引入了 nativelib 模塊的核心運算文件 game.h

然后定義了入口函數: int* update(int** board, int row, int col, int* newList)

可以看到不同于 jni 的入口函數,它多了幾個參數:rowcolnewList

rowcolboard 的行和列數量,因為這里傳遞給 C 的數組是指針的形式,所以不好確定長度,索性直接從 kotlin 傳過來,而 newList 則是我們在 kotlin 中分配的內存地址的指針,用于將運算完成的結果存入。

這里我將返回類型寫為了 int* 其實這里可以不用返回值,在 kotlin 直接使用傳給它的 newList 指針即可,但是我只是為了保持一致性,所以把 newList 又返回回去了。

另外,這里我的返回值 newList 不使用二維數組而是使用一維數組是因為返回二維數組有點麻煩,索性轉成一維來返回了。

查看 nativelib.h 函數的完整代碼如下:

#include "../../../../../../nativelib/src/main/cpp/game.h"#ifndef LIB2_H_GAME
#define LIB2_H_GAMEint* update(int** board, int row, int col, int* newList) {updateStep(board, row, col);// 將結果轉為一維數組傳回for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {int value = board[i][j];int index = i * col + j;newList[index] = value;}}return newList;
}#endif

對了,上述代碼還有個問題,就是我在將二維數組轉為一維時直接遍歷了整個數組來轉,這樣效率非常低,完全可以直接使用 memcpy批量復制內存數據,這樣會快的多。

接下來,我們來看一下 iOS 端調用這個函數的 kotlin 代碼,我們在 iosMain 包中定義了一個函數 stepUpdateNative

@OptIn(ExperimentalForeignApi::class, ExperimentalForeignApi::class)
fun stepUpdateNative(sourceData: Array<IntArray>): Array<IntArray> {val row = sourceData.sizeval col = sourceData[0].sizeval list1 = sourceData.map { it.pin() }val passList = list1.map { it.addressOf(0) }val result = mutableListOf<IntArray>()memScoped {val arg = allocArray<IntVar>(row * col)val resultNative = update(passList.toCValues(), row, col, arg)for (i in 0 until row) {val line = IntArray(col)for (j in 0 until col) {val index = i * col + jline[j] = resultNative!![index]// println("current value from kotlin: result[$index] = ${resultNative?.get(index)}")}result.add(line)}}return result.toTypedArray()
}

代碼很簡單,重點在于中間:

memScoped {val arg = allocArray<IntVar>(row * col)val resultNative = update(passList.toCValues(), row, col, arg)
}

我們在 memScoped 語句中使用 allocArray<IntVar>(row * col) 分配了一塊類型為 IntVar 長度為 row * col 的數組空間,然后將其作為 C 函數 update 的參數傳入。

返回值 resultNative 即 C 函數運算結束后得到的一維數組結果,所以我們在后面遍歷了一遍將其轉回二維數組:

for (i in 0 until row) {val line = IntArray(col)for (j in 0 until col) {val index = i * col + jline[j] = resultNative!![index]// println("current value from kotlin: result[$index] = ${resultNative?.get(index)}")}result.add(line)
}

簡單總結一下

通過上面兩個小節的講解,相信讀者也看出問題來了,這時候如果我再說調用 C 的運算速度實際上比直接使用 kotlin 還慢讀者應該也恍然大悟知道什么原因了吧。

其實并不是 C 本身慢,而在于我為了在調用 C 的同時復用 C 代碼,做了大量的數據轉換工作,而每次數據轉換的代價都是極其昂貴的,這就導致運算時間反而相比于直接使用 kt 計算還要慢了。

這也是我說的使用這個項目舉例是不恰當的原因,因為這就相當于為了這碗醋還特意去大張旗鼓的包了頓餃子一樣。

實際上,這系列文章只是為了說明如何在 kmp 中調用 C 代碼而不需要每個平臺都單獨寫一套適配代碼。

或許在我舉的這個例子中小題大做了,但是如果是調用第三方的優秀 C 項目,例如 FFmpeg ,那就是收益遠大于付出。

不管怎么說,現在我們已經在所有平臺中復用了同一套 C 代碼,并且編寫好了相應的調用函數,接下來只需要按照我前兩篇文章的步驟依次將其接入我們的 KMP 項目中即可。

文章中有沒有說到的地方各位讀者也可以直接看我的項目代碼:life-game-compose

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/211455.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/211455.shtml
英文地址,請注明出處:http://en.pswp.cn/news/211455.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Git 五分鐘教程速度入門

Git 五分鐘教程速度入門 分類 編程技術 許多人認為 Git 太混亂&#xff0c;或認為它是一種復雜的版本控制系統&#xff0c;其實不然&#xff0c;這篇文章有助于大家快速上手使用 Git。 入門 使用Git前&#xff0c;需要先建立一個倉庫(repository)。您可以使用一個已經存在的…

Win10操作系統安裝Python

1 Python解釋器下載 1.1 安裝環境 Windows 10 專業工作站版22H2 python-3.9.6-amd64.exe 1.2 下載地址 Python官網&#xff1a;Welcome to Python.org Python鏡像&#xff1a;CNPM Binaries Mirror 2 Python解釋器安裝 2.1 Install Python 3.9.6 (64-bit)界面 雙擊運行下…

鴻蒙開發組件之list

1、鴻蒙中的list作為可滑動列表功能&#xff0c;初始化方式是 List({space: 10}){ForEach(arr, item > {ListItem() {//列表單個Item組件}})} 其中&#xff0c;List中的space可以設置兩個ListItem組件的間距 List中是一個ForEach&#xff0c;需要注意的是item要返回的是L…

【數據結構】面試OJ題———棧|隊列|互相實現|循環隊列|括號匹配

目錄 1. 有效的括號 思路&#xff1a; 2.用隊列實現棧 思路&#xff1a; 3.用棧實現隊列 思路&#xff1a; 4.設計循環隊列 思路&#xff1a; 1. 有效的括號 20. 有效的括號 - 力扣&#xff08;LeetCode&#xff09; 給定一個只包括 (&#xff0c;)&#xff0c;{&…

Hive SQL間隔連續問題

問題引入 下面是某游戲公司記錄的用戶每日登錄數據, 計算每個用戶最大的連續登錄天數&#xff0c;定義連續登錄時可以間隔一天。舉例&#xff1a;如果一個用戶在 1,3,5,6,9 登錄了游戲&#xff0c;則視為連續 6 天登錄。 id dt1001 2021-12-121002 2021-12-12…

visual studio code 好用的插件

vscode-icons Better comments 該插件對不同類型的注釋會附加了不同的顏色&#xff0c;更加方便區分&#xff0c;幫助我們在代碼中創建更人性化的注釋。 Error Lens Error Lens插件是一款可以檢測你編寫的代碼的語法錯誤&#xff0c;并且會顯示出對語法錯誤的診斷信息…

USB的高速速率是如何確定的?

從全局說起。先說host對dev的插入檢測。由于dev插入到host&#xff0c;導致為0的D和D-線突然有了電平變化&#xff0c;有且只有一根線的電平會變。在高速和全速模式下&#xff0c;D線會被拉高&#xff1b;在低速模式下D-線會被拉高。同時&#xff0c;host會對插入的dev進行消抖…

RCNN 學習

RCNN算法流程 RCNN算法流程可分為4個步驟 一張圖像生成1K~2K個候選區域&#xff08;使用Selective Search方法&#xff09;對每個候選區域&#xff0c;使用深度網絡圖特征特征送入每一類的SVM分類器&#xff0c;判別是否屬于該類使用回歸期器細修正候選框位置 1.候選區域的生…

【星海隨筆】Prometheus(一)

注&#xff1a;Pagerduty作為報警系統&#xff0c;出鏡率很高。 雖然收費&#xff0c;但對于企業來說很便宜。 一個月幾十美金 不太支持中文&#xff0c;主要是語音方面。 Prometheus 查詢語句 &#xff0c; 基于數學運算模式的監控查詢 我們計算一下一天多少秒 1 * 24 * 60 *…

ChatGPT是科學還是藝術?

OpenAI最近談到GPT4變懶的問題&#xff0c;說“它更像是多人共同參與的藝術創作”&#xff0c;那到底大模型是科學還是藝術&#xff1f;

公式識別任務各個鏈條全部打通

目錄 引言公式識別任務是什么&#xff1f;公式識別任務解決方案初探使用建議寫在最后 引言 隨著LaTeX-OCR模型轉換問題的解決&#xff0c;公式識別任務中各個鏈條已經全部打通。小伙伴們可以放開膀子干了。 解決業界問題的方案&#xff0c;并不是單獨訓練一個模型就完事了&am…

如何確認網站是否有漏洞,如何找出網站存在的漏洞,找到漏洞該如何處理

如何確認網站或者服務器是否有漏洞 判斷一個網站是否是存在漏洞的方法&#xff1a; 1.可以借助德迅云安全漏洞掃描功能來檢查漏洞。 2.打開德迅云安全首頁&#xff0c;點擊最上面導航欄中的“安全產品”。 3.滑到“漏洞掃描”&#xff0c;選擇“產品價格”服務。 4.選擇您需…

【力扣】141和142環形鏈表

141.環形鏈表 法一&#xff1a;快慢指針 思路&#xff1a; 用兩個指針slow,fast,后者能比前者多走一步路&#xff0c;那判斷是不是有環&#xff0c;只需要判斷是否會相遇。 就是有一個能比烏龜跑2倍快的兔子&#xff0c;兩小只都在有環的路上跑&#xff0c;那是不是肯定會相…

golang開發之個微機器人的二次開發

簡要描述&#xff1a; 下載消息中的文件 請求URL&#xff1a; http://域名地址/getMsgFile 請求方式&#xff1a; POST 請求頭Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 參數&#xff1a; 參數名必選類型…

java基礎之TreeMap詳解

TreeMap詳解 TreeMap是Map接口的一個實現類&#xff0c;底層基于紅黑樹的實現&#xff0c;按照key的順序存儲 TreeMap 從繼承結構可以看到TreeMap除了繼承了AbstractMap類&#xff0c;還實現了NavigableMap接口&#xff0c;而NavigableMap接口是繼承自SortedMap接口的&#xff…

使用Vue3+Typescript手寫一個日歷簽到組件

設計理念 昨天寫了個簡單美觀的日歷簽到組件&#xff0c;使用的是Vue3TypeScript&#xff0c;大概邏輯是先找到本月份第一天是周幾&#xff0c;然后開始填充月份日期&#xff1a;weeksArray:[[]]:之后渲染到表格中&#xff0c;對于簽到事件觸發則先判斷是否是今天且還未沒有簽…

【PyTorch】模型訓練過程優化分析

文章目錄 1. 模型訓練過程劃分1.1. 定義過程1.1.1. 全局參數設置1.1.2. 模型定義 1.2. 數據集加載過程1.2.1. Dataset類&#xff1a;創建數據集1.2.2. Dataloader類&#xff1a;加載數據集 1.3. 訓練循環 2. 模型訓練過程優化的總體思路2.1. 提升數據從硬盤轉移到CPU內存的效率…

SPRD Android 13 需要在設置--顯示--鎖定屏幕--雙行時鐘--<關閉>

開始去改默認值沒生效 --- a/frameworks/base/packages/SettingsProvider/res/values/defaults.xml +++ b/frameworks/base/packages/SettingsProvider/res/values/defaults.xml @@ -336,4 +336,6 @@<integer name="def_navigation_bar_config">0</integer…

西南科技大學數字電子技術實驗三(MSI邏輯器件設計組合邏輯電路及FPGA的實現)FPGA部分

一、實驗目的 進一步掌握MIS(中規模集成電路)設計方法。通過用MIS譯碼器、數據選擇器實現電路功能,熟悉它們的應用。進一步學習如何記錄實驗中遇到的問題及解決方法。二、實驗原理 1、4位奇偶校驗器 Y=S7i=0DiMi D0=D3=D5=D6=D D1=D2=D4=D7= `D 2、組合邏輯電路 F=A`B C …

面試計算機網絡八股文五問五答第二期

面試計算機網絡八股文五問五答第二期 作者&#xff1a;程序員小白條&#xff0c;個人博客 相信看了本文后&#xff0c;對你的面試是有一定幫助的&#xff01; ?點贊?收藏?不迷路&#xff01;? 1.OSI七層協議&#xff1f; 2. TCP和UDP傳輸協議的區別&#xff1f; TCP是可…