当前位置: 首页 > news >正文

淺談Cocos2djs逆向

前言

簡單聊一下cocos2djs手遊的逆向,有任何相關想法歡迎和我討論^^

一些概念

列出一些個人認為比較有用的概念:

  • Cocos遊戲的兩大開發工具分別是CocosCreatorCocosStudio,區別是前者是cocos2djs專用的開發工具,後者則是cocos2d-lua、cocos2d-cpp那些。

  • 使用Cocos Creator 2開發的手遊,生成的關鍵so默認名稱是libcocos2djs.so
  • 使用Cocos Creator 3開發的手遊,生成的關鍵so默認名稱是libcocos.so ( 入口函數非applicationDidFinishLaunching )
  • Cocos Creator在構建時可以選擇是否對.js腳本進行加密&壓縮,而加密算法固定是xxtea,還可以選擇是否使用Zip壓縮

  • libcocos2djs.so裡的AppDelegate::applicationDidFinishLaunching是入口函數,可以從這裡開始進行分析
  • Cocos2djs是Cocos2d-x的一個分支,因此https://github.com/cocos2d/cocos2d-x源碼同樣適用於Cocos2djs

自己寫一個Demo

自己寫一個Demo來分析的好處是能夠快速地判斷某個錯誤是由於被檢測到?還是本來就會如此?

版本信息

嘗試過2.4.2、2.4.6兩個版本,都構建失敗,最終成功的版本信息如下:

  • 編輯器版本:Creator 2.4.13 ( 2系列裡的最高版本,低版本在AS編譯時會報一堆錯誤 )
  • ndk版本:23.1.7779620
  • project/build.gradleclasspath 'com.android.tools.build:gradle:8.0.2'
  • project/gradle/gradle-wrapper.propertiesdistributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip

Cocos Creator基礎用法

由於本人不懂cocos遊戲開發,只好直接用官方的Hello World模板。

首先要設置SDK和NDK路徑

然後構建的參數設置如下,主要需要設置以下兩點:

  • 加密腳本:全都勾上,密鑰用默認的
  • Source Map:保留符號,這樣IDA在打開時才能看到函數名

我使用Cocos Creator能順利構建,但無法編譯,只好改用Android Studio來編譯。

使用Android Studio打開build\jsb-link\frameworks\runtime-src\proj.android-studio,然後就可以按正常AS流程進行編譯

Demo如下所示,在中心輸出了Hello, World!

jsc腳本解密

上述Demo構建中有一個選項是【加密腳本】,它會將js腳本通過xxtea算法加密成.jsc

而遊戲的一些功能就會通過js腳本來實現,因此cocos2djs逆向首要事件就是將.jsc解密,通常.jsc會存放在apk內的assets目錄下

獲取解密key

方法一:從applicationDidFinishLaunching入手

方法二:HOOK

  1. hook set_xxtea_key
// soName: libcocos2djs.so
function hook_jsb_set_xxtea_key(soName) {let set_xxtea_key = Module.findExportByName(soName, "_Z17jsb_set_xxtea_keyRKNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE");Interceptor.attach(set_xxtea_key,{onEnter(args){console.log("xxtea key: ", args[0].readCString())},onLeave(retval){}})
}
  1. hook xxtea_decrypt
function hook_xxtea_decrypt(soName) {let set_xxtea_key = Module.findExportByName(soName, "xxtea_decrypt");Interceptor.attach(set_xxtea_key,{onEnter(args){console.log("xxtea key: ", args[2].readCString())},onLeave(retval){}})
}

python加解密腳本

一次性解密output_dir目錄下所有.jsc,並在input_dir生成與output_dir同樣的目錄結構。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

# pip install xxtea-py

# pip install jsbeautifier

import xxtea

import gzip

import jsbeautifier

import os

KEY = "abdbe980-786e-45"

input_dir = r"cocos2djs_demo\assets" # abs path

