接入 Mars XLog 的坎坷历程


当我们的应用接入 IM 后,整个功能复杂度上来了,出现了很多用户反馈相关的问题,这个时候仅通过 Sentry 的异常日志已经不足以帮助我们排查问题了,所以在新的版本开始逐步完善日志输出的相关功能。

经过一番考虑后,选择微信开源的 Mars XLog 作为我们应用的基础日志库,开始折腾进行改造。

关于 XLog 更详细的内容可以参考:

【腾讯 Bugly 干货分享】微信 mars 的高性能日志模块 xlog

然后我需要做三个事情:

  • 集成 iOS 端的 xlog 库,作为一个 LocalPod 引入
  • 集成 Android 端的库,直接作为依赖引入
  • 编写 Flutter Channel 让 Flutter 侧可以使用 xlog 输出日志

官方文档的指引算是不错的了,但是过程中还是出现了一些细节的问题比较折腾,这里记录一下,分两端说起。

iOS

Include of non-modular header

Mars XLog 是用 C++ 写的,我参考 XLog 官方的 Demo,在 XLog API 的基础上简单封装了一个 LogHelper 用于初始化 XLog 相关功能,打印日志的相关方法则是直接沿用官方 Demo 的 LogUtil.h 提供的宏定义。

然后编译过程中抛了异常,提示:Include of non-modular header inside framework module ‘XLog’: ..

我是个 iOS 新手,只能找一下相关的解决方案了,下边这篇文章算是讲得比较明白的:

模块化与 Framework(21):引用非 Framework 模块 Include of non-modular header

根据这个原因,我便梳理一下 header 文件的引用:

我的 LocalPod 里的 LogHelper 就是作为子模块露出,且引用了 mars 的 header 文件,因为这里定义了宏,又需要引用 mars xlog 里边头文件的方法定义,所以 mars/xlog/xloggerbase.h 这个头文件会跟随我的 LogHelper 模块露出。

C++ 代码对我来说改起来太吃力了,考虑快捷的解决方案,即文章中也有提及的:设置 Allow Non-modular Includes In Framework Modules 为 YES。

我这个库放在了一个 LocalPod 里,所以我需要修改 podspec 文件给它新增一个配置,查询了一番之后才发现这个配置:

