0x00 解决问题

  1. 网络请求查看比较麻烦,需要fiddler/charles代理,再格式化json。手机端可以像打印日志一样打印json格式化后的log

  2. 系统Log比较挫,限制多多。优化系统控制台log打印

项目地址 https://github.com/xsfelvis/ZeusLog

0x01 ZeusLog

主要分为两大块,移动端Log和控制台Log,先上图

移动端

主要支持

  • 显示当前Actvity的名称

  • 显示所需打印网络请求的内容,内容部分透传点击事件,不影响app使用

  • 右边有一个长条控制区域,可以滑动联动内容区域,便于阅读长的网络请求

具体如下

【黄色区域】 当前Actvity名称

【红色区域】 log日志开启或者关闭,关闭后右边控制区域也随之消失,只有当前的Activity名称,如下

【橙色区域】 网络请求格式化显示区域,该部分透传所有点击事件,从而使整个App使用不受影响

【绿色区域】 自定义sideBar,滑动该区域控制橙色区域长文本滚动阅读

技术点:

没有采用window去实现,原因很简单,兼容性不好,现在各大厂商对自己的windows权限管理都很紧,而且正好尝试一下自定义view+事件分发,有了想法,一时技痒,就撸一个呗,当然你也可以有其他更好的思路,也可以跟我交流。

主要采用了自定义viewgroup+自定义viewSideDragBar【绿色部分】,通过橙色部分显示内容,透传点击事件从而不影响用户对app操作,SideDragBar控制内容区域的滚动,从而不影响内容区长文本的阅读。

几个关键点:

  1. 附着到当前Activity屏幕

获取屏幕的content区域内容,然后将我们的自定义viewgroup add进去

1
ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);

首先我们需要知道android中window的位置

  1. 透传文本区域的点击事件

我们希望屏幕日志只是给用户用来查看,而不要影响用户对app的操作,但是也要对长文本支持,这样采用重写ScrollView包裹一个textView的方法来解决这个问题,这个ScrollView的拦截事件方法均被重写

1
2
3
4
5
6
7
8
9
10

@Override
public boolean onTouchEvent(MotionEvent ev) {
return false;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

这样这个scrollview就可以透传了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<xsf.zeuslibrary.zeusMobile.ScrollViewSV
android:id="@+id/svContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="9">

<TextView
android:textColor="@color/white"
android:id="@+id/tvShowInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="日志显示 \n"
android:textSize="12dp"/>
</xsf.zeuslibrary.zeusMobile.ScrollViewSV>
  1. 长日志如何滑动查看

这里思考了一会,最后给出的解决方案就是在边栏加一个自定义view SideDragBar【绿色部分】

通过重写dispatchTouchEvent,记录滑动变化,然后使用控制传入的scrollview滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final int action = event.getAction();
//记录当前点击位置
int x = (int) event.getX();
int y = (int) event.getY();

switch (action) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
case MotionEvent.ACTION_MOVE:
int offsetX = x-lastX;
int offsetY = y-lastY;
sv.smoothScrollBy(0,offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}

总之就是通过重写scrollview使得控制滚动,从而解决这个问题

4.格式化json数据

关于这个,刚开始在网上找了一下,很多都是直接for循环一个一个字符的去解析,感觉实在不够优雅,而且效率也很低,其实JSONObjectJSONArray就可以解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

String message;
try{
if(msg.startsWith("{")){
JSONObject jsonObject = new JSONObject(msg);
message = jsonObject.toString(ZeusLog.JSON_INDENT);
}else if(msg.startsWith("[")){
JSONArray jsonArray = new JSONArray(msg);
message = jsonArray.toString(ZeusLog.JSON_INDENT);
}else {
message = msg;
}

}catch (JSONException e){
message = msg;
}

控制台

控制台有一些比较全的Log库,如orhanobut/logger、JakeWharton/timber等,感觉这些库写的都很重,当然功能很全,个人不太不喜欢他们打印log格式化方式,而且也想看看具体原理还有也不想增加无需求的功能,如xml格式化等,最终自己搓了一个支持基本log+多参数+json格式,具体如下:

  • 支持显示行号

  • 支持显示Log所在函数名称

  • 支持无Tag快捷打印

  • 支持在Android Studio开发IDE中,点击函数名称,跳转至Log所在位置

  • 支持JSON字符串解析打印

  • 支持无限长字符串打印,无Logcat4000字符限制

  • 支持变长参数,任意个数打印参数

  • 支持设置全局Tag

基本tag

无tag显示当前类名

格式化输出json

多参数log

几个关键点

  1. 显示行号、函数名

这个需要使用到Thread.currentThread().getStackTrace()返回的是一个StackTraceElement数组,内容为调用函数堆栈,并且以调用层级关系保存。android中对应返回值数组是19个,而且最终都是调用

1
dalvik.system.NativeStart.main(Native Method)

有兴趣的可以断点看下,其实日志打印工具类实现都离不开它的应用,然后获取对应index的StackTraceElement你就可以获取当前行号,类名,部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
StackTraceElement targetElement = stackTrace[stackTraceIndex];
String className = targetElement.getClassName();
String[] classNameInfo = className.split("\\.");
if (classNameInfo.length > 0) {
className = classNameInfo[classNameInfo.length - 1] + SUFFIX;
}
if (className.contains("$")) {
className = className.split("\\$")[0] + SUFFIX;
}
String methodName = targetElement.getMethodName();
int lineNumber = targetElement.getLineNumber();
if (lineNumber < 0) {
lineNumber = 0;
}
  1. log长字符的限制

先介绍下Android中Log的实现结构

可以看出大致过程 App通过util.log.产生日志->JVM->JNI(Native C)调用->log_write的sys_call()->logger驱动->dispatch分发给订阅者,android打印受限问题就出在这个Logger驱动上

1
2
3
4

#define LOGGER_ENTRY_MAX_LEN (4*1024)
#define LOGGER_ENTRY_MAX_PAYLOAD \\
(LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))