output_dir = r"cocos2djs_demo\output" # abs path

def jscDecrypt(data: bytes, needJsBeautifier = True):

    dec = xxtea.decrypt(data, KEY)

    jscode = gzip.decompress(dec).decode()

    if needJsBeautifier:

        return jsbeautifier.beautify(jscode)

    else:

        return jscode

def jscEncrypt(data):

    compress_data = gzip.compress(data.encode())

    enc = xxtea.encrypt(compress_data, KEY)

    return enc

def decryptAll():

    for root, dirs, files in os.walk(input_dir):

         

        # 創建與input_dir一致的結構

        for dir in dirs:

            dir_path = os.path.join(root, dir)

            target_dir = output_dir + dir_path.replace(input_dir, "")

            if not os.path.exists(target_dir):

                os.mkdir(target_dir)

        for file in files:

            file_path = os.path.join(root, file)

        

            if not file.endswith(".jsc"):

                continue

             

            with open(file_path, mode = "rb") as f:

                enc_jsc = f.read()

             

            dec_jscode = jscDecrypt(enc_jsc)

             

            output_file_path = output_dir + file_path.replace(input_dir, "").replace(".jsc", "") + ".js"

            print(output_file_path)

            with open(output_file_path, mode = "w", encoding = "utf-8") as f:

                f.write(dec_jscode)

