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,之后有其余的开源库须要导入的时候均可以相似的这么干。
  • 我知道不贴源码地址是可耻的,就是懒,有空时再整理上传吧,就这样。
相关文章
相关标签/搜索