Runtime应用之Sunglasses崩溃排查工具实践


背景介绍

01
Sunglasses简介

Sunglasses,太阳镜,希望能让你在如火如荼的移动端开发过程中变的更 cool !

Sunglasses是京东主站的购物车团队孵化出的用于在开发、测试阶段使用的提效工具集,解决了由于组件化所导致的各团队提效工具分散、共享难度大的痛点,目前已经融合了京东主站各开发团队贡献的包括代码调试、UI调试、性能调优、数据调试等多种类型的提效工具,拥有接入成本低、代码无侵入、使用简便等优点,以不参与上线的组件化模块形式帮助各开发团队提升效率,目前已经参与到京东商城、京喜、京东极速版等App的版本迭代过程中。未来Sunglasses将持续优化共建方式,完善工具覆盖场景,打磨工具使用体验,并扩大支持App的范围,帮助各团队持续提效,成为研发、测试环节中更 cool 的一环!

02
Sunglasses崩溃排查工具
痛点分析:测试阶段测试工程师触发崩溃后需要通过连接设备导出日志,并根据符号表解析崩溃堆栈,过程耗时且繁琐,无意中降低了测试人员的工作效率。
Sunglasses方案:在程序发生崩溃时实时捕获崩溃相关信息,提示并记录,方便测试同学快速把崩溃堆栈反馈给研发同学,省去日志导出和崩溃解析的时间。
效果展示:


Runtime

01
消息机制
OC中方法调用最终都会转换为objc_msgSend(id self,SEL _cmd,…),整体会经历三个阶段:

  • 消息发送: 在类和父类的cache列表及方法列表中查找方法

  • 动态解析: 消息发送阶段没有找到方法,动态解析阶段动态添加

  • 消息转发: 未实现动态解析,消息进入转发阶段,可转发给可处理消息的接受者来处理。

如果上述三个阶段都未找到,则会触发 unrecognzied selector sent to instance

接下来,我们结合Runtime源码分析其内部实现。

1.1.1 方法查找

我们先看下整体流程图:

通过流程图我们可以看到首先会进行nil检测,源码中也会涉及对Tagged Pointer检测(Tagged Pointer此次暂不涉及)。此后会检测缓存是否存在调用方法,存在调用,不存在继续查找过程,流程图最终会调用到 lookUpImpOrForward
方法,此方法也是查找的核心函数,接下来我们分析下该核心方法做了哪些操作。

lookUpImpOrForward 函数

首先看下此函数的整体流程图:


下面我们在通过源码看一下Runtime底层的优化点:

  • 如果方法列表是有序数组,会进行二分查找,提高查找效率;如果无需,则进行顺序查找。
/***********************************************************************

* getMethodNoSuper_nolock

* fixme

* Locking: runtimeLock must be read- or write-locked by the caller

**********************************************************************/

static method_t *search_method_list(const method_list_t *mlist, SEL sel)

{

    int methodListIsFixedUp = mlist->isFixedUp();

    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);

    // 方法有序的情况下,采用二分查找,提高查找效率

    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {

        return findMethodInSortedMethodList(sel, mlist);

    } else {

        // Linear search of unsorted method list

        for (auto& meth : *mlist) {

            if (meth.name == sel) return &meth;

        }

    }



#if DEBUG // sanity-check negative results if (mlist->isFixedUp()) { for (auto& meth : *mlist) { if (meth.name == sel) { _objc_fatal("linear search worked when binary search did not"); } } } #endif