def decryptOne(path):

    with open(path, mode = "rb") as f:

        enc_jsc = f.read()

     

    dec_jscode = jscDecrypt(enc_jsc, False)

    output_path = path.split(".jsc")[0+ ".js"

    with open(output_path, mode = "w", encoding = "utf-8") as f:

        f.write(dec_jscode)

def encryptOne(path):

    with open(path, mode = "r", encoding = "utf-8") as f:

        jscode = f.read()

    enc_data = jscEncrypt(jscode)

     

    output_path = path.split(".js")[0+ ".jsc"

    with open(output_path, mode = "wb") as f:

        f.write(enc_data)

if __name__ == "__main__":

    decryptAll()

jsc文件的2種讀取方式

為實現對遊戲正常功能的干涉,顯然需要修改遊戲執行的js腳本。而替換.jsc文件是其中一種思路,前提是要找到讀取.jsc文件的地方。

方式一:從apk裡讀取

我自己編譯的Demo就是以這種方式讀取/data/app/XXX/base.apkassets目錄內的.jsc文件。

cocos引擎默認使用xxtea算法來對.jsc等腳本進行加密,因此讀取.jsc的操作定然在xxtea_decrypt之前。

跟cocos2d-x源碼,找使用xxtea_decrypt的地方,可以定位到LuaStack::luaLoadChunksFromZIP

向上跟會發現它的bytes數據是由getDataFromFile函數獲取

繼續跟getDataFromFile的邏輯,它會調用getContents,而getContents裡是調用fopen來打開,但奇怪的是hook fopen卻沒有發現它有打開任何.jsc文件

後來發現調用的並非FileUtils::getContents,而是FileUtilsAndroid::getContents

它其中一個分支是調用libandroid.soAAsset_read來讀取.jsc數據,調用AAssetManager_open來打開.jsc文件。

繼續對AAssetManager_open進行深入分析( 在線源碼 ),目的是找到能夠IO重定向的點:

AAssetManager_open裡調用了AssetManager::open函數

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

// frameworks/base/native/android/asset_manager.cpp

AAsset* AAssetManager_open(AAssetManager* amgr, const char* filename, int mode)

{

    Asset::AccessMode amMode;

    switch (mode) {

    case AASSET_MODE_UNKNOWN:

        amMode = Asset::ACCESS_UNKNOWN;

        break;

    case AASSET_MODE_RANDOM:

        amMode = Asset::ACCESS_RANDOM;

        break;

    case AASSET_MODE_STREAMING:

        amMode = Asset::ACCESS_STREAMING;

        break;

    case AASSET_MODE_BUFFER:

        amMode = Asset::ACCESS_BUFFER;

        break;

    default:

        return NULL;

    }

    AssetManager* mgr = static_cast<AssetManager*>(amgr);

    // here

    Asset* asset = mgr->open(filename, amMode);

    if (asset == NULL) {

        return NULL;

    }

    return new AAsset(asset);

}

AssetManager::open調用openNonAssetInPathLocked

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// frameworks/base/libs/androidfw/AssetManager.cpp

Asset* AssetManager::open(const char* fileName, AccessMode mode)

{

    AutoMutex _l(mLock);

    LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager");

    String8 assetName(kAssetsRoot);

    assetName.appendPath(fileName);

    size_t i = mAssetPaths.size();

    while (i > 0) {

        i--;

        ALOGV("Looking for asset '%s' in '%s'\n",

                assetName.string(), mAssetPaths.itemAt(i).path.string());

        // here

        Asset* pAsset = openNonAssetInPathLocked(assetName.string(), mode, mAssetPaths.itemAt(i));

        if (pAsset != NULL) {

            return pAsset != kExcludedAsset ? pAsset : NULL;

        }

    }

    return NULL;

}

AssetManager::openNonAssetInPathLocked先判斷assets是位於.gz還是.zip內,而.apk.zip基本等價,因此理應會走else分支。

1

奇怪的是當我使用frida hook驗證時,能順利hook到`openAssetFromZipLocked`,卻hook不到`getZipFileLocked`,顯然是不合理的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

// frameworks/base/libs/androidfw/AssetManager.cpp

Asset* AssetManager::openNonAssetInPathLocked(const char* fileName, AccessMode mode,

    const asset_path& ap)

{

    Asset* pAsset = NULL;

    if (ap.type == kFileTypeDirectory) {

        String8 path(ap.path);

        path.appendPath(fileName);

        pAsset = openAssetFromFileLocked(path, mode);

        if (pAsset == NULL) {

            /* try again, this time with ".gz" */

            path.append(".gz");

            pAsset = openAssetFromFileLocked(path, mode);

        }

        if (pAsset != NULL) {

            //printf("FOUND NA '%s' on disk\n", fileName);

            pAsset->setAssetSource(path);

        }

    // run this branch

    else {

        String8 path(fileName);

                // here

        ZipFileRO* pZip = getZipFileLocked(ap);

        if (pZip != NULL) {

            ZipEntryRO entry = pZip->findEntryByName(path.string());

            if (entry != NULL) {

                 

                pAsset = openAssetFromZipLocked(pZip, entry, mode, path);

                pZip->releaseEntry(entry);

            }

        }

        if (pAsset != NULL) {

            pAsset->setAssetSource(

                    createZipSourceNameLocked(ZipSet::getPathName(ap.path.string()), String8(""),

                                                String8(fileName)));

        }

    }

    return pAsset;

}

嘗試繼續跟剛剛hook失敗的AssetManager::getZipFileLocked,它調用的是AssetManager::ZipSet::getZip

1

同樣用frida hook `getZip`,這次成功了,猜測是一些優化移除了`getZipFileLocked`而導致hook 失敗。

1

2

3

4

5

6

7

// frameworks/base/libs/androidfw/AssetManager.cpp

ZipFileRO* AssetManager::getZipFileLocked(const asset_path& ap)

{

    ALOGV("getZipFileLocked() in %p\n"this);

    return mZipSet.getZip(ap.path);

}

ZipSet::getZip會調用SharedZip::getZip,後者直接返回mZipFile

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// frameworks/base/libs/androidfw/AssetManager.cpp

ZipFileRO* AssetManager::ZipSet::getZip(const String8& path)

{

    int idx = getIndex(path);

    sp<SharedZip> zip = mZipFile[idx];

    if (zip == NULL) {

        zip = SharedZip::get(path);

        mZipFile.editItemAt(idx) = zip;

    }

    return zip->getZip();

}

ZipFileRO* AssetManager::SharedZip::getZip()

{

    return mZipFile;

}

尋找mZipFile賦值的地方,最終會找到是由ZipFileRO::open(mPath.string())賦值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// frameworks/base/libs/androidfw/AssetManager.cpp

AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen)

    : mPath(path), mZipFile(NULL), mModWhen(modWhen),

      mResourceTableAsset(NULL), mResourceTable(NULL)

{

    if (kIsDebug) {

        ALOGI("Creating SharedZip %p %s\n"this, (const char*)mPath);

    }

    ALOGV("+++ opening zip '%s'\n", mPath.string());

    // here

    mZipFile = ZipFileRO::open(mPath.string());

    if (mZipFile == NULL) {

        ALOGD("failed to open Zip archive '%s'\n", mPath.string());

    }

}

1

從`frameworks/base/libs/androidfw/Android.bp`可知上述代碼的lib文件是`libandroidfw.so`,位於`/system/lib64/`下。將其pull到本地然後用IDA打開,就能根據IDA所示的函數導出名稱/地址對這些函數進行hook。

方式二:從應用的數據目錄裡讀取

無論是方式一還是方式二,.jsc數據都是通過getDataFromFile獲取。而getDataFromFile裡調用了getContents

1

getDataFromFile -> getContents

在方式一中,我一開始看的是FileUtils::getContents,但其實是FileUtilsAndroid::getContents才對。

只有當fullPath[0] == '/'時才會調用FileUtils::getContents,而FileUtils::getContents會調用fopen來打開.jsc

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// https://github.com/cocos2d/cocos2d-x/blob/76903dee64046c7bfdba50790be283484b4be271/cocos/platform/android/CCFileUtils-android.cpp

FileUtils::Status FileUtilsAndroid::getContents(const std::string& filename, ResizableBuffer* buffer) const

{

    static const std::string apkprefix("assets/");

    if (filename.empty())

        return FileUtils::Status::NotExists;

    string fullPath = fullPathForFilename(filename);

    if (fullPath[0] == '/')

            // here

        return FileUtils::getContents(fullPath, buffer);

         

    // 方式一會走這裡....

}

替換思路

正常來說有以下幾種替換腳本的思路:

  1. 找到讀取.jsc文件的地方進行IO重定向。

  2. 直接進行字節替換,即替換xxtea_decypt解密前的.jsc字節數據,或者替換xxtea_decypt解密後的明文.js腳本。

    這裡的替換是指開闢一片新內存,將新的數據放到這片內存,然後替換指針的指向。

  3. 直接替換apk裡的.jsc,然後重打包apk。

  4. 替換js明文,不是像2那樣開闢一片新內存,而是直接修改原本內存的明文js數據。

經測試後發現只有134是可行的,2會導致APP卡死( 原因不明??? )。

思路一實現

從上述可知第一種.jsc讀取方式會先調用ZipFileRO::open(mPath.string())來打開apk,之後再通過AAssetManager_open來獲取.jsc

hook ZipFileRO::open看看傳入的參數是什麼。

function hook_ZipFile_open(flag) {let ZipFile_open = Module.getExportByName("libandroidfw.so", "_ZN7android9ZipFileRO4openEPKc"); console.log("ZipFile_open: ", ZipFile_open)return Interceptor.attach(ZipFile_open,{onEnter: function (args) {console.log("arg0: ", args[0].readCString());},onLeave: function (retval) {}});
}

可以看到其中一條是當前APK的路徑,顯然assets也是從這裡取的,因此這裡是一個可以嘗試重定向點,先需構造一個fake.apk push 到/data/app/XXX/下,然後hook IO重定向到fake.apk實現替換。

對我自己編譯的Demo而言,無論是以apktool解包&重打包的方式,還是直接解壓縮&重壓縮&手動命名的方式來構建fake.apk都是可行的,但要記得賦予fake.apk最低644的權限。

以下是我使用上述方法在我的Demo中實踐的效果,成功修改中心的字符串。

但感覺這種方式的實用性較低( 什至不如直接重打包… )

思路二嘗試(失敗)

連這樣僅替換指針指向都會導致APP卡死??

function hook_xxtea_decrypt() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "xxtea_decrypt"), {onEnter(args) {let jsc_data = args[0];let size = args[1].toInt32();let key = args[2].readCString();let key_len = args[3].toInt32();this.arg4 = args[4];let target_list = [0x15, 0x43, 0x73];let flag = true;for (let i = 0; i < target_list.length; i++) {if (target_list[i] != Memory.readU8(jsc_data.add(i))) {flag = false;}}this.flag = flag;if (flag) {let new_size = size;let newAddress = Memory.alloc(new_size);Memory.protect(newAddress, new_size, "rwx")Memory.protect(args[0], new_size, "rwx")Memory.writeByteArray(newAddress, jsc_data.readByteArray(new_size))args[0] = newAddress;}},onLeave(retval) {}})}

思路四實現

參考這位大佬的文章可知cocos2djs內置的v8引擎最終通過evalString來執行.jsc解密後的js代碼。

在正式替換前,最好先通過hook evalString的方式保存一份目標js( 因為遊戲的熱更新策略等原因,可能導致evalString執行的js代碼與你從apk裡手動解密.jsc得到的js腳本有所不同 )。

function saveJscode(jscode, path) {var fopenPtr = Module.findExportByName("libc.so", "fopen");var fopen = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);var fclosePtr = Module.findExportByName("libc.so", "fclose");var fclose = new NativeFunction(fclosePtr, 'int', ['pointer']);var fseekPtr = Module.findExportByName("libc.so", "fseek");var fseek = new NativeFunction(fseekPtr, 'int', ['pointer', 'int', 'int']);var ftellPtr = Module.findExportByName("libc.so", "ftell");var ftell = new NativeFunction(ftellPtr, 'int', ['pointer']);var freadPtr = Module.findExportByName("libc.so", "fread");var fread = new NativeFunction(freadPtr, 'int', ['pointer', 'int', 'int', 'pointer']);var fwritePtr = Module.findExportByName("libc.so", "fwrite");var fwrite = new NativeFunction(fwritePtr, 'int', ['pointer', 'int', 'int', 'pointer']);let newPath = Memory.allocUtf8String(path);let openMode = Memory.allocUtf8String('w');let str = Memory.allocUtf8String(jscode);let file = fopen(newPath, openMode);if (file != null) {fwrite(str, jscode.length, 1, file)fclose(file);}return null;
}function hook_evalString() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_"), {onEnter(args) {let path = args[4].readCString();path = path == null ? "" : path;let jscode = args[1];let size = args[2].toInt32();if (path.indexOf("assets/script/index.jsc") != -1) {saveJscode(jscode.readCString(), "/data/data/XXXXXXX/test.js");}}})
}

利用Memory.scan來找到修改的位置

function findReplaceAddr(startAddr, size, pattern) {Memory.scan(startAddr, size, pattern, {onMatch(address, size) {console.log("target offset: ", ptr(address - startAddr))return 'stop';},onComplete() {console.log('Memory.scan() complete');}});
}function hook_evalString() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_"), {onEnter(args) {let path = args[4].readCString();path = path == null ? "" : path;let jscode = args[1];let size = args[2].toInt32();if (path.indexOf("assets/script/index.jsc") != -1) {let pattern = "76 61 72 20 65 20 3D 20 64 2E 50 6C 61 79 65 72 41 74 74 72 69 62 75 74 65 43 6F 6E 66 69 67 2E 67 65 74 44 72 65 61 6D 48 6C 70 65 49 74 65 6D 44 72 6F 70 28 29 2C";findReplaceAddr(jscode, size, pattern);}}})
}

最後以Memory.writeU8來逐字節修改,不用Memory.writeUtf8String的原因是它默認會在最終添加'\0'而導致報錯。

function replaceEvalString(jscode, offset, replaceStr) {for (let i = 0; i < replaceStr.length; i++) {Memory.writeU8(jscode.add(offset + i), replaceStr.charCodeAt(i))}
}// 例:
function cheatAutoChopTree(jscode) {let replaceStr = 'true || "                                 "';replaceEvalString(jscode, 0x3861f6, replaceStr)
}