s.user_target_xcconfig = { 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES' }

这个问题这样算是解了,等我有空再去深究改 C++ 代码的解决方案。

symbol(s) not found for architecture armv7

我很开心地跑起了模拟器,确保 XLog 日志可以正常输出到文件中了。当我以为 XLog 集成到 iOS 端已经告一段落时,发现 CI 机子 iOS 打包不了,一翻构建日志,发现了这个异常:

14:36:42     Undefined symbols for architecture armv7:
14:36:42       "appender_close()", referenced from:
14:36:42           +[XLog close] in XLog.o
14:36:42       "_xlogger_Write", referenced from:
14:36:42           +[LogHelper logWithLevel:moduleName:fileName:lineNumber:funcName:message:] in LogHelper.o
14:36:42       "appender_open(XLogConfig const&)", referenced from:
14:36:42           +[XLog init:] in XLog.o
14:36:42       "_xlogger_SetLevel", referenced from:
14:36:42           +[XLog init:] in XLog.o
14:36:42       "_xlogger_Level", referenced from:
14:36:42           +[LogHelper shouldLog:] in LogHelper.o
14:36:42       "appender_set_console_log(bool)", referenced from:
14:36:42           +[XLog init:] in XLog.o
14:36:42     ld: symbol(s) not found for architecture armv7

翻找了一下官方 issue,发现官网已经是不支持 armv7 了,即使用 build_ios.py 脚本构建出来的 mars.framework 并没有包括 armv7 版本的库。

我是 iOS 新手,查了一番资料后了解到,修改 xcodebuild 命令指定 ARCHS 仅构建 arm64 即可,然而坎坷的是我们构建脚本直接走的 flutter build ios,并没有走自己的 xcodebuild 命令。

那么第一感觉我想去找 Flutter 的这个构建命令是否支持传入 cpu architecture 的相关配置,命令行 help 没有查到,感觉已经凉了一半,再去翻翻源码:

flutter/flutter

大致看了一下,感觉还是没戏,如果深入去摸索这个构建命令源码会比较花时间,所以我先去 Flutter Repo 提了一个 issue 放着,看看有没有支援。

但是不能一直等着它,如果要调整为不走 Flutter 构建命令的话改动量好像也蛮大的。

我开始探索另外一个办法,就是让 Mars XLog 支持 armv7(虽然这样打包出来的产物更大)。我就去翻看 build_ios.py 脚本的代码,发现还是比较好懂的。

查询 cmake 和 iOS framework 包构建的相关资源后,参考脚本里原有的 simulator x86 构建,我便尝试修改这个构建脚本来输出支持 armv7 的包。修改的内容大致如下:

// 新增 armv7 构建命令
IOS_BUILD_OS_CMD_ARMV7 = 'cmake ../.. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../ios.toolchain.cmake -DIOS_PLATFORM=OS -DIOS_ARCH="armv7" -DENABLE_ARC=0 -DENABLE_BITCODE=0 -DENABLE_VISIBILITY=1 && make -j8 && make install'

// 在 build_ios_xlog 方法中新增构建流程
// ...
		clean(BUILD_OUT_PATH)
    os.chdir(BUILD_OUT_PATH)

    ret = os.system(IOS_BUILD_OS_CMD_ARMV7)
    os.chdir(SCRIPT_PATH)
    if ret != 0:
        print('!!!!!!!!!!!build os fail!!!!!!!!!!!!!!!')
        return False

    libtool_armv7_dst_lib = INSTALL_PATH + '/armv7'
    if not libtool_libs(libtool_src_libs, libtool_armv7_dst_lib):
        return False

// 执行 lipo 命令前把 armv7 的库加进去
lipo_src_libs.append(libtool_armv7_dst_lib)

运气不错,构建成功,输出的 mars.framework 丢到项目中可以跑模拟器,也可以正常打包了。

在这之前真没接触过相关内容,摸着石头过河,幸亏我只是需要 XLog,如果需要 Mars 整个库,还得去解决 OpenSSL 的 armv7 构建。

这个问题也算是解了,等 Flutter 官方看能否提供仅构建 arm64 的方法,可以的话再优化一下,毕竟 armv7 基本是用不到的了。

Android

多进程写日志

Android 在编译构建过程中算是比较顺利,但是在验证日志内容时却发现日志解码异常,查看 Mars 官方 issues 发现,官方要求 android 端在多进程进行写日志时,需要用不同的日志文件来进行区分,不然会导致日志内容异常。

应该是 Flutter 的原因导致应用本身就有三个进程了(这个相关资料比较少,后续再挖坑填),并且我们接入 IM 相关的 SDK 可能还有额外的进程,所以会出现这个问题。

所以我开始考虑如何只提供一个 Log API,但是可以区分进程,在不同进程调用时写不同日志文件。

由于对 Android 也不是特别熟悉,所以还是花了些时间折腾。我用一个 HashMap 以进程名为 key 保存我封装的 XLogger 类实例化的对象。写日志时检查当前进程名和当前用的 XLogger 标识是否能够对应,不然的话就要切换 XLogger 来执行写日志操作,简单代码如下:

private static void switchXLogger() {
    String processName = getProcessName();
    if (processName.isEmpty()) {
        processName = "xdragon";
    }
    // 不是当前进程对应的日志标识
    if (currentProcessName != processName) {
        if (xlogger != null) {
            xlogger.close();
        }

        if (xloggers.containsKey(processName)) {
            xlogger = xloggers.get(processName);
        } else  {
            xlogger = new XLogger(processName);
            xloggers.put(processName, xlogger);
        }

        if (xlogger != null) {
            xlogger.open();
            com.tencent.mars.xlog.Log.setLogImp(xlogger.xlog);
        }
        currentProcessName = processName;
    }
}

这样可以让调用方无感知,直接走 Log API 即可,只是再写日志时会有切换 XLogger 的损耗,暂时从业务上看应该没有进程频繁切换且写日志的场景,性能相关的问题也暂时没有发现。