Flutter开发文件管理器基本原理

·  阅读 1484

这是一个自用的文件管理器,可能开发有一点久了有些部分忘记是为什么了,一直都想着来水水这部分的文章

自定义File、Directory类

dart自带是有一个抽象类FileSystemEntity,并且有_File类,_Directory类,加了下划线是因为File/Directory也是一个抽象类,我们能用File(path)直接实例化是由File类的工厂函数生成的

为什么不用dart自带的这些类🤔

  • _File加了下划线,没有对以外的文件公开,所有我们不能自定义一个类来继承_File类来增加属性
  • Directory的属性其实是没有Java中Directory的属性丰富的,如文件夹的创建时间,修改时间等
  • 如果你想你的文件管理器在一定情况下能访问unix设备的/这些目录,dart的Directory.list()永远会返回permission defined

文件节点抽象类

abstract class FileEntity {
  //这个名字可能带有->/x/x的字符
  String path;
  //完整信息
  String fullInfo;
  //文件创建日期

  String accessed = "";
  //文件修改日期
  String modified = "";
  //如果是文件夹才有该属性,表示它包含的项目数
  String itemsNumber = "";
  // 节点的权限信息
  String mode = "";
  // 文件的大小,isFile为true才赋值该属性
  String size = "";
  String uid = "";
  String gid = "";
  String get nodeName => path.split(" -> ").first.split("/").last;
  bool get isFile => this.runtimeType == NiFile;
  bool get isDirectory => this.runtimeType == NiDirectory;
  static final List<String> imagetype = ["jpg", "png"]; //图片的所有扩展名
  static final List<String> textType = [
    "smali",
    "txt",
    "xml",
    "py",
    "sh",
    "dart"
  ]; //文本的扩展名
  static bool isText(FileEntity fileNode) {
    String type = fileNode.nodeName.replaceAll(RegExp(".*\\."), "");
    print(type);
    return textType.contains(type);
  }

  static bool isImg(FileEntity fileNode) {
  // Directory();
  // File
    String type = fileNode.nodeName.replaceAll(RegExp(".*\\."), "");
    print(type);
    return imagetype.contains(type);
  }
}
复制代码

目前该类很简单,待扩展

File类

为了不与dart自带File类名冲突,我命名为NiFile

class NiFile extends FileEntity {
  final String path;
  //
  final String fullInfo;
  NiFile(this.path,this.fullInfo);
}

复制代码

Directory类


class NiDirectory extends FileEntity {
  final String path;
  