return nil; } static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) { assert(list);

const method_t * const first = &list->first; const method_t *base = first; const method_t *probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; /* 二分查找通过位移运算进行 */ for (count = list->count; count != 0; count >>= 1) { // middle probe = base + (count >> 1); uintptr_t probeValue = (uintptr_t)probe->name; if (keyValue == probeValue) { // `probe` is a match. // Rewind looking for the *first* occurrence of this value. // This is required for correct category overrides. while (probe > first && keyValue == (uintptr_t)probe[-1].name) { probe--; } return (method_t *)probe; } // 二分操作 if (keyValue > probeValue) { base = probe + 1; count--; } } return nil; }

  • 找到方法,缓存到本类中

  // Try superclass caches and method lists.

    {

        /*

        类方法列表中没有找到方法,需要继续在父类的cache以及method list中进行查找

        */

        unsigned attempts = unreasonableClassCount();

        for (Class curClass = cls->superclass;

             curClass != nil;

             curClass = curClass->superclass)

{

            // Halt if there is a cycle in the superclass chain.

            if (--attempts == 0) {

                _objc_fatal("Memory corruption in class list.");

            }

            

            // Superclass cache.

            imp = cache_getImp(curClass, sel);

            if (imp) {

                if (imp != (IMP)_objc_msgForward_impcache) {

                    // Found the method in a superclass. Cache it in this class.

                    /*

                    父类缓存中找到方法,进行缓存,缓存的地点是当前类的缓存列表中,并没有缓存到父类的缓存中

                    */

                    log_and_fill_cache(cls, imp, sel, inst, curClass);

                    goto done;

                }

                else {

                    // Found a forward:: entry in a superclass.

                    // Stop searching, but don't cache yet; call method 

                    // resolver for this class first.

                    break;

                }

            }

            

            // Superclass method list.

            Method meth = getMethodNoSuper_nolock(curClass, sel);

            if (meth) {

                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);

                imp = meth->imp;

                goto done;

            }

        }

    }

1.1.2 动态解析

动态解析部分的流程图如下:

/* 动态解析 */

// No implementation found. Try method resolver once.

if (resolver  &&  !triedResolver) {

    runtimeLock.unlock();

    _class_resolveMethod(cls, sel, inst);

    runtimeLock.lock();

    // Don't cache the result; we don't hold the lock so it may have 

    // changed already. Re-do the search from scratch instead.

    triedResolver = YES; //  标识是否进行过动态解析

    goto retry; // 重新调用retry逻辑

}

动态解析只进行一次,如果进行动态解析,并实现对应的方法,这时会重新调用retry。

/***********************************************************************

* _class_resolveMethod

* Call +resolveClassMethod or +resolveInstanceMethod.

* Returns nothing; any result would be potentially out-of-date already.

* Does not check if the method already exists.

**********************************************************************/

void _class_resolveMethod(Class cls, SEL sel, id inst)

{

    if (! cls->isMetaClass()) {

        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);

    } 

    else {

        // try [nonMetaClass resolveClassMethod:sel]

        // and [cls resolveInstanceMethod:sel]

        _class_resolveClassMethod(cls, sel, inst);

        if (!lookUpImpOrNil(cls, sel, inst, 

                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 

        {

            _class_resolveInstanceMethod(cls, sel, inst);

        }

    }

}

通过源码我们得知在动态解析时会区分类方法和对象方法,分别会调用resolveClassMethod 和 resolveInstanceMethod两个方法,在源码中类方法的逻辑中最终如果没有找到方法,会调用resolveInstanceMethod方法。这里做个解释: 对象方法和类方法是存储在不同的位置。对象方法存在类的方法列表中,类方法存在元类的方法列表中,没有找到对应的方法时会通过继承关系最终找到NSObject,类方法依旧是如此。通过下图中的关系我们可以看到所有元类最终的父类都是NSObject,NSObject的最终父类是nil。同时方法存储在方法列表中是没有 “+” “-”之分的,所以方法最终查找到NSObject时就需要使用 resolveInstanceMethod 来查找。


动态解析时会涉及到如下两个方法:

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

1.1.3 消息转发

消息转发阶段涉及到的方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

- (void)forwardInvocation:(NSInvocation *)anInvocation

针对消息转发这里,源码中没有开源,我们通过查找最终定位到如下:

// Default forward handler halts the process.

__attribute__((noreturn)) void 

objc_defaultForwardHandler(id self, SEL sel)

{

    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "

                "(no message forward handler is installed)", 

                class_isMetaClass(object_getClass(self)) ? '+' : '-', 

                object_getClassName(self), sel_getName(sel), self);

}

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

从源码中我们只能定位到一个错误信息的log。不过消息转发的整个流程是我们是了解的:

  • forwardingTargetForSelector
    可以选择备用接收者进行消息处理
  • methodSignatureForSelector
  • forwardInvocation
    上面这两个方法是结合来使用的,methodSignatureForSelector返回相应的方法签名,forwardInvocation进行消息转发的处理。

0 2

Method Swizzle

1.2.1 实现原理

1.2.2 代码实现

+ (void)load {


static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; // 原方法名和替换方法名 SEL originalSelector = @selector(viewDidAppear:); SEL swizzledSelector = @selector(swizzle_viewDidAppear:); // 原方法结构体和替换方法结构体 Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加默认的方法实现IMP BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {// 添加方法实现IMP成功后,修改替换方法结构体内的方法实现IMP和方法类型编码TypeEncoding class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 添加失败,调用交互两个方法的实现 method_exchangeImplementations(originalMethod, swizzledMethod); } });

我们可以看到一共使用三个方法:

  • class_addMethod
  • class_replaceMethod
  • method_exchangeImplementations

1.2.3 源码展示

/***********************************************************************

* class_addMethod

**********************************************************************/

static IMP _class_addMethod(Class cls, SEL name, IMP imp, 

                            const char *types, bool replace)

{

    old_method *m;

    IMP result = nil;



if (!types) types = "";

mutex_locker_t lock(methodListLock);

if ((m = _findMethodInClass(cls, name))) { // already exists // fixme atomic result = method_getImplementation((Method)m); if (replace) { method_setImplementation((Method)m, imp); } } else { // fixme could be faster old_method_list *mlist = (old_method_list *)calloc(sizeof(old_method_list), 1); mlist->obsolete = fixed_up_method_list; mlist->method_count = 1; mlist->method_list[0].method_name = name; mlist->method_list[0].method_types = strdup(types); mlist->method_list[0].method_imp = imp; _objc_insertMethods(cls, mlist, nil); if (!(cls->info & CLS_CONSTRUCTING)) { flush_caches(cls, NO); } else { // in-construction class has no subclasses flush_cache(cls); } result = nil; }

return result; } /*********************************************************************** * class_addMethod **********************************************************************/ BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) { IMP old; if (!cls) return NO;

old = _class_addMethod(cls, name, imp, types, NO); return !old; }



/*********************************************************************** * class_replaceMethod **********************************************************************/ IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) { if (!cls) return nil;

return _class_addMethod(cls, name, imp, types, YES); } void method_exchangeImplementations(Method m1_gen, Method m2_gen) { IMP m1_imp; old_method *m1 = oldmethod(m1_gen); old_method *m2 = oldmethod(m2_gen); if (!m1 || !m2) return;

impLock.lock(); m1_imp = m1->method_imp; m1->method_imp = m2->method_imp; m2->method_imp = m1_imp; impLock.unlock(); }

通过源码我们可以清晰的看到实现原理,addMethod和和和replaceMethod都会调用_class_addMethod,该方法里面两种操作一种是设置 IMP指针,一个是添加方法,添加到对应的MethodList。method_exchangeImplementations方面里面就是交换方法的IMP指针。


类簇

01
介绍
官方解释:


Class clusters are a design pattern that the Foundation framework makes extensive use of. Class clusters group a number of private concrete subclasses under a public abstract superclass. The grouping of classes in this way simplifies the publicly visible architecture of an object-oriented framework without reducing its functional richness. Class clusters are based on the Abstract Factory design pattern.

大致意思:
类簇是一种Foundation框架经常使用的设计模式(desigen pattern)。类簇聚合类在一个共有抽象基类下的一些私有的具体的子类。用这种方法组合的类简化了面向对象(object-oriented)框架的共有的可用的结构,同时并没有减少它的功能的丰富性,类簇是基于抽象工厂设计模式的 (Abstract Factory design pattern)。
类簇设计理念:Simple Concept and Simple Interface(简单的概念,简单的接口)

02
部分类簇私有类


Sunglasses崩溃排查工具实现原理

01
常见崩溃总结

  • KVO
  • NSNull
  • NSTimer
  • NSArray
  • NSString
  • Zombie Pointer
  • NSNotification
  • Unrecognized Selector Sent to Instance

关于NSNotification

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don’t need to unregister an observer in its dealloc method. Otherwise, you should call this method or removeObserver: before observer or any object specified in addObserverForName:object:queue:usingBlock: or addObserver:selector:name:object: is deallocated.

0 2

实现原理
利用Method Swizzle,来完成异常处理机制,从而防止crash,并且提供相应提示。

2.2.1 集合类

NSArray,NSMutableArray等

针对类簇进行hook,要注意的问题:

  • 找准对应的私有类名称。
  • 可变 不可变会有重复的方法,hook时要注意hook之后的方法名称要不同,不然获取的信息不准确。

2.2.2 Unrecognized Selector Sent to Instance

通过之前的Runtime源码分析我们可以了解到,找不到方法,还有三次补救的机会,分别为

  • 动态解析
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

  • 备用接收者

- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

  • 消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");

针对这种情况我们hook了两个方法:

  • methodSignatureForSelector:(SEL)aSelector
  • forwardInvocation:(NSInvocation *)anInvocation
+ (NSMethodSignature*)hook_methodSignatureForSelector:(SEL)aSelector {

    NSMethodSignature* methodSignature = [self hook_classMethodSignatureForSelector:aSelector];

    if (methodSignature) {

        return methodSignature;

    }

    // 随意返回签名,保证进入invocation

    return [NSMethodSignature signatureWithObjCTypes:"v@:"];

}

我们返回了一个v@:的方法签名,会直接触发forwardInvocation,在forwardInvocation方法中我们不会再去触发系统的API,直接选择自己处理异常,进行相应的异常展示。


Sunglasses崩溃排查工具未来规划

01
交互优化

  • 优化crash实时展示效果,会结合Sunglasses悬浮窗展示交互。

0 2

功能优化

  • 客户端增加数据缓存,在客户端进行简单崩溃数据汇总。
  • 增加多场景crash异常处理机制。
  • 增加崩溃发生时除崩溃堆栈以外的关键信息,如操作路径、设备状态等信息的捕获。
  • 支持server端数据联通,增加web端崩溃数据统计、分析。
参考文献

Runtime源码:
https://opensource.apple.com/tarballs/objc4/
类簇私有类收集:
https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2
Class Clusters:
https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2
Type Encodings:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
Concepts in Objective-C Programming:
https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html