flutter添加7z压缩支持之assets

一、背景介绍

  • flutter压缩方式一般使用archive插件,但是根据https://pub.dev/packages/archive的介绍看,仅支持如下方式

    Zip (Archive)

    Tar (Archive)

    ZLib [Inflate decompression]

    GZip [Inflate decompression]

    BZip2 [decompression]
    描述中没有对7z的支持,所以只好自己下载源码编译导入。

  • C/C++源码导入正常是选择在android或者ios中进行。拿android来说,需要在CMakeList.txt中增加对.cpp和.c的列表,以及include头文件。如果仅是编译好之后拿动态链接库也是可以的。
  • 在android或ios中导入需要两个平台分别操作,相对会复杂一些,因此考虑直接在dart中调用支持。

二、实现方案

  • dart的支持包为ffi(https://dart.cn/guides/libraries/c-interop),虽然还是beta版本,但是基本的使用还是可以的。
  • 7z的源码编译出动态链接库之后,很少说会再次编译,所以我们首次编译出库之后,把它打包到assets中,在需要使用的时候,根据对应平台的ABI,取出对应的so文件使用即可。

三、p7zip源码

  • 官网下载源码https://sourceforge.net/projects/p7zip/,将它解压到创建的flutter项目根路径的p7zip目录中。
  • 有关p7zip的源码结构,大家感兴趣的可以自行找资料了解,我们直接看在Android中编译的时候,依赖了哪些文件。打开文件p7zip/CPP/ANDROID/7zr/jni/Android.mk,我们可以看到所有的-I相关的include目录和LOCAL_SRC_FILES都是在C和CPP两个目录下的,基于代码库最小化考虑,可以把其他的删掉。
    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.cpp
  • 仅支持.7z的压缩的话,我们使用7zr即可,我们使用命令方式,文档在p7zip/DOC/MANUAL/cmdline/index.htm中可以看到,具体使用如下
    # 压缩
    7zr a 输出文件名.7z 文件或路径列表
    
    # 解压缩
    7zr x 需要解压文件 -o解压路径
    
    因此,我们的p7zip.cpp增加一个p7zipShell函数传入指令,调用main
    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 ;
    #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
    # fPIE不去掉x86链接时会异常
    #LOCAL_CFLAGS += -fPIE
    #LOCAL_LDFLAGS += -fPIE -pie
    # 下面的这个参数不加编译时会有DWORD类型错误
    LOCAL_CFLAGS += -Wno-error=c++11-narrowing
    LOCAL_LDFLAGS += -Wno-error=c++11-narrowing
    APP_ABI := armeabi-v7a arm64-v8a
    APP_PLATFORM := android-14
    
  • native完成,启用ndk编译
    # 找到你的sdk下的ndk目录,加入到PATH中
    ndk-build
    
    编译完成后,lib生成到p7zip/CPP/ANDROID/7zr/libs中,暂时先记下。

四、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是极为相似的,大家可自行编写。

五、结语

  • 因为工作是做android设备的,基本很少接触跨平台,这次是第一次在项目中使用flutter,对于dart的界面搭建用来爽的不要不要的,但是确实还是不熟悉,比如光isolate就研究了好久,还不知道这种用法是不是常规的,有什么不合理的大家可以留言,感谢。
  • 过程中当然碰到很多问题,关键是网上flutter的相关案例还不够完善,需要自己开脑洞来摸索,感谢多篇文章中不知名的网友。由于是事后再写的文章,就没法一一列出了。
  • 不仅仅7z,以后有其他的开源库需要导入的时候都可以类似的这么干。
  • 我知道不贴源码地址是可耻的,就是懒,有空时再整理上传吧,就这样。

你可能感兴趣的:(flutter)