  //如果是文件夹才有该属性,表示它包含的项目数
  String itemsNumber = "";
  final String fullInfo;
  NiDirectory(this.path, this.fullInfo);
  Future<List<FileEntity>> listAndSort() async {
    List<FileEntity> _fileNodes = [];
    String lsPath;
    if (Platform.isAndroid)
      lsPath = "/system/bin/ls";
    else
      lsPath = "ls";
    int _startIndex;
    List<String> _fullmessage = [];
    path = path.replaceAll("//", "/");
    // print("刷新的路径=====>>$path");
    _fullmessage = (await CustomProcess.exec("$lsPath -aog '$path'\n"))
        .split("\n")
          ..removeAt(0);
    String b = "";
    for (int i = 0; i < _fullmessage.length; i++) {
      if (_fullmessage[i].startsWith("l")) {
        //说明这个节点是符号链接
        if (_fullmessage[i].split(" -> ").last.startsWith("/")) {
          //首先以 -> 符号分割开,last拿到的是该节点链接到的那个元素
          //如果这个元素不是以/开始,则该符号链接使用的是相对链接
          b += _fullmessage[i].split(" -> ").last + "\n";
        } else {
          b += "$path/${_fullmessage[i].split(" -> ").last}\n";
        }
      }
    }
    // print("======>$b");
    if (b.isNotEmpty) {
      //-g取消打印owner  -0取消打印group   -L不跟随符号链接,会指向整个符号链接最后指向的那个
      List<String> linkFileNodes =
          (await CustomProcess.exec("echo '$b'|xargs $lsPath -ALdog\n"))
              .replaceAll("//", "/")
              .split("\n");
      print("linkFileNodes=====>$linkFileNodes");
      Map<String, String> map = Map();
      for (String str in linkFileNodes) {
        // print(str);
        map[str.replaceAll(RegExp(".*[0-9] "), "")] = str.substring(0, 1);
      }
      print(map);
      for (int i = 0; i < _fullmessage.length; i++) {
        if (_fullmessage[i].startsWith("l") &&
            map.keys.contains(_fullmessage[i].split(" -> ").last)) {
          print(_fullmessage[i]);
          _fullmessage[i] = _fullmessage[i].replaceAll(
              RegExp("^l"), map[_fullmessage[i].split(" -> ").last]);
          // f.remove(f.first);
        }
      }
      File("/sdcard/MToolkit/日志文件夹/自定义日志.txt")
          .writeAsString(_fullmessage.join("\n"));
    }
    // DateTime three = DateTime.now();
    // print("得到最终的文件列表信息耗时===>>${three.difference(two)}");
    // _fullmessage..toString().re
    _fullmessage.removeWhere((a) {
      //查找.这个所在的行数
      return a.endsWith(" .");
    });
    int currentIndex = _fullmessage.indexWhere((a) {
      return a.endsWith(" ..");
    });
    _startIndex = _fullmessage[currentIndex].indexOf(".."); //获取文件名开始的地址
    // print("startIndex===>>>$_startIndex");
    if (path == "/") {
      //如果当前路径已经是/就不需要再加一个/了
      for (int i = 0; i < _fullmessage.length; i++) {
        FileEntity fileEntity;
        if (_fullmessage[i].startsWith(RegExp("-|l"))) {
          fileEntity = NiFile("$path" + _fullmessage[i].substring(_startIndex),
              _fullmessage[i]);
        } else {
          fileEntity = NiDirectory(
              "$path" + _fullmessage[i].substring(_startIndex),
              _fullmessage[i]);
        }
        _fileNodes.add(fileEntity);
      }
    } else {
      for (int i = 0; i < _fullmessage.length; i++) {
        FileEntity fileEntity;
        if (_fullmessage[i].startsWith(RegExp("-|l"))) {
          fileEntity = NiFile("$path/" + _fullmessage[i].substring(_startIndex),
              _fullmessage[i]);
        } else {
          fileEntity = NiDirectory(
              "$path/" + _fullmessage[i].substring(_startIndex),
              _fullmessage[i]);
        }
        _fileNodes.add(fileEntity);
      }
    }
    _fileNodes.sort((a, b) => fileNodeCompare(a, b));
    return _fileNodes;
  }

  /* */
//文件节点的比较,文件夹在上面
  int fileNodeCompare(FileEntity a, FileEntity b) {
    //在遵循文件夹在上的条件下且按文件名排序
    if (a.isFile && !b.isFile) return 1;
    if (!a.isFile && b.isFile) return -1;
    return a.path.toLowerCase().compareTo(b.path.toLowerCase());
  }
}

复制代码

如果你要直接用这两个类,需要手动去除一些调试输出🤪,没错,整个列表的获取都是基于ls命令

其中用到的CustomProcess

import 'dart:async';
import 'dart:convert';
import 'dart:io';

abstract class NightmareProcess extends Process {}

typedef ProcessCallBack = void Function(String output);

class CustomProcess {
  final ProcessCallBack callback;
  static Process _process;
  static bool isUseing = false;
  CustomProcess(this.callback);
  static Process get process => _process;
  String exitCode = "";
  static String getlsPath() {
    if (Platform.isAndroid)
      return "/system/bin/ls";
    else
      return "ls";
  }

  static init() async {
    _process = await Process.start('sh', [],
        includeParentEnvironment: true, runInShell: false);
    // _process.stderr.transform(utf8.decoder).listen((d) {
    //   print(d);
    // });
  }

