一. 为什么要封装打印类
虽然 flutter/原生给我们提供了日志打印的功能,但是超出一定长度以后会被截断
Json打印挤在一起看不清楚
堆栈打印深度过深多打印一些不需要的东西
实现 log 的多种展示方式
二. 需要哪些类
为了可以实现对日志的多种内容格式化和各种显示输出所以抽出来以下几个类
- 一些常量的字符串表示
- 对日志内容的打印输出抽象类
- 对日志内容格式化的抽象类
- 日志工具的config类
- 日志工具的管理类
- 日志工具的Util类
三. 打印输出的抽象类
打印类核心的功能就是打印日志 所以它有一个方法就是打印的方法
而我们要打印输出的内容有 当前 log等级 log的tag 需要打印的数据 当前堆栈信息 亦或是获取的Json数据
/// 日志打印输出的接口类
abstract class IHCLogPrint {
void logPrint({
required LogType type,
required String tag,
required String message,
StackTrace? stackTrace,
Map<String, dynamic>? json,
});
}
四. 格式化日志内容
这里定义一个IHCLogFormatter抽象类
///格式化的接口类
abstract class IHCLogFormatter<T> {
String format(T data);
}
格式化堆栈
堆栈的格式例如这样
#0 LogUtil._logPrint (package:com.halfcity.full_flutter_app/utils/log/log_util.dart:104:42)
#1 LogUtil._logPrint (package:com.halfcity.full_flutter_app/utils/log/log_util.dart:104:42)
#2 LogUtil._logPrint (package:com.halfcity.full_flutter_app/utils/log/log_util.dart:104:42)
....
会返回来很多无用的数据 而我们实际用到的也不过前五层就可以了
所以需要一个工具来剔除无用的数据和当前自己的包名
堆栈裁切工具类
class StackTraceUtil {
///正则表达式 表示#+数字+空格的格式
static final RegExp _startStr = RegExp(r'#\d+[\s]+');
///正则表达式表示 多个非换行符+ (非空) 正则表达式中()代表子项 如果需要正则()需要转义\( \)
///了解更多 https://www.runoob.com/regexp/regexp-syntax.html
static final RegExp _stackReg = RegExp(r'.+ \(([^\s]+)\)');
/// 把StackTrace 转成list 并去除无用信息
/// [stackTrace] 堆栈信息
///#0 LogUtil._logPrint (package:com.halfcity.full_flutter_app/utils/log/log_util.dart:104:42)
static List<String> _fixStack(StackTrace stackTrace) {
List tempList = stackTrace.toString().split("\n");
List<String> stackList = [];
for (String str in tempList) {
if (str.startsWith(_startStr)) {
//又是#号又是空格比较占地方 这里省去了 如果你不想省去直接传入str即可
stackList.add(str.replaceFirst(_startStr, ' '));
}
}
return stackList;
}
///获取剔除忽略包名及其其他无效信息的堆栈
/// [stackTrace] 堆栈
/// [ignorePackage] 需要忽略的包名
static List<String> _getRealStackTrack(
StackTrace stackTrace, String ignorePackage) {
///由于Flutter 上的StackTrack上的不太一样,Android返回的是list flutter返回的是StackTrack 所以需要手动切割 再处理
List<String> stackList = _fixStack(stackTrace);
int ignoreDepth = 0;
int allDepth = stackList.length;
//倒着查询 查到倒数第一包名和需要屏蔽的包名一致时,数据往上的数据全部舍弃掉
for (int i = allDepth - 1; i > -1; i--) {
Match? match = _stackReg.matchAsPrefix(stackList[i]);
//如果匹配且第一个子项也符合 group 0 表示全部 剩下的数字看子项的多少返回
if (match != null &&
(match.group(1)!.startsWith("package:$ignorePackage"))) {
ignoreDepth = i + 1;
break;
}
}
stackList = stackList.sublist(ignoreDepth);
return stackList;
}
/// 裁切堆栈
/// [stackTrace] 堆栈
/// [maxDepth] 深度
static List<String> _cropStackTrace(List<String> stackTrace, int? maxDepth) {
int realDeep = stackTrace.length;
realDeep =
maxDepth != null && maxDepth > 0 ? min(maxDepth, realDeep) : realDeep;
return stackTrace.sublist(0, realDeep);
}
///裁切获取到最终的stack 并获取最大深度的栈信息
static getCroppedRealStackTrace(
{required StackTrace stackTrace, ignorePackage, maxDepth}) {
return _cropStackTrace(
_getRealStackTrack(stackTrace, ignorePackage), maxDepth);
}
}
格式化堆栈信息
class StackFormatter implements ILogFormatter<List<String>> {
@override
String format(List<String> stackList) {
///每一行都设置成单独的 字符串
StringBuffer sb = StringBuffer();
///堆栈是空的直接返回
if (stackList.isEmpty) {
return "";
///堆栈只有一行那么就返回 - 堆栈
} else if (stackList.length == 1) {
return "\n\t-${stackList[0].toString()}\n";
///多行堆栈格式化
} else {
for (int i = 0; i < stackList.length; i++) {
if (i == 0) {
sb.writeln("\n\t┌StackTrace:");
}
if (i != stackList.length - 1) {
sb.writeln("\t├${stackList[i].toString()}");
} else {
sb.write("\t└${stackList[i].toString()}");
}
}
}
return sb.toString();
}
}
格式化JSON
class JsonFormatter extends ILogFormatter<Map<String, dynamic>> {
@override
String format(Map<String, dynamic> data) {
///递归调用循环遍历data 在StringBuffer中添加StringBuffer
String finalString = _forEachJson(data, 0);
finalString = "\ndata:$finalString";
return finalString;
}
/// [data] 传入需要格式化的数据
/// [spaceCount] 需要添加空格的长度 一个数字是两个空格
/// [needSpace] 需不需要添加空格
/// [needEnter] 需不需要回车
String _forEachJson(dynamic data, int spaceCount,
{bool needSpace = true, needEnter = true}) {
StringBuffer sb = StringBuffer();
int newSpace = spaceCount + 1;
if (data is Map) {
///如果它是Map走这里
///是否需要空格
sb.write(buildSpace(needSpace ? spaceCount : 0));
sb.write(needEnter ? "{\n" : "{");
data.forEach((key, value) {
///打印输出 key
sb.write("${buildSpace(needEnter ? newSpace : 0)}$key: ");
///递归调用看value是什么类型 如果字符长度少于30就不回车显示
sb.write(_forEachJson(value, newSpace,
needSpace: false,
needEnter: !(value is Map ? false : value.toString().length < 50)));
///不是最后一个就加,
if (data.keys.last != key) {
sb.write(needEnter ? ",\n" : ",");
}
});
if (needEnter) {
sb.writeln();
}
sb.write("${buildSpace(needEnter ? spaceCount : 0)}}");
} else if (data is List) {
///如果他是列表 走这里
sb.write(buildSpace(needSpace ? spaceCount : 0));
sb.write("[${needEnter ? "\n" : ""}");
for (var item in data) {
sb.write(_forEachJson(item, newSpace,
needEnter: !(item.toString().length < 30)));
///不是最后一个就加的,
if (data.last != item) {
sb.write(needEnter ? ",\n" : ",");
}
}
sb.write(needEnter ? "\n" : "");
sb.write("${buildSpace(needSpace?spaceCount:0)}]");
} else if (data is num || data is bool) {
///bool 或者数组不加双引号
sb.write(data);
} else if (data is String) {
///string 或者其他的打印加双引号 如果他是回车就改变他 按回车分行会错乱
sb.write("\"${data.replaceAll("\n", r"\n")}\"");
} else {
sb.write("$data");
}
return sb.toString();
}
///构造空格
String buildSpace(int deep) {
String temp = "";
for (int i = 0; i < deep; i++) {
temp += " ";
}
return temp;
}
}
五. 需要用到的常量
///常量
//log的type
enum LogType {
V, //VERBOSE
E, //ERROR
A, //ASSERT
W, //WARN
I, //INFO
D, //DEBUG
}
int logMaxLength=1024;
///log的type 字符串说明
List logTypeStr = ["VERBOSE", "ERROR", "ASSERT", "WARN", "INFO", "DEBUG"];
///log的type 数字说明(匹配的Android原生,ios暂不清楚)
List<int> logTypeNum = [2, 6, 7, 5, 4, 3];
六. 为了控制多个打印器的设置做了一个配置类
class LogConfig {
///是否开启日志
bool _enable = false;
///默认的Tag
String _globalTag = "LogTag";
///堆栈显示的深度
int _stackTraceDepth = 0;
///打印的方式
List<ILogPrint>? _printers;
LogConfig({enable, globalTag, stackTraceDepth, printers}) {
_enable = enable;
_globalTag = globalTag;
_stackTraceDepth = stackTraceDepth;
_printers?.addAll(printers);
}
@override
String toString() {
return 'LogConfig{_enable: $_enable, _globalTag: $_globalTag, _stackTraceDepth: $_stackTraceDepth, _printers: $_printers}';
}
get enable => _enable;
get globalTag => _globalTag;
get stackTraceDepth => _stackTraceDepth;
get printers => _printers;
}
七. Log的管理类
class LogManager {
///config
late LogConfig _config;
///打印器列表
List<ILogPrint> _printers = [];
///单例模式
static LogManager? _instance;
factory LogManager() => _instance ??= LogManager._();
LogManager._();
///初始化 Manager方法
LogManager.init({config, printers}) {
_config = config;
_printers.addAll(printers);
_instance = this;
}
get printers => _printers;
get config => _config;
void addPrinter(ILogPrint print) {
bool isHave = _printers.any((element) => element == print);
if (!isHave) {
_printers.add(print);
}
}
void removePrinter(ILogPrint print) {
_printers.remove(print);
}
}
九. 调用LogUtil
class LogUtil {
static const String _ignorePackageName = "log_demo/utils/log";
static void V(
{String? tag,
dynamic? message,
LogConfig? logConfig,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
_logPrint(
type: LogType.V,
tag: tag ??= "",
logConfig: logConfig,
message: message,
json: json,
stackTrace: stackTrace);
}
static void E(
{String? tag,
dynamic? message,
LogConfig? logConfig,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
_logPrint(
type: LogType.E,
tag: tag ??= "",
message: message,
logConfig: logConfig,
json: json,
stackTrace: stackTrace);
}
static void I(
{String? tag,
dynamic? message,
LogConfig? logConfig,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
_logPrint(
type: LogType.I,
tag: tag ??= "",
message: message,
json: json,
stackTrace: stackTrace);
}
static void D(
{String? tag,
dynamic? message,
LogConfig? logConfig,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
_logPrint(
type: LogType.D,
tag: tag ??= "",
logConfig: logConfig,
message: message,
json: json,
stackTrace: stackTrace);
}
static void A(
{String? tag,
LogConfig? logConfig,
dynamic? message,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
_logPrint(
type: LogType.A,
tag: tag ??= "",
message: message,
logConfig: logConfig,
json: json,
stackTrace: stackTrace);
}
static void W(
{String? tag,
dynamic? message,
LogConfig? logConfig,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
_logPrint(
type: LogType.W,
tag: tag ??= "",
message: message,
logConfig: logConfig,
json: json,
stackTrace: stackTrace);
}
static Future<void> _logPrint({
required LogType type,
required String tag,
LogConfig? logConfig,
dynamic message,
StackTrace? stackTrace,
Map<String, dynamic>? json,
}) async {
///如果logConfig为空那么就用默认的
logConfig ??= LogManager().config;
if (!logConfig?.enable) {
return;
}
StringBuffer sb = StringBuffer();
///打印当前页面
if (message.toString().isNotEmpty) {
sb.write(message);
}
///如果传入了栈且 要展示的深度大于0
if (stackTrace != null && logConfig?.stackTraceDepth > 0) {
sb.writeln();
String stackTraceStr = StackFormatter().format(
StackTraceUtil.getCroppedRealStackTrace(
stackTrace: stackTrace,
ignorePackage: _ignorePackageName,
maxDepth: logConfig?.stackTraceDepth));
sb.write(stackTraceStr);
}
if (json != null) {
sb.writeln();
String body = JsonFormatter().format(json);
sb.write(body);
}
///获取有几个打印器
List<ILogPrint> prints = logConfig?.printers ?? LogManager().printers;
if (prints.isEmpty) {
return;
}
///遍历打印器 分别打印数据
for (ILogPrint print in prints) {
print.logPrint(type: type, tag: tag, message: sb.toString());
}
}
}
十. 定义一个Flutter 控制台打印输出的方法
class ConsolePrint extends ILogPrint {
@override
void logPrint(
{required LogType type,
required String tag,
required String message,
StackTrace? stackTrace,
Map<String, dynamic>? json}) {
///如果要开启颜色显示 那么就是1000
///如果不开启颜色显示 那么就是1023
int _maxCharLength = 1000;
//匹配中文字符以及这些中文标点符号 。 ? ! , 、 ; : “ ” ‘ ' ( ) 《 》 〈 〉 【 】 『 』 「 」 ﹃ ﹄ 〔 〕 … — ~ ﹏ ¥
RegExp _chineseRegex = RegExp(r"[\u4e00-\u9fa5|\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]");
///用回车做分割
List<String> strList = message.split("\n");
///判断每句的长度 如果长度过长做切割
for (String str in strList) {
///获取总长度
int len = 0;
///获取当前长度
int current = 0;
///获取截断点数据
List<int> entry = [0];
///遍历文字 查看真实长度
for (int i = 0; i < str.length; i++) {
//// 一个汉字再打印区占三个长度,其他的占一个长度
len += str[i].contains(_chineseRegex) ? 3 : 1;
///寻找当前字符的下一个字符长度
int next = (i + 1) < str.length
? str[i + 1].contains(_chineseRegex)
? 3
: 1
: 0;
///当前字符累计长度 如果达到了需求就清空
current += str[i].contains(_chineseRegex) ? 3 : 1;
if (current < _maxCharLength && (current + next) >= _maxCharLength) {
entry.add(i);
current = 0;
}
}
///如果最后一个阶段点不是最后一个字符就添加上
if (entry.last != str.length - 1) {
entry.add(str.length);
}
///如果所有的长度小于1023 那么打印没有问题
if (len < _maxCharLength) {
_logPrint(type, tag, str);
} else {
///按照获取的截断点来打印
for (int i = 0; i < entry.length - 1; i++) {
_logPrint(type, tag, str.substring(entry[i], entry[i + 1]));
}
}
}
}
_logPrint(LogType type, String tag, String message) {
///前面的\u001b[31m用于设定SGR颜色,后面的\u001b[0m相当于一个封闭标签作为前面SGR颜色的作用范围的结束点标记。
/// \u001b[3 文字颜色范围 0-7 标准颜色 0是黑色 1是红色 2是绿色 3是黄色 4是蓝色 5是紫色 6蓝绿色 是 7是灰色 范围之外都是黑色
/// \u001b[9 文字颜色范围 0-7 高强度颜色 0是黑色 1是红色 2是绿色 3是黄色 4是蓝色 5是紫色 6蓝绿色 是 7是灰色 范围之外都是黑色
/// 自定义颜色 \u001b[38;2;255;0;0m 表示文字颜色 2是24位 255 0 0 是颜色的RGB 可以自定义颜色
/// \u001b[4 数字 m 是背景色
/// \u001b[1m 加粗
/// \u001b[3m 斜体
/// \u001b[4m 下划线
/// \u001b[7m 黑底白字
///\u001b[9m 删除线
///\u001b[0m 结束符
//////详情看 https://www.cnblogs.com/zt123123/p/16110475.html
String colorHead = "";
String colorEnd = "\u001b[0m";
switch (type) {
case LogType.V:
// const Color(0xff181818);
colorHead = "\u001b[38;2;187;187;187m";
break;
case LogType.E:
colorHead = "\u001b[38;2;255;0;6m";
break;
case LogType.A:
colorHead = "\u001b[38;2;143;0;5m";
break;
case LogType.W:
colorHead = "\u001b[38;2;187;187;35m";
break;
case LogType.I:
colorHead = "\u001b[38;2;72;187;49m";
break;
case LogType.D:
colorHead = "\u001b[38;2;0;112;187m";
break;
}
/// 这里是纯Flutter项目所以在控制台打印这样子是可以有颜色的 如果是flutter混编 安卓原生侧打印\u001b 可能是一个乱码也没有变色效果
/// 如果你不想只在调试模式打印 你可以把debugPrint换成print
debugPrint("$colorHead$message$colorEnd");
/// 如果原生侧有封装log工具直接 写一个methodChannel 传参数就好 ,如果没有,可以调用原生的log打印 传入 level tag 和message
/// kDebugMode 用这个可以判断是否在debug模式下
/// if(kDebugMode){
/// 在debug模式下打印日志
// bool? result=await CustomChannelUtil.printLog(level:logTypeNum[type.index],tag:tag,message:message);
/// }
}
}
十一. 使用
现在使用前初始化log打印器一次
Widget build(BuildContext context) {
LogManager.init(
config: LogConfig(enable: true, globalTag: "TAG", stackTraceDepth: 5),
printers: [ConsolePrint()]);
使用
///打印堆栈
LogUtil.I(tag: "test", stackTrace: StackTrace.current);
///打印json
LogUtil.E(tag: "JSON", json: json);
///打印信息
LogUtil.V(tag: "LogText", message: message);