iOS远程调试工具设计文档

 

前言

由于业务迭代速度增快,我们遇到线上问题进行排查时复杂度也在不断增加,对于客户端来说,现有的排查方法无非只有查看crash后台、根据客诉信息尝试复现场景;对于线上问题的修复方式更是只能让服务端做兼容,如果遇到兼容配置无法满足需求的时候只能做发版处理。因此对于客户端来说有一套完整的可在线调试的系统的构建迫在眉睫。

设计目标

Doctor与其说是一个功能模块,不如说是一个工具集。对于一个工具集来说以下几点要素是必须要考虑的:

  1. 操作的便利性
  2. 支持复杂的调用结构
  3. 可以轻松横向扩展功能集,功能之间可以有依赖、也可以相互独立
  4. 对多个命令有批处理能力
  5. 功能的错误调用抛出的内部异常不会导致主程序崩溃

对于前两点我们通过模仿shell的调用方式,将字符串解析为命令,以空格作为参数分隔符,以回车作为命令分隔符,以|作为管道传递操作符,通过独立TCP链接传入客户端。

链接采用半双工模式,每次发出命令后需要等待客户端回执消息结束才能再次发送消息,以此保证命令顺序。

每个功能通过插件的形式接入Doctor内,使用路径查询和依赖声明的方式来保证调用的准确性。这种结构利于横向功能扩展和功能的独立性。

构建命令执行串行队列,保证每条命令的执行顺序,单条命令异常不影响下一条命令。支持管道传递参数。

对命令的执行代码进行保护,防止命令异常导致程序崩溃。

结构概述

alt

Doctor的大致结构如上图所示。

模块顶层为抽象接入层,提供远程连接、命令队列、DSL解析和执行器的功能。抽象接入层提供整个Doctor的基础执行能力,具体如下:

  1. 远程连接用于服务端主动向客户端建立TCP连接,该链接需要独立于现有的MQTT消息通道,因为MQTT通道可能本身就处于需要Debug的状态。在成功建立连接后将会使App转为恢复模式,禁止其他所有服务启动,只有通过服务端主动发送模式退出命令或者用户点击退出恢复模式按钮才可以恢复为正常App启动流程。
  2. 命令队列提供将消息拆分为独立任务并提供串行执行的容器。任务容器内调用DSL Parser做任务拆分,每个任务都是一个标准的同步非纯函数,包含标准的输入输出流,通过输入输出流提供管道功能进行参数传递。任务可递归嵌套,语法解析会将任务完全展开为执行器,每个执行器的则是对单条符合最小语法规则的代码抽象。
  3. DSL Parser提供语法解析功能,基于文本匹配构造语法树,然后根据树节点的参数转为带参数执行对象(Executor)。自定义DSL Parser的优势在于语法结构可自定义,如果遇到不满足的情况可以通过更新该模块进行扩展。
  4. 执行器则是执行任务的最小单元,每个执行器同样符合标准的命令任务结构。同时执行器内会对所执行的任务进行异常保护,防止执行器自身的异常导致整个程序崩溃。执行器通过返回元组参数进行流程控制,包括一个标准整形(int8),和一个字符串(utf-8编码)。执行器直接通过反射获取实际的功能模块的可执行对象,如果能查到该对象,则尝试调用。

模块的底层为可横向扩展,符合功能模块协议的具体实现。根据目前业务方的需求,我们初期将分为两类实现八个功能模块,具体如下:

  1. 网络监控模块。本模块提供DNS检测、出口IP检测、HTTP访问检测、本机网卡信息等功能。
  2. UI卡顿监控模块。本模块提供UI卡顿时内存调用栈信息捕获的功能。
  3. 崩溃监控模块。本模块提供方法调用错误、KVO异常、容器空值错误、野指针错误等调用堆栈信息捕获和对应防护。
  4. OOM监控模块。本模块提供大内存分配监控、单次大块连续内存分配、内存泄漏、OOM时调用堆栈的信息捕获。
  5. SQL远程注入后门模块。本模块提供远程下发SQL修改本地客户端数据库信息的功能。
  6. Runtime动态修改对象状态模块。本模块提供通过远程下发命令,动态修改运行时对象内存状态的功能。
  7. 动态修改代码实现模块。本模块提供通过远程下发热修复脚本,动态修改代码功能实现的模块。
  8. 沙盒文件操作模块。本模块提供沙盒内所有文件的CURD功能,目前对所有文档都做文本文件处理,编码类型可选。

执行流程

alt

样例

SEND > chkstk set -t 1000 // 设置卡顿监测间隔为1000ms RECV > chkstk set time limit success! SEND > ifconfig // 查询本机网卡信息 RECV > en0: xxxx RECV > en1: xxxx RECV > Gateway: xxx.xxx.xxx.xxx RECV > IP: xxx.xxx.xxx.xxx SEND > sqlinject “SELECT XXX FROM XXX” RECV > [SQLite] xxx xxx xxx xxx RECV > [SQLite] xxx xxx xxx xxx RECV > [SQLite] xxx xxx xxx xxx RECV > [SQLite] xxx xxx xxx xxx RECV > [SQLite] xxx xxx xxx xxx

功能模块介绍

抽象接入层