  static Stream<List<int>> processStdout = _process.stdout.asBroadcastStream();
  static Stream<List<int>> processStderr = _process.stderr.asBroadcastStream();
  static Future<String> exec(String script,
      {ProcessCallBack callback,
      bool getStdout = true,
      bool getStderr = false}) async {
    while (_process == null) {
      await Future.delayed(Duration(milliseconds: 100));
    }
    String output = "";
    // _process.stdout.listen()..
    while (isUseing) {
      await Future.delayed(Duration(milliseconds: 100));
    }
    _process.stdin.write(script + "\necho exitCode\n");
    isUseing = true;
    if (getStdout)
      await processStdout.transform(utf8.decoder).every((v) {
        output += v;
        if (callback != null) callback(v);
        // print("$script来自监听的打印$v");
        if (v.contains("exitCode"))
          return false;
        else
          return true;
      });
    if (getStderr) {
      await processStderr.transform(utf8.decoder).every((v) {
        output += v;
        if (callback != null) callback(v);
        print("来自监听的打印错误输出$v");
        return false;
      });
    }
    isUseing = false;
    return output.replaceAll("exitCode", "").trim();
  }
}

复制代码

可以观察到这用到了Process.start,也就是说它是一个可以持续存在的Process,在已经root的android设备上,提前键入su命令,之后就能用这样的方法获取隐私权限的一些目录与文件了

排序逻辑

我参考了其他的一些文件管理器的排序规则,需要文件夹在上,文件在下的原则下并按字符进行排序

在上面Directory的listAndSort函数内已经有实现方法了,单独解释一下

sort是dart中List<String>自带的方法,看一下原型

void sort([int compare(E a, E b)]);
复制代码

大写的E就是List<E>中间的泛型,也就是说你是List<int>那么它需要的参数就是

void sort([int compare(int a, int b)]);
复制代码

是可选参数

让我印象比较深刻的是,当时在写这部分的时候,学校刚好教过C语言的冒泡排序,我二话不说把他弄到dart来,最后那个性能实在是不敢恭维😕

据我目前的知识所知它是一个快速排序,可以看一下快速排序的原理,并不会完全的比较里面每两个节点

写过java排序就会发现这部分原理是差不多的

看下实现代码

_fileNodes.sort((a, b) => fileNodeCompare(a, b));
int fileNodeCompare(FileEntity a, FileEntity b) {
    //在遵循文件夹在上的条件下且按文件名排序
    if (a.isFile && !b.isFile) return 1;
    if (!a.isFile && b.isFile) return -1;
    return a.path.toLowerCase().compareTo(b.path.toLowerCase());
}
复制代码
  • a节点为文件,b节点为文件夹,需要将a放在b后,返回1(正数)
  • a节点为文件夹,b节点为文件,需要将b放在a后,返回-1(负数)
  • 如果所比较两个节点类型相同,将其路径转换为小写在进行字符比较

为什么要转换成小写

如果不转换为小写可能会存在:

a

b

A

B

但我们目前文件管理器普遍为

a

A

b

B

获取文件节点列表List

List<FileEntity> _fileNodes = []; //保存所有文件的节点
_fileNodes = await NiDirectory(path).listAndSort();
复制代码

拿到列表就能渲染到ListView这些组件了

预览

支持平台

我亲自测试了在Linux/Macos/Android都是支持的,其中在Android上的支持最好,性能也比较可观

有没可能支持Windows?🧐

  • 有的,可以观察到listAndSort函数中用到了lsPath这个路径,它指向设备ls命令的路径,而在windows上,cygwin中有Linux上api的移植,我猜想可以通过cygwin对ls的源码进行编译,生成ls.exe
  • 我很久前见过busybox在windows上的移植,busybox 是有ls命令的,所以理论是行得通的

存在问题

在无ROOT的安卓设备上去获取根目录/的列表时,会出现部分节点无法获取信息的情况,列表会直接报红(当然可以通过优化代码来避免),毕竟设备本来没有高级权限嘛。

结语

本篇只是简单说了基本实现原理,还涉及比较多的其他部分的处理,项目已经开源,可以直接跑起来的。很多逻辑还没有时间写😜

MToolkit部分开源

如果想直接预览这个文件管理器,可以去酷安下载这个app

MToolkit

分类:
开发工具
标签:
分类:
开发工具
标签: