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

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

自定义 File、Directory 类

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

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

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

文件节点抽象类

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
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

1
2
3
4
5
6
7
class NiFile extends FileEntity {
final String path;
//
final String fullInfo;
NiFile(this.path,this.fullInfo);
}

Directory 类

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

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

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
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>自带的方法,看一下原型

1
void sort([int compare(E a, E b)]);

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

1
void sort([int compare(int a, int b)]);

是可选参数

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

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

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

看下实现代码

1
2
3
4
5
6
7
_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

1
2
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

作者

梦魇兽

发布于

2020-04-17

更新于

2023-03-11

许可协议

评论