flutter添加7z壓縮支持之assets

1、背景介紹

  • flutter壓縮方式通常使用archive插件,可是根據https://pub.dev/packages/archive的介紹看,僅支持以下方式android

    Zip (Archive)

    Tar (Archive)ios

    ZLib [Inflate decompression]c++

    GZip [Inflate decompression]shell

    BZip2 [decompression]
    描述中沒有對7z的支持,因此只好本身下載源碼編譯導入。async

  • C/C++源碼導入正常是選擇在android或者ios中進行。拿android來講,須要在CMakeList.txt中增長對.cpp和.c的列表,以及include頭文件。若是僅是編譯好以後拿動態連接庫也是能夠的。
  • 在android或ios中導入須要兩個平臺分別操做,相對會複雜一些,所以考慮直接在dart中調用支持。

2、實現方案

  • dart的支持包爲ffi(https://dart.cn/guides/libraries/c-interop),雖然仍是beta版本,可是基本的使用仍是能夠的。
  • 7z的源碼編譯出動態連接庫以後,不多說會再次編譯,因此咱們首次編譯出庫以後,把它打包到assets中,在須要使用的時候,根據對應平臺的ABI,取出對應的so文件使用便可。

3、p7zip源碼

  • 官網下載源碼https://sourceforge.net/projects/p7zip/,將它解壓到建立的flutter項目根路徑的p7zip目錄中。
  • 有關p7zip的源碼結構,你們感興趣的能夠自行找資料瞭解,咱們直接看在Android中編譯的時候,依賴了哪些文件。打開文件p7zip/CPP/ANDROID/7zr/jni/Android.mk,咱們能夠看到全部的-I相關的include目錄和LOCAL_SRC_FILES都是在C和CPP兩個目錄下的,基於代碼庫最小化考慮,能夠把其餘的刪掉。ide

    android
    ios
    lib
    p7zip
        C
        CPP
  • p7zip源碼中入口爲main函數,在p7zip/CPP/7zip/UI/Console/MainAr.cpp中,原型爲函數

    int MY_CDECL main
    (
      #ifndef _WIN32
      int numArgs, char *args[]
      #endif
    );

    根據ffi的相關類型支持和轉換,argv很是很差處理,而且因爲C++命名空間的存在,非extern "C"的函數編譯後的函數名會不同。避免對源碼的入侵,咱們封裝一層,在p7zip目錄下新建p7zip.cppui

  • 僅支持.7z的壓縮的話,咱們使用7zr便可,咱們使用命令方式,文檔在p7zip/DOC/MANUAL/cmdline/index.htm中能夠看到,具體使用以下spa

    # 壓縮
    7zr a 輸出文件名.7z 文件或路徑列表
    
    # 解壓縮
    7zr x 須要解壓文件 -o解壓路徑

    所以,咱們的p7zip.cpp增長一個p7zipShell函數傳入指令,調用main.net

    extern "C" int p7zipShell(char *cmd) {
        int numArgs;
        // 最大支持16個參數
        char temp[16][512] = {0};
        numArgs = parseCmd(cmd, temp);
        char *args[16] = {0};
        for (int i = 0; i < numArgs; ++i) {
            args[i] = temp[i];
        }
        return main(numArgs, args);
    }

    咱們的字符串指令傳入以後,須要解析出參數列表argv,parseCmd就是幹這事的

    static int parseCmd(char *cmd, char argv[16][512]) {
        int size = strlen(cmd);
        int preChar = 0;
        int a = 0;
        int b = 0;
        for (int i = 0; i < size; ++i) {
            char c = cmd[i];
            switch (c) {
            case ' ':
            case '\t':
                if (preChar == 1) {
                    argv[a][b++] = '\0';
                    a++;
                    b = 0;
                    preChar = 0;
                }
                break;
    
            default:
                preChar = 1;
                argv[a][b++] = c;
                break;
            }
        }
    
        if (cmd[size - 1] != ' ' && cmd[size - 1] != '\t') {
            argv[a][b] = '\0';
            a++;
        }
        return a;
    }

    最後再導入頭文件支持和main函數聲明

    #include <string.h>;
    #include "C/7zTypes.h";
    
    extern int MY_CDECL main
    (
      int numArgs, char *args[]
    );

    至此,cpp文件寫完。

  • 咱們將cpp文件加入到Android.mk文件中

    LOCAL_SRC_FILES := \
        ...
        ../../../../p7zip.cpp \

    再將原先編譯成可執行文件改爲動態連接庫

    #include $(BUILD_EXECUTABLE)
    include $(BUILD_SHARED_LIBRARY)

    在打開Application.mk文件,修改要編譯的ABI

    APP_ABI := armeabi-v7a arm64-v8a
    APP_PLATFORM := android-14
  • native完成,啓用ndk編譯

    # 找到你的sdk下的ndk目錄,加入到PATH中
    ndk-build

    編譯完成後,lib生成到p7zip/CPP/ANDROID/7zr/libs中,暫時先記下。

4、dart調用

  • pubspec.yaml中增長so資源

    flutter:
        assets:
        - p7zip/CPP/ANDROID/7zr/libs/arm64-v8a/lib7zr.so
        - p7zip/CPP/ANDROID/7zr/libs/armeabi-v7a/lib7zr.so
  • 新建p7zip.dart,因爲壓縮解壓縮是阻塞式,因此咱們要把指令執行任務放在isolate中

    // 傳入須要壓縮的文件列表,以及壓縮文件的路徑
    Future<String> compress(List<String> files, {String path}) async {
      // 獲取共享庫路徑
      final soPath = await _checkSharedLibrary();
      if (soPath == null) {
        return null;
      }
      ...
      // 文件列表轉化爲字符串
      String filesStr = "";
      files.forEach((element) {
        filesStr += " $element";
      });
    
      // 執行isolate任務
      final receivePort = ReceivePort();
      await Isolate.spawn(_shell, [ receivePort.sendPort, soPath, "7zr a $path $filesStr" ]);
      // 等待任務完成,獲得執行結果,0表示執行成功
      final result = await receivePort.first;
      print("[p7zip] compress: after first result = $result");
      return result == 0 ? path : null;
    }
  • isolate任務,調用p7zipShell函數

    // dart <=> native函數原型定義
    typedef _NativeP7zipShell = Int32 Function(Pointer<Int8>);
    typedef _DartP7zipShell = int Function(Pointer<Int8>);
    
    void _shell(List argv) {
      // 傳遞進來的參數列表轉化
      final SendPort sendPort = argv[0];
      final String soPath = argv[1];
      final String cmd = argv[2];
      // 打開動態連接庫
      final p7zip = DynamicLibrary.open(soPath);
      if (p7zip == null) {
        return null;
      }
      // 獲得native中的p7zipShell函數
      final _DartP7zipShell p7zipShell = p7zip.lookup<NativeFunction<_NativeP7zipShell>>("p7zipShell")
        .asFunction();
      if (p7zipShell == null) {
        return null;
      }
      // 把dart的String轉化爲c++中的char *
      final cstr = _toNativeStr(cmd);  
      final result = p7zipShell.call(cstr);
      // 通知主線程任務執行結果
      sendPort.send(result);
    }
  • 核心的_checkSharedLibrary把動態連接庫從assets中取出來,拷貝到cache目錄下。

    Future<String> _checkSharedLibrary() async {
      // 把so放在臨時路徑中
      final dir = await getTemporaryDirectory();
      if (dir == null) {
        return null;
      }
      final libFile = File(dir.path + "/lib7zr.so");
      final exist = await libFile.exists();
      if (exist) {
        return libFile.path;
      }
      // 獲取系統
      if (Platform.isAndroid) {
        // 獲取abi
        final devicePlugin = DeviceInfoPlugin();
        final deviceInfo = await devicePlugin.androidInfo;
        if (deviceInfo == null) {
          return null;
        }
        // 這裏的soResource就是前面p7zip編譯生成的庫路徑
        String soResource = "p7zip/CPP/ANDROID/7zr/libs/armeabi-v7a/lib7zr.so";
        final support64 = deviceInfo.supported64BitAbis;
        if (support64 != null && support64.length > 0) {
          soResource = "p7zip/CPP/ANDROID/7zr/libs/arm64-v8a/lib7zr.so";
        }
        // 從rootBundle加載出assets資源
        final data = await rootBundle.load(soResource);
        if (data == null) {
          return null;
        }
        // 建立文件
        final createFile = await libFile.create();
        if (createFile == null) {
          return null;
        }
        // 文件以寫方式打開
        final writeFile = await createFile.open(mode: FileMode.write);
        if (writeFile == null) {
          return null;
        }
        // 拷貝數據
        await writeFile.writeFrom(Uint8List.view(data.buffer));
        return libFile.path;
      } else {
        // ios平臺的是用dylib
        ...
      }
    }
  • 最後,在其餘dart文件中使用

    final path = await p7zip.compress(files, path: "/sdcard/Download/test.7z");

    至於解壓縮的的dart部分和compress是極爲類似的,你們可自行編寫。

5、結語

  • 由於工做是作android設備的,基本不多接觸跨平臺,此次是第一次在項目中使用flutter,對於dart的界面搭建用來爽的不要不要的,可是確實仍是不熟悉,好比光isolate就研究了很久,還不知道這種用法是否是常規的,有什麼不合理的你們能夠留言,感謝。
  • 過程當中固然碰到不少問題,關鍵是網上flutter的相關案例還不夠完善,須要本身開腦洞來摸索,感謝多篇文章中不知名的網友。因爲是過後再寫的文章,就無法一一列出了。
  • 不只僅7z,之後有其餘的開源庫須要導入的時候均可以相似的這麼幹。
  • 我知道不貼源碼地址是可恥的,就是懶,有空時再整理上傳吧,就這樣。
相關文章
相關標籤/搜索