某砍樹手遊實踐

以某款砍樹遊戲來進行簡單的實踐。

遊戲有自動砍樹的功能,但需要符合一定條件

如何找到對應的邏輯在哪個.jsc中?直接搜字符串就可以。

利用上述替換思路4來修改對應的js判斷邏輯,最終效果:

結語

思路4那種替換手段有大小限制,不能隨意地修改,暫時還未找到能隨意修改的手段,有知道的大佬還請不嗇賜教,有任何想法也歡迎交流^^

後記

在評論區的一位大佬指點下,終於是找到一種更優的替換方案,相比起思路4來說要方便太多了。
最開始時我其實也嘗試過這種直接的js明文替換,但APP會卡死/閃退,現在才發現是frida的api所致,那時在開辟內存空間時使用了Memory.alloc、Memory.allocUtf8String,改成使用libc.so的malloc就不會閃退了,具體為什麼會這樣我也不清楚,看看以後有沒有機會研究下frida的源碼吧^^

相关文章:

淺談Cocos2djs逆向

前言 簡單聊一下cocos2djs手遊的逆向&#xff0c;有任何相關想法歡迎和我討論^^ 一些概念 列出一些個人認為比較有用的概念&#xff1a; Cocos遊戲的兩大開發工具分別是CocosCreator和CocosStudio&#xff0c;區別是前者是cocos2djs專用的開發工具&#xff0c;後者則是coco…...

【ROS2】RViz2加载URDF模型文件

1、RViz2加载URDF模型文件 1)运行RViz2 rviz22)添加组件:RobotModel 3)选择通过文件添加 4)选择URDF文件,此时会报错,需要修改Fixed Frame为map即可 5)因为没有坐标转换,依然会报错,下面尝试解决 2、运行坐标转换节点 1)运行ROS节点:robot_state_publishe...

Unity导入特效,混合模式无效问题

检查spine导出设置与Unity导入设置是否一致 检查Blend Mode Materials是否勾选 检查是否使用导入时产生的对应混合模式的材质&#xff0c;混合模式不适用默认材质 这里选导入时生成的材质...

el-table自定义按钮控制扩展expand

需求&#xff1a;自定义按钮实现表格扩展内容的展开和收起&#xff0c;实现如下&#xff1a; 将type“expand”的表格列的宽度设置为width"1"&#xff0c;让该操作列不展示出来&#xff0c;然后通过ref动态调用组件的内部方法toggleRowExpansion(row, row.expanded)控…...

opencv CV_TM_SQDIFF未定义标识符

opencv CV_TM_SQDIFF未定义标识符 opencv4部分命名发生变换&#xff0c;将CV_WINDOW_AUTOSIZE改为WINDOW_AUTOSIZE&#xff1b;CV_TM_SQDIFF_NORMED改为TM_SQDIFF_NORMED。...

2024acl论文体悟

总结分析归纳 模型架构与训练方法&#xff1a;一些论文关注于改进大语言模型的架构和训练方法&#xff0c;以提高其性能和效率。例如&#xff0c;“Quantized Side Tuning: Fast and Memory-Efficient Tuning of Quantized Large Language Models”提出了一种量化侧调优方法&a…...

【Git原理与使用】版本回退reset 详细介绍、撤销修改、删除文件