抽象业务层作为与远程服务直接交互的代码,主要功能就是对所有底层功能的抽象,并提供恢复模式、长链接、执行队列、语法解析、以及具体功能执行的功能。

恢复模式

为了排除APP内所有功能代码的影响,我们需要提供一个相对单纯的环境来进行DEBUG。因此抽象接入层提供了一个恢复模式的功能。

恢复模式本质上为一个单纯的页面控制器,该控制器在启动时通过预埋下的标记符启动。恢复模式启动时将替代现有APP启动流程,转为唤醒Doctor模块的远程连接功能,根据预埋下的启动标记自动连接至远程服务,如果15s内连接成功则进入Doctor模块;如果不成功,或者用户选择退出恢复模式,则切换为原APP启动流程继续执行。

恢复模式在退出时需要清理所有资源和销毁所有开辟线程,避免污染主流程的运行状态,因此整套逻辑需要注意资源回收工作。

恢复模式主要为做彻底排查用,正常检测可以直接使用MQTT命令转发至Doctor的命令队列。

远程连接

远程连接模块通过读取服务端下发的MQTT中附带的恢复模式启动标记中的服务器地址来进行连接访问操作。

远程连接模块使用TCP裸字节流进行命令和结果传输,使用自定义简单文本协议。消息传输使用半双工队列并开启读写锁,保证命令下发顺序不出错。

远程连接提供简单的鉴权校验功能,鉴权密钥在MQTT下发的标记符中,文本匹配成功则建立后续通讯。

DSL解析

DSL解析器为传入文本的简单语法解析模块。语法解析规则基本按照bash的规则。

以空格作为参数分隔符,第一个参数为模块名,第二个命令为模块第一个传参,以此类推。

以双引号""作为字符串

命令队列/执行器

命令队列主要提供两个功能:1. 执行命令的任务队列,并保证调用顺序;2. 传递任务之间的上下文,并记录环境参数。

任务队列的元素为执行器对象,在往任务队列中添加任务时,将传入的字符串进行DSL解析,并展开为独立任务,并按顺序执行。

执行器执行代码时需要用try-catch进行包装,效率可以低一些,但是要避免执行器内部代码异常导致程序崩溃。

性能监控

性能检测模块在本次Doctor模块设计中主要尝试参考目前市面上已有的功能框架来做,由于监测相关原理相对复杂,本文不再详细说明实现方式。

网络状态监测

本模块提供DNS检测、出口IP检测、HTTP访问检测、ARP检测、本机网卡信息等功能。

DNS检测/出口IP检测

通过模拟ping和traceroute的原理进行检测。

参考代码:

  1. LDNetDiagnoService
HTTP访问检测

通过模仿CURL请求器的方式构造HTTP请求,并发送给指定服务器,返回数据通过管道传递给下一层调用。

例子:

SEND > curl -I http://www.baidu.com RECV > HTTP/1.1 200 OK RECV > Accept-Ranges: bytes RECV > Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform RECV > Connection: Keep-Alive RECV > Content-Length: 277 RECV > Content-Type: text/html RECV > Date: Fri, 22 Feb 2019 07:52:25 GMT RECV > Etag: “575e1f60-115” RECV > Last-Modified: Mon, 13 Jun 2016 02:50:08 GMT RECV > Pragma: no-cache RECV > Server: bfe/1.0.8.18

参考代码:

  1. libcurl for iOS
本机网卡信息

通过<ifaddrs.h><arpa/inet.h><net/if.h>采集硬件信息并回调

卡顿监测

卡顿监测最常见的作坊就是通过子线程去访问主线程,然后通过访问主线程耗时来判断卡顿状态。

参考代码:

  1. LXDAppMonitor

崩溃监测

常见的野指针监测都是hook掉dealloc方法之后,延后释放对象,然后通过检查下一次访问者是谁来得知野指针触发的代码调用栈。

第二种方法:dealloc的时候,照样让对象释放,但是去填充整块对象的内存,缺点是由于内存可分配,监控率下降,但是少了内存的干扰。

参考代码:

  1. LXDZombieSniffer
  2. XXShield

OOM检测

OOM检测主要包含oom、dump引起的内存溢出,大内存分配超页,循环引用等内存泄漏这类内存问题的监测。

参考代码:

  1. OOMDetector
  2. FBRetainCycleDetector
  3. FBMemoryProfiler

动态修复

动态修复是本次Doctor主要的功能模块,但由于苹果对热修复相关功能检查较严格,因此手段相对受限。

SQL远程注入

如果出现数据库升级异常,可以通过下发sql修复数据库,核心方法就是通过字符串转换执行sqlite_exec(),然后将执行后的返回值和文本字符串发回服务端,将sqlite中的几个核心函数做字符映射即可。

动态修改指定对象信息 / 动态修改对象实现

通过字符映射将常见的元编程函数映射过去,包括取类型、调用方法、获取对象、类型判断、方法判断、KVC等方法,并提供基础的遍历语法即可。

动态修改实现即将上述修改指定对象的操作转为本地脚本化进行记录,然后每次启动时再执行一次即可。

参考文献: limboy的轻量级方案

沙盒数据CURD

对文件CURD方法做字符映射,做好文件递归访问操作即可,必须要时需要提供压缩解压缩的命令。