可以看出,系统的显示单条Log长度是有限制的4*1024字符长度,那么我们打印日志的时候可以对症下药,在写自己的log日志时可以对msg做下处理,采取分段打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public static void printDefault(int type, String tag, String msg) {
int index = 0;
int length = msg.length();
int countOfSub = length / MAX_LENGTH;
if (countOfSub > 0) {
for (int i = 0; i < countOfSub; i++) {
String sub = msg.substring(index, index + MAX_LENGTH);
printSub(type, tag, sub);
index += MAX_LENGTH;
}
printSub(type, tag, msg.substring(index, length));
} else {
printSub(type, tag, msg);
}
}

0x02 How to Use

引入这个库 或者aar

移动端Log使用很简单,需要在你想打印的地方,调用如下API即可

ZeusMobileView.startZeus(MainActivity.this).setJsonStr(JSON_LONG);

控制台Log 需要先初始化安全等级

ZeusLog.init(BuildConfig.DEBUG); 表示仅仅在debug包下打印日志,如果不初始化也可使用,但是需要注意release包保护

然后如同使用系统API一样使用即可

不带tag

  • ZeusLog.v(LOG_MSG);

  • ZeusLog.d(LOG_MSG);

  • ZeusLog.i(LOG_MSG);

  • ZeusLog.w(LOG_MSG);

  • ZeusLog.e(LOG_MSG);

  • ZeusLog.a(LOG_MSG);

带tag

  • ZeusLog.v(TAG, LOG_MSG);

  • ZeusLog.d(TAG, LOG_MSG);

  • ZeusLog.i(TAG, LOG_MSG);

  • ZeusLog.w(TAG, LOG_MSG);

  • ZeusLog.e(TAG, LOG_MSG);

  • ZeusLog.a(TAG, LOG_MSG);

json格式化

  • ZeusLog.printJsonStr(JSON);

多个参数

  • ZeusLog.v(TAG, LOG_MSG, “params1”, “params2”, this);

  • ZeusLog.d(TAG, LOG_MSG, “params1”, “params2”, this);

  • ZeusLog.i(TAG, LOG_MSG, “params1”, “params2”, this);

  • ZeusLog.w(TAG, LOG_MSG, “params1”, “params2”, this);

  • ZeusLog.e(TAG, LOG_MSG, “params1”, “params2”, this);

  • ZeusLog.a(TAG, LOG_MSG, “params1”, “params2”, this);