Kotlin x Java打造 UI 通用组件<一>-------封装一款易用高扩展的Hilog日志库一
对于程序员的职业生涯来说走向架构之路应该是人人的梦想,在之前已经学习了不少关于Android架构方面的东西,但是比较零碎,只要用一个项目中来进行操练才能更加深刻的理解架构的思想,所以这里打算学习从0开始打造一款实操项目的课程,注意这项目是针对中大型规模而设计的,将各个架构的思想都融入其中,全方位的来巩固自己的项目架构能力,为能在架构方面新上一个台阶而努力,先来感受一下最终这个项目的整体架构:
其中包含四大套件的开发:
其中各套件的作用贴一下官网的说明:
另外对于整个APP架构说明如下:
我觉得说得挺有道理的,在我们做项目时如果没有这些基础库作为支撑那要做一款大型的APP效率是非常之低的,而假如说整个通用库全是自己来一手打造的, 那么对于项目中如果哪个库有问题维护起来也非常的容易,而不像如果是自己找的三方的要维护起来就比较麻烦了,以上就是这个项目的整体架构说明,真的要完全能把这些知识点啃完,架构能力新上一个台阶肯定是毋庸置疑的,所以接下来耐着住寂寞来脚踏实地的迎来自己架构能力的蜕变吧!!!
效果演示:
首先咱们先来打造我们的hi-library,从app的架构图中可以看到它是位于最底层的,也是能否高效的开发出一款企业级的APP的一个非常重要的库,而它库中会包含有很多功能的库,这里首先从日志库开始封装:
在正式开动之前,先来看一下最终这个库封装后的效果:
其中日志中除了Logcat中输出了之外,我们在APP中集成了一个可视化的日志,既使没有IDE环境也能快速的根据日志来排查问题,通常是针对用户或者测试这功能是比较有用的,其实网络上有一些类似抓包的工具也长这样,可以展开与隐藏,这里也一样的:
另外日志在实际项目中可能还有需要往存储卡中来记录的需求,这里也支持,查看一下生成的文件:
这里生成的目录是在应用的缓存目录中,打开瞅一眼文件中记录的日志内容:
接下来则从0开始打造自己的一个日志库。
Hilog库疑难点分析与架构设计:
需求分析:
这个日志库需要满足以下需求:
- 能够打印堆栈信息
- 支持任何数据类型的打印
我们知道Android的Log是只能打印String类型的,而咱们实现的这个要能打印任何类型。 - 能够实现日志可视化
- 能够实现文件打印模块
- 支持不同打印器的插拔
它是指用户可以动态的配置要怎么打印,比如在Logcat中输出、在View中可视化输出、在文件中输出等等。
日志经历的几个过程:
下面贴官网的图:
目前看此图有些抽象是吧,木关系,待之后实现了可以回过头再来看一眼就亲切了。
疑难点分析:
- 堆栈信息获取
- 打印器插拔设计
- 线程复用防止频繁的创建线程
- 线程同步
架构设计图:
贴一下图,先感受一下,接下来的实现则会以此图作为大纲进行一一实现:
这些涉及到的类待之后实现时再来体验,有个大概的感知既可。
Hilog基础框架搭建:
接下来新建一个工程开撸:
由于这个是需要做为一个库进行使用,所以这里在里面创建一个Module,将日志相关的代码都放到这个module里面,好在未来时集成到项目工程中进行使用,如下:
首先来创建一个门面类:
其实也就是供用户调用的类:
添加日志级别:
我们知道对于Android的日志是分级别的,所以这里封装一个级别类型:
package org.devio.hi.library.log; import android.util.Log; /** * 日志类型 */ public class HiLogType { public final static int V = Log.VERBOSE; public final static int D = Log.DEBUG; public final static int I = Log.INFO; public final static int W = Log.WARN; public final static int E = Log.ERROR; public final static int A = Log.ASSERT; }
然后这里面创建一个注解,目的是为了限制用户在调用咱们封装的这个日志库时其日志级别只能选咱们定义的这些整型,而不是随意传一个值,具体如下:
接下来咱们则可以来使用一下这个注解了:
此时咱们来随意传一个整型看一下会不会报错?
还是将代码还原。
加上日志的TAG:
对于平常咱们使用Logcat时会加上一个TAG,所以这里再提供一个带Tag的重载方法:
package org.devio.hi.library.log; import androidx.annotation.NonNull; /** * HiLog的门面类 * 1. 打印堆栈信息 * 2. File输出 * 3. 模拟控制台 */ public class HiLog { public static void v(Object... contents) { log(HiLogType.D, contents); } public static void vt(String tag, Object... contents) { log(HiLogType.V, tag, contents); } public static void log(@HiLogType.TYPE int type, Object... contents) { } public static void log(@HiLogType.TYPE int type, @NonNull String tag, Object... contents) { } }
添加其它级别的重载方法:
现在只是写了带v级别的门面方法,以此为例,再完善其它剩下的级别:
package org.devio.hi.library.log; import androidx.annotation.NonNull; /** * HiLog的门面类 * 1. 打印堆栈信息 * 2. File输出 * 3. 模拟控制台 */ public class HiLog { public static void v(Object... contents) { log(HiLogType.D, contents); } public static void vt(String tag, Object... contents) { log(HiLogType.V, tag, contents); } public static void d(Object... contents) { log(HiLogType.D, contents); } public static void dt(String tag, Object... contents) { log(HiLogType.D, tag, contents); } public static void i(Object... contents) { log(HiLogType.I, contents); } public static void it(String tag, Object... contents) { log(HiLogType.I, tag, contents); } public static void w(Object... contents) { log(HiLogType.W, contents); } public static void wt(String tag, Object... contents) { log(HiLogType.W, tag, contents); } public static void e(Object... contents) { log(HiLogType.E, contents); } public static void et(String tag, Object... contents) { log(HiLogType.E, tag, contents); } public static void a(Object... contents) { log(HiLogType.A, contents); } public static void at(String tag, Object... contents) { log(HiLogType.A, tag, contents); } public static void log(@HiLogType.TYPE int type, Object... contents) { } public static void log(@HiLogType.TYPE int type, @NonNull String tag, Object... contents) { } }
增加日志的配置接口:
对于一个通用灵活的组件给用户提供配置功能是不可或缺的,这里如图涉及到2个类,下面则来创建一下:
目前配置先只有2个方法,随着不断的完善,它里面的配置项再逐一进行添加,接下来则需要新建一个管理类来关联这个配置类:
package org.devio.hi.library.log; import androidx.annotation.NonNull; /** * HiLog管理类 */ public class HiLogManager { private HiLogConfig config; private static HiLogManager instance; private HiLogManager(HiLogConfig config) { this.config = config; } public static HiLogManager getInstance() { return instance; } public static void init(@NonNull HiLogConfig config) { instance = new HiLogManager(config); } public HiLogConfig getConfig() { return config; } }
使用配置:
接下来则就可以在HiLog中来使用它了,此时则又需要增加带配置的一个重载方法:
package org.devio.hi.library.log; import androidx.annotation.NonNull; /** * HiLog的门面类 * 1. 打印堆栈信息 * 2. File输出 * 3. 模拟控制台 */ public class HiLog { public static void v(Object... contents) { log(HiLogType.D, contents); } public static void vt(String tag, Object... contents) { log(HiLogType.V, tag, contents); } public static void d(Object... contents) { log(HiLogType.D, contents); } public static void dt(String tag, Object... contents) { log(HiLogType.D, tag, contents); } public static void i(Object... contents) { log(HiLogType.I, contents); } public static void it(String tag, Object... contents) { log(HiLogType.I, tag, contents); } public static void w(Object... contents) { log(HiLogType.W, contents); } public static void wt(String tag, Object... contents) { log(HiLogType.W, tag, contents); } public static void e(Object... contents) { log(HiLogType.E, contents); } public static void et(String tag, Object... contents) { log(HiLogType.E, tag, contents); } public static void a(Object... contents) { log(HiLogType.A, contents); } public static void at(String tag, Object... contents) { log(HiLogType.A, tag, contents); } public static void log(@HiLogType.TYPE int type, Object... contents) { log(type, HiLogManager.getInstance().getConfig().getGlobalTag(), contents); } public static void log(@HiLogType.TYPE int type, @NonNull String tag, Object... contents) { log(HiLogManager.getInstance().getConfig(), type, tag, contents); } public static void log(@NonNull HiLogConfig config, @HiLogType.TYPE int type, @NonNull String tag, Object... contents) { if (!config.enable()) { return; } } }
简单处理HiLog输出:
接下来则来处理真正的输出,先来简单将内容打印一下,最终还会迭待完善的:
调用HiLog:
以上对于日志框架搭建的雏形就已经形成,接下来则来调用看一下效果,先在主APP中添加一下依赖:
然后咱们新建一个测试页面,
在主界面上增加一个Button:
然后增加按钮的点击事件:
接下来需要配置一下HiLog,配置则需要放到Application创建之时:
package org.devio.hi.library.app import android.app.Application import org.devio.hi.library.log.HiLogConfig import org.devio.hi.library.log.HiLogManager class MyApplication : Application() { override fun onCreate() { super.onCreate() HiLogManager.init(object : HiLogConfig() { override fun getGlobalTag(): String { return "MyApplication" } override fun enable(): Boolean { return true } }) } }
接下来则在主界面来调用一下这个测试页面:
运行看一下:
现在只是满足了最最基础的Logcat的日志输出了,当然这库还得支持其它形式的,继续再一点点扩充。
Hilog堆栈信息打印与日志格式化功能实现:
创建打印抽象接口:
接下来则继续对咱们的日志库进行扩展,我们知道该库是要支持多种形式的日志输出的,目前咱们只满足了一个Logcat的输出,所以这里先抽象出一个日志打印的类出来:
package org.devio.hi.library.log; import androidx.annotation.NonNull; /** * HiLog打印抽象接口 */ public interface HiLogPrinter { void print(@NonNull HiLogConfig config, int level, String tag, @NonNull String printString); }
创建日志化的抽象类:
对于我们的日志目前输出没有格式化,实际肯定是要做格式化处理滴,而为了灵活扩展对于输出的行为也得进行抽象
,所以定义个接口:
创建具体的格式化类:
然后有具体两个格式化实现,一个是堆栈的日志格式化,另一个是线程的日志格式化,所以下面来实现一下:
打印线程在实际开发有啥用呢?当然有用,因为可以让你清晰的知道是在哪个线程报错的嘛,这里直接打印一下线程名就好了,接下来则来定义堆栈的日志输出,这个是最重要的:
package org.devio.hi.library.log; /** * 日志堆格式化 */ public class HiStackTraceFormatter implements HiLogFormatter<StackTraceElement[]> { @Override public String format(StackTraceElement[] stackTrace) { StringBuilder sb = new StringBuilder(128); if (stackTrace == null || stackTrace.length == 0) { return null; } else if (stackTrace.length == 1) { return "\t─ " + stackTrace[0].toString(); } else { for (int i = 0, len = stackTrace.length; i < len; i++) { if (i == 0) { sb.append("stackTrace: \n"); } if (i != len - 1) { sb.append("\t├ "); sb.append(stackTrace[i].toString()); sb.append("\n"); } else { sb.append("\t└ "); sb.append(stackTrace[i].toString()); } } return sb.toString(); } } }
上面的逻辑比较简单,就不多解释了。
将格式化类定义到配置中:
由于咱们能打印所有对象,所以难免会要用到JSON对其对象转换成一个String,所以这里定义一个可配置对象格式化的方式,而不是写死成Gson了,具体格式化的时候由用户来决定到底是用哪个开源框架来对对象进行json格式化,对于库来说能写活就不写死,如下:
另外这里还定义两个东东,也是在格式化时用得到的,先提前定义了:
package org.devio.hi.library.log; public class HiLogConfig { /* 堆栈打印的最大长度 */ static int MAX_LEN = 512; static HiThreadFormatter HI_THREAD_FORMATTER = new HiThreadFormatter(); static HiStackTraceFormatter HI_STACK_TRACE_FORMATTER = new HiStackTraceFormatter(); public JsonParser injectJsonParser() { return null; } /** * 全局的默认TAG */ public String getGlobalTag() { return "HiLog"; } /** * 是否开启日志打印 */ public boolean enable() { return true; } /** * 是否要打印线程的信息 */ public boolean includeThread() { return false; } /** * 由于错误线程的堆栈信息往往很深,而实际对于问题的排查只需要前几行既可,所以这里用来 * 定义堆栈要打印的深度 */ public int stackTraceDepth() { return 5; } public interface JsonParser { String toJson(Object src); } }
实现控制台日志打印器:
各种抽象的行为都定义好了,接下来定义一个用来打印到控制台的日志打印器,如下:
package org.devio.hi.library.log; import android.util.Log; import androidx.annotation.NonNull; import static org.devio.hi.library.log.HiLogConfig.MAX_LEN; /** * 控制台日志打印器 */ public class HiConsolePrinter implements HiLogPrinter { @Override public void print(@NonNull HiLogConfig config, int level, String tag, @NonNull String printString) { int len = printString.length(); int countOfSub = len / MAX_LEN; if (countOfSub > 0) { //有多行,则需要根据最大长度进行处理 StringBuilder log = new StringBuilder(); int index = 0; for (int i = 0; i < countOfSub; i++) { log.append(printString.substring(index, index + MAX_LEN)); index += MAX_LEN; } if (index != len) { log.append(printString.substring(index, len)); } Log.println(level, tag, log.toString()); } else { //一行可以打印完,直接打印 Log.println(level, tag, printString); } } }
日志管理器增加打印机的插拔接口:
package org.devio.hi.library.log; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * HiLog管理类 */ public class HiLogManager { private HiLogConfig config; private static HiLogManager instance; private List<HiLogPrinter> printers = new ArrayList<>(); private HiLogManager(HiLogConfig config, HiLogPrinter[] printers) { this.config = config; this.printers.addAll(Arrays.asList(printers)); } public static HiLogManager getInstance() { return instance; } public static void init(@NonNull HiLogConfig config, HiLogPrinter... printers) { instance = new HiLogManager(config, printers); } public HiLogConfig getConfig() { return config; } public List<HiLogPrinter> getPrinters() { return printers; } public void addPrinter(HiLogPrinter printer) { printers.add(printer); } public void removePrinter(HiLogPrinter printer) { if (printers != null) { printers.remove(printer); } } }
此时则需要在配置中增加打印器:
修改打印日志的逻辑:
目前我们打印的代码还是写死的:
此时则需要根据配置动态的打印器来实现打印了,具体修改如下:
package org.devio.hi.library.log; import androidx.annotation.NonNull; import java.util.Arrays; import java.util.List; /** * HiLog的门面类 * 1. 打印堆栈信息 * 2. File输出 * 3. 模拟控制台 */ public class HiLog { public static void v(Object... contents) { log(HiLogType.D, contents); } public static void vt(String tag, Object... contents) { log(HiLogType.V, tag, contents); } public static void d(Object... contents) { log(HiLogType.D, contents); } public static void dt(String tag, Object... contents) { log(HiLogType.D, tag, contents); } public static void i(Object... contents) { log(HiLogType.I, contents); } public static void it(String tag, Object... contents) { log(HiLogType.I, tag, contents); } public static void w(Object... contents) { log(HiLogType.W, contents); } public static void wt(String tag, Object... contents) { log(HiLogType.W, tag, contents); } public static void e(Object... contents) { log(HiLogType.E, contents); } public static void et(String tag, Object... contents) { log(HiLogType.E, tag, contents); } public static void a(Object... contents) { log(HiLogType.A, contents); } public static void at(String tag, Object... contents) { log(HiLogType.A, tag, contents); } public static void log(@HiLogType.TYPE int type, Object... contents) { log(type, HiLogManager.getInstance().getConfig().getGlobalTag(), contents); } public static void log(@HiLogType.TYPE int type, @NonNull String tag, Object... contents) { log(HiLogManager.getInstance().getConfig(), type, tag, contents); } public static void log(@NonNull HiLogConfig config, @HiLogType.TYPE int type, @NonNull String tag, Object... contents) { if (!config.enable()) { return; } StringBuilder sb = new StringBuilder(); if (config.includeThread()) { String threadInfo = HiLogConfig.HI_THREAD_FORMATTER.format(Thread.currentThread()); sb.append(threadInfo).append("\n"); } if (config.stackTraceDepth() > 0) { String stackTrace = HiLogConfig.HI_STACK_TRACE_FORMATTER.format(new Throwable().getStackTrace()); sb.append(stackTrace).append("\n"); } String body = parseBody(contents, config); sb.append(body); List<HiLogPrinter> printers = config.printers() != null ? Arrays.asList(config.printers()) : HiLogManager.getInstance().getPrinters(); if (printers == null) { return; } //打印log for (HiLogPrinter printer : printers) { printer.print(config, type, tag, sb.toString()); } } private static String parseBody(@NonNull Object[] contents, @NonNull HiLogConfig config) { if (config.injectJsonParser() != null) { //只有一个数据且为String的情况下不再进行序列化 if (contents.length == 1 && contents[0] instanceof String) { return (String) contents[0]; } return config.injectJsonParser().toJson(contents); } StringBuilder sb = new StringBuilder(); for (Object o : contents) { sb.append(o.toString()).append(";"); } if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } }
修改初始化配置:
接下来则需要在Applictaion中来初始化配置,具体修改如下:
package org.devio.hi.library.app import android.app.Application import org.devio.hi.library.log.HiConsolePrinter import org.devio.hi.library.log.HiLogConfig import org.devio.hi.library.log.HiLogManager class MyApplication : Application() { override fun onCreate() { super.onCreate() HiLogManager.init(object : HiLogConfig() { override fun getGlobalTag(): String { return "MyApplication" } override fun enable(): Boolean { return true } override fun includeThread(): Boolean { return true } override fun stackTraceDepth(): Int { return 5 } override fun injectJsonParser(): JsonParser { return super.injectJsonParser() } }, HiConsolePrinter()) } }
其中解析JSON咱们采用Gson,则先来添加相关的依赖:
然后重写JSON格式化方法:
修改调用方式:
接下来则来修改一下调用方式来看一下效果:
输出:
2020-07-12 16:41:47.610 5041-5041/org.devio.hi.library.app E/---: stackTrace: ├ org.devio.hi.library.log.HiLog.log(HiLog.java:83) ├ org.devio.hi.library.app.demo.HiLogDemoActivity.printLog(HiLogDemoActivity.kt:21) ├ org.devio.hi.library.app.demo.HiLogDemoActivity.access$printLog(HiLogDemoActivity.kt:11) ├ org.devio.hi.library.app.demo.HiLogDemoActivity$onCreate$1.onClick(HiLogDemoActivity.kt:16) ├ android.view.View.performClick(View.java:6608) ├ android.view.View.performClickInternal(View.java:6585) ├ android.view.View.access$3100(View.java:785) ├ android.view.View$PerformClick.run(View.java:25921) ├ android.os.Handler.handleCallback(Handler.java:873) ├ android.os.Handler.dispatchMessage(Handler.java:99) ├ android.os.Looper.loop(Looper.java:201) ├ android.app.ActivityThread.main(ActivityThread.java:6810) ├ java.lang.reflect.Method.invoke(Native Method) ├ com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) └ com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873) test new log 2020-07-12 16:41:47.613 5041-5041/org.devio.hi.library.app A/MyApplication: Thread:main stackTrace: ├ org.devio.hi.library.log.HiLog.log(HiLog.java:83) ├ org.devio.hi.library.log.HiLog.log(HiLog.java:70) ├ org.devio.hi.library.log.HiLog.log(HiLog.java:66) ├ org.devio.hi.library.log.HiLog.a(HiLog.java:57) ├ org.devio.hi.library.app.demo.HiLogDemoActivity.printLog(HiLogDemoActivity.kt:26) ├ org.devio.hi.library.app.demo.HiLogDemoActivity.access$printLog(HiLogDemoActivity.kt:11) ├ org.devio.hi.library.app.demo.HiLogDemoActivity$onCreate$1.onClick(HiLogDemoActivity.kt:16) ├ android.view.View.performClick(View.java:6608) ├ android.view.View.performClickInternal(View.java:6585) ├ android.view.View.access$3100(View.java:785) ├ android.view.View$PerformClick.run(View.java:25921) ├ android.os.Handler.handleCallback(Handler.java:873) ├ android.os.Handler.dispatchMessage(Handler.java:99) ├ android.os.Looper.loop(Looper.java:201) ├ android.app.ActivityThread.main(ActivityThread.java:6810) ├ java.lang.reflect.Method.invoke(Native Method) ├ com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) └ com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873) test log
确实够灵活,默认的打印则走的是全局的那个配置,而我们在调用时还可以单独再配置。
堆栈日志深度控制:
不过,目前发现好像堆栈的日志深度木有受配制的控制
这是因为咱们这块还木有处理:
所以下面来处理一下,这里用一个工具类来将处理逻辑封装一下:
package org.devio.hi.library.log; /** * HiLog堆栈处理工具类 */ public class HiStackTraceUtil { /** * Get the real stack trace and then crop it with a max depth. * * @param stackTrace the full stack trace * @param maxDepth the max depth of real stack trace that will be cropped, 0 means no limitation * @return the cropped real stack trace */ public static StackTraceElement[] getCroppedRealStackTrack(StackTraceElement[] stackTrace, int maxDepth) { return cropStackTrace(stackTrace, maxDepth); } /** * Crop the stack trace with a max depth. * * @param callStack the original stack trace * @param maxDepth the max depth of real stack trace that will be cropped, * 0 means no limitation * @return the cropped stack trace */ private static StackTraceElement[] cropStackTrace(StackTraceElement[] callStack, int maxDepth) { int realDepth = callStack.length; if (maxDepth > 0) { realDepth = Math.min(maxDepth, realDepth); } StackTraceElement[] realStack = new StackTraceElement[realDepth]; System.arraycopy(callStack, 0, realStack, 0, realDepth); return realStack; } }
里面的逻辑就不细说了,比较容易理解,就是一个数组的拷贝,接下来调用一下:
好,咱们再来运行看一下:
添加忽略的包名堆栈信息:
对于目前的堆栈日志的输出貌似挺完美的了,但是!!!有一个问题,日志中有些信息是对于我们排查问题是没啥用的,咱们来分析一下目前这个堆栈有啥毛病?
so,咱们得添加针对指定包名的过滤才行,下面处理一下:
package org.devio.hi.library.log; /** * HiLog堆栈处理工具类 */ public class HiStackTraceUtil { /** * Get the real stack trace and then crop it with a max depth. * * @param stackTrace the full stack trace * @param maxDepth the max depth of real stack trace that will be cropped, 0 means no limitation * @return the cropped real stack trace */ public static StackTraceElement[] getCroppedRealStackTrack(StackTraceElement[] stackTrace, String ignorePackage, int maxDepth) { return cropStackTrace(getRealStackTrack(stackTrace, ignorePackage), maxDepth); } /** * Get the real stack trace, all elements that come from XLog library would be dropped. * * @param stackTrace the full stack trace * @return the real stack trace, all elements come from system and library user */ private static StackTraceElement[] getRealStackTrack(StackTraceElement[] stackTrace, String ignorePackage) { int ignoreDepth = 0; int allDepth = stackTrace.length; String className; for (int i = allDepth - 1; i >= 0; i--) { className = stackTrace[i].getClassName(); if (ignorePackage != null && className.startsWith(ignorePackage)) { ignoreDepth = i + 1; break; } } int realDepth = allDepth - ignoreDepth; StackTraceElement[] realStack = new StackTraceElement[realDepth]; System.arraycopy(stackTrace, ignoreDepth, realStack, 0, realDepth); return realStack; } /** * Crop the stack trace with a max depth. * * @param callStack the original stack trace * @param maxDepth the max depth of real stack trace that will be cropped, * 0 means no limitation * @return the cropped stack trace */ private static StackTraceElement[] cropStackTrace(StackTraceElement[] callStack, int maxDepth) { int realDepth = callStack.length; if (maxDepth > 0) { realDepth = Math.min(maxDepth, realDepth); } StackTraceElement[] realStack = new StackTraceElement[realDepth]; System.arraycopy(callStack, 0, realStack, 0, realDepth); return realStack; } }
调用也发生变化了:
此时再运行一下:
至此,对于堆栈日志的处理就比较完美了~~