目录 一、版本回退 reset 1.1 指令&#xff1a; 1.2 参数说明&#xff1a; 1.3 演示&#xff1a; 二、撤销修改 情况一&#xff1a;对于工作区的代码&#xff0c;还没有 add 情况二&#xff1a;已经 add &#xff0c;但没有 commit 情况三&#xff1a;已经 add &…...

反规范化带来的数据不一致问题的解决方案

在数据库设计中&#xff0c;规范化&#xff08;Normalization&#xff09;和反规范化&#xff08;Denormalization&#xff09;是两个相互对立但又不可或缺的概念。规范化旨在消除数据冗余&#xff0c;确保数据的一致性和准确性&#xff0c;但可能会降低查询效率。相反&#xf…...

【Android】直接使用binder的transact来代替aidl接口

aidl提供了binder调用的封装&#xff0c;有的时候&#xff0c;比如&#xff1a; 1. 懒得使用aidl生成的接口文件&#xff08;确实是懒&#xff0c;Android studio中aidl生成接口文件很方便&#xff09; 2. 服务端的提供者只公开了部分接口出来&#xff0c;只给了调用编号和参…...

Python机器学习笔记(十八、交互特征与多项式特征)

添加原始数据的交互特征&#xff08;interaction feature&#xff09;和多项式特征&#xff08;polynomial feature&#xff09;可以丰富特征表示&#xff0c;特别是对于线性模型。这种特征工程可以用统计建模和许多实际的机器学习应用中。 上一次学习&#xff1a;线性模型对w…...

《跟我学Spring Boot开发》系列文章索引❤(2025.01.09更新)

章节文章名备注第1节Spring Boot&#xff08;1&#xff09;基于Eclipse搭建Spring Boot开发环境环境搭建第2节Spring Boot&#xff08;2&#xff09;解决Maven下载依赖缓慢的问题给火车头提提速第3节Spring Boot&#xff08;3&#xff09;教你手工搭建Spring Boot项目纯手工玩法…...

【AI进化论】 如何让AI帮我们写一个项目系列:将Mysql生成md文档

一、python脚本 下面给出一个简易 Python 脚本示例&#xff0c;演示如何自动获取所有表的结构&#xff0c;并生成一份 Markdown 文件。你可根据自己的需求做修改或使用其他编程语言。 import mysql.connector# ------------------------ # 1. 连接数据库 # -----------------…...

(已开源-AAAI25) RCTrans:雷达相机融合3D目标检测模型

在雷达相机融合三维目标检测中&#xff0c;雷达点云稀疏、噪声较大&#xff0c;在相机雷达融合过程中提出了很多挑战。为了解决这个问题&#xff0c;我们引入了一种新的基于query的检测方法 Radar-Camera Transformer (RCTrans)。具体来说&#xff1a; 首先设计了一个雷达稠密…...

Elasticsearch:在 HNSW 中提前终止以实现更快的近似 KNN 搜索

作者&#xff1a;来自 Elastic Tommaso Teofili 了解如何使用智能提前终止策略让 HNSW 加快 KNN 搜索速度。 在高维空间中高效地找到最近邻的挑战是向量搜索中最重要的挑战之一&#xff0c;特别是当数据集规模增长时。正如我们之前的博客文章中所讨论的&#xff0c;当数据集规模…...

unittest VS pytest

以下是 unittest 和 pytest 框架的对比表格&#xff1a; 特性unittestpytest设计理念基于类的设计&#xff0c;类似于 Java 的 JUnit更简洁&#xff0c;基于函数式编程设计&#xff0c;支持类和函数两种方式测试编写需要继承 unittest.TestCase 类&#xff0c;方法以 test_ 开…...

Tableau数据可视化与仪表盘搭建-基础图表制作

目录 对比分析&#xff1a;比大小 柱状图 条形图 数据钻取 筛选器 热力图 气泡图 变化分析&#xff1a;看趋势 折线图 预测 面积图 关系分布&#xff1a;看位置 散点图 直方图 地图 构成分析&#xff1a;看占比 饼图 树地图 堆积图 对比分析&#xff1a;比大…...

Center Loss 和 ArcFace Loss 笔记

一、Center Loss 1. 定义 Center Loss 旨在最小化类内特征的离散程度&#xff0c;通过约束样本特征与其类别中心之间的距离&#xff0c;提高类内特征的聚合性。 2. 公式 对于样本 xi​ 和其类别yi​&#xff0c;Center Loss 的公式为&#xff1a; xi​: 当前样本的特征向量&…...

3125: 【入门】求1/1+1/2+2/3+3/5+5/8+8/13+13/21……的前n项的和

文章目录 题目描述输入输出样例输入样例输出 题目描述 求1/11/22/33/55/88/1313/2121/34……的前n项的和。 输入 第1行&#xff1a;一个整数n&#xff08;1 < n < 30 &#xff09;。 输出 一行&#xff1a;一个小数&#xff0c;即前n项之和&#xff08;保留3位小数&…...

如何确保获取的淘宝详情页数据的准确性和时效性?

要确保获取的淘宝详情页数据的准确性和时效性&#xff0c;可从以下几个方面着手&#xff1a; 合法合规获取数据 遵守平台规则&#xff1a;在获取淘宝详情页数据之前&#xff0c;务必仔细阅读并严格遵守淘宝平台的使用协议和相关规定。明确哪些数据可以获取、以何种方式获取以及…...

云计算是如何帮助企业实现高可用性的

想象一下&#xff0c;你正在享受一个悠闲的周末&#xff0c;突然接到同事的电话&#xff1a;公司的核心系统宕机了&#xff01;这个场景对很多IT从业者来说并不陌生。但在云计算时代&#xff0c;这样的噩梦正在逐渐远去。 一位前辈告诉我&#xff1a;"在技术世界里&#…...

idea大量爆红问题解决

问题描述 在学习和工作中&#xff0c;idea是程序员不可缺少的一个工具&#xff0c;但是突然在有些时候就会出现大量爆红的问题&#xff0c;发现无法跳转&#xff0c;无论是关机重启或者是替换root都无法解决 就是如上所展示的问题&#xff0c;但是程序依然可以启动。 问题解决…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程&#xff0c;然后使用强化学习的Actor-Critic机制&#xff08;中文译作“知行互动”机制&#xff09;&#xff0c;逐步迭代求解…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)

2025年能源电力系统与流体力学国际会议&#xff08;EPSFD 2025&#xff09;将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会&#xff0c;EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2

每日一言 今天的每一份坚持&#xff0c;都是在为未来积攒底气。 案例&#xff1a;OLED显示一个A 这边观察到一个点&#xff0c;怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 &#xff1a; 如果代码里信号切换太快&#xff08;比如 SDA 刚变&#xff0c;SCL 立刻变&#…...

在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?

uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件&#xff0c;用于在原生应用中加载 HTML 页面&#xff1a; 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...

Python 包管理器 uv 介绍

Python 包管理器 uv 全面介绍 uv 是由 Astral&#xff08;热门工具 Ruff 的开发者&#xff09;推出的下一代高性能 Python 包管理器和构建工具&#xff0c;用 Rust 编写。它旨在解决传统工具&#xff08;如 pip、virtualenv、pip-tools&#xff09;的性能瓶颈&#xff0c;同时…...

Vite中定义@软链接

在webpack中可以直接通过符号表示src路径&#xff0c;但是vite中默认不可以。 如何实现&#xff1a; vite中提供了resolve.alias&#xff1a;通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...

c++第七天 继承与派生2

这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分&#xff1a;派生类构造函数与析构函数 当创建一个派生类对象时&#xff0c;基类成员是如何初始化的&#xff1f; 1.当派生类对象创建的时候&#xff0c;基类成员的初始化顺序 …...