Runtime底层原理之源码分析

2019-08-12  本文已影响0人  越来越胖了

前面两篇放了一张runtime的思维导图,提供了一些runtime的常用API,这一章主要从汇编 、C、 C++源码上分析runtime的底层,后面还会写Runtime开发中的实际应用(构思ing中...)。本篇博客涉及到的demoruntime源码会在博客结尾给出下载链接

Runtime

一点半了,我靠,今天不写了,明天还要上班,睡觉去了,狗命要紧......这个文章这周一定找时间更新上去😴😴😴😴😴

runtime是什么❓

c,c++,汇编一起写的API,装载到内存给程序提供运行时的功能;编译期runtime下层会被编译成runtime的API。

runtime的三种调用方式

  1. runtimeAPI
  2. NSObject的API(isKindOf等)
  3. OC的上层(@selector)
  • 注:代码中...代表不去讲解的代码,非重点

我们从一个简单实例入手,查看runtime的原理;首先创建一个LGPerson类,包含一个run的实例方法;VC中的代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *p = [[LGPerson alloc] init];
    
    [p run];
    
}

通过在终端输入clang -rewrite-objc ViewController.m -o new.cpp.m文件转 c++后看到如下东西:

...
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS; //    
};
...
  LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));

  1. 什么是objc_msgSend?
    objc_msgSend是汇编写的一个C函数,用于消息的发送。
  2. 为什么用汇编去写?
    C写一个函数,没法保留未知的参数,跳转到任意的指针。汇编有寄存器可以做到;汇编快速,相比于C,快50%-80%;

[p run];被转换成((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));其中(id)p是消息接受者;sel_registerName("run")代表方法编号SEL _cmd,底层是一个字符串name;我们可以笼统的看成所有的方法都会转成类似于👇这样的一个实现(这里是没有带参数的🌰):

void runIMP(id self,SEL _cmd){
    ...
}

通过sel,我们可以找到imp(函数实现的指针).怎么找到的???这个是重点(敲黑板ing),老司机源码解析告诉你。

首先消息的发送((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));前面代码就是做了类型的转换,我们可以写成如下:

objc_msgSend(s,sel_registerName("run”));

如果大家对sel_registerName感到陌生,可以执行如下代码打印看看就知道了:

NSLog(@"%p---%p",sel_registerName("run"),@selector(run));
//*************************************************************************
2019-08-10 12:25:23.712418+0800 01---runtime[3542:307149] 0x10b64d4d3---0x10b64d4d3

方法的本质是消息发送,在发送时,还隐式的传了两个参数消息接收者和方法编号;

对象方法和类方法发送消息,向父类发送消息对比:

1. 对象方法
objc_msgSend(s,sel_registerName("run”));
向父类发送消息:objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
objc_super结构体代码如下:

struct objc_super {
    /// Specifies an instance of a class.类的实例
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
    

对应一个实例,就是这样的:

 struct objc_super mySuper;
    mySuper.receiver = p;
    mySuper.super_class = class_getSuperclass([p class]);
    objc_msgSendSuper(&mySuper,@selector(run))

2. 类方法
objc_msgSend(objc_getClass("LGPerson"),sel_registerName("run”));

向父类发送消息:

     struct objc_super myClassSuper;
    myClassSuper.receiver = p;
    myClassSuper.super_class = class_getSuperclass(object_getClass([p class]));
    objc_msgSendSuper(&myClassSuper,sel_registerName(“run”))

object_getClass([p class]) 是元类。元类就是类所属的类。

面试题:

对象方法存在哪? 对应的类里面.
类方法存在哪? 类的元类里面,本质是一个实例方法.

  • 一个类方法,在元类里面,就是一个实例 ,所以类方法存在对应的元类里面,本质是一个实例方法。
  • OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个 isa 指针,指向他所属的类,也就是元类。

当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
当你给类发消息时,消息是在寻找这个类的元类的方法列表。

Runtime源码分析

源码下载地址:https://opensource.apple.com/ 最新的版本是objc4-750,大家也可以去GitHub上找有注释的源码,我在最后的demo下载中也会提供一份runtime的源码。

objc_msgSend的消息的发送有两种方式:

    1. 快速的查找,从缓存找cache_t cache查找;

缓存来源于我们的类.这个可以通过源码验证一下,在objc4的源码中找到class:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;

    每一个类里面都会有一个cache,存储方法的sel,🙃
    imp,objc_msgSend首先回来这里通过sel查找imp,找到了,🙃
    直接返回,没有则下面介绍的第二种方式继续查找,找到了会存入cache。🙃
    cache_t cache;             
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    下面的代码不是重点介绍的,省略了🙃
    ...
}

    1. 慢速的查找,lookup(C,C++,汇编查找);

下面从objc_msgSend的源码入手,查看消息的发送流程:
runtime源码中搜索_objc_msgSend(方法前面加_可以查看方法的汇编代码),可以看到有各种环境下的,这里以arm64为例子讲解:

1目录
objc_msgSend方法执行,首先进入到ENTRY
Snip20190815_2.png
LNilOrTagged:
    /*为空则直接返回  END_ENTRY _objc_msgSend*/
    b.eq    LReturnZero     // nil check

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    //上面这些代码 是对isa进行了处理,具体不管它,LGetIsaDone这个才是重要方法🙃
    b   LGetIsaDone

LNilOrTagged这个方法走完后,我们接着往下走,来到CacheLookup,就是从缓存中查找:

CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

从注释可以看到,会出现两种情况: calls imp 也就是我们要的东西;或者执行objc_msgSend_uncached;下面是CacheLookup的实现

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *   x1 = selector
 *   x16 = class to be searched
 *
 * Kills:
 *   x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/
//.macro是一个宏的意思🙃
.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

注:一般我们看源码看到类似👆这样注释的基本都是核心代码,一个小技巧

CacheLookup,先进入缓存进行查找CacheHit,这是一种快速查找的方法,通过哈希表找到对应的imp;如果没有找到--->CheckMiss,则进入慢速的查找lookup,慢速查找找到了也会进行缓存起来;add操作是其他地方找到了会调用,添加到缓存中。下面是没找到时执行的代码:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz x9, LGetImpMiss
.elseif $0 == NORMAL
   //因为我们传入的参数 是NORMAL,所以走这里🙃
    cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

因为我们传入的参数 是NORMAL,没找到则执行__objc_msgSend_uncached(.macro是宏定义)

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    //里面就这一个方法,就他了,意思就是‘方法列表中查找’🙃
    MethodTableLookup
    br  x17

    END_ENTRY __objc_msgSend_uncached

//*************************************************************
.macro MethodTableLookup
    
    。。。
    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // imp in x0
    mov x17, x0
    
    。。。

.endmacro


通过MethodTableLookup 方法列表查找imp; 会执行 __class_lookupMethodAndLoadCache3这个方法我们搜索时会发现怎么没有了,找不到了,很多小伙伴在这里直接懵逼了,其实这里需要从汇编回到C,干掉一个 _ 搜索_class_lookupMethodAndLoadCache3,找到如下代码:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

YES/initialize/, 因为前面编译找到了isa,所以为YES
NO/cache/, 因为没有缓存才来到这的,所以NO
YES/resolver/ 最后都没有找到imp,则是否进行动态解析,所以YES

👇代码的流程是这样的lookUpImpOrForward ------------> realizeClass(cls);---------> imp = cache_getImp(cls, sel); 重映射,再一次取,这是一个漫长的方法查找过程。代码如下,老长老长了,不想看可以跳过,看的话看我写的代码注释就可以了:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {//这里肯定为NO🙃
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
  
    runtimeLock.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();
       // 没有实现则走这个方法🙃
        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        //初始化🙃
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

//*********************************************************    
                ✨✨✨ 下面这段代码是重点✨✨✨
*********************************************************//
 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.
  这里再去一次缓存的原因是1.因为多线程异步执行等原因,🙃
  缓存中有了对应的imp;2.remap重映射的原因有了imp🙃
    imp = cache_getImp(cls, sel);
    if (imp) goto done;//找到了就结束🙃

    // Try this class's method lists.
    {//没找到继续getMethodNoSuper_nolock,传入class ,sel,具体方法实现我下面也给出来了🙃
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到了,缓存起来🙃
            imp = meth->imp;
            goto done;//找到了,结果,回去。🙃
        }
    }
    //上面的流程没找到,开始找父类了 superclass🙃
    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;//nil的加入是到了NSObject可以结束了,上面没有了🙃
             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;
                }
            }
            
           //再次调用getMethodNoSuper_nolock(curClass, sel);进行循环了进行循环了进行循环了,这是一个漫长的过程🙃
            // 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;
            }
        }
    }
   
   //一直找不到,从这里开始开始动态方法解析了 🙃
    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

这里把上面过程中cache_getImp(cls, sel)没有找到后--->调用的getMethodNoSuper_nolock(cls, sel)展示给大家看一下:

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    //for 循环去方法列表中查找,从开始beginLists到结束endLists,把search_method_list查一遍🙃
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

而如果还是没有找到,则执行动态方法解析,就是最后的那一小段代码,我单独再抠出来:

动态方法解析

//一直找不到,从这里开始开始动态方法解析了 ,尝试方法解析一次,是的,once🙃
// No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
       //这里只会进来一次,triedResolver = YES;🙃
        runtimeLock.unlockRead();
//走这个方法🙃
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    //没有找到实现,方法解析也没有帮助,则使用转发,消息转发来了~~~🙃
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
/***********************************************************************
✨✨✨Call +resolveClassMethod or +resolveInstanceMethod.✨✨✨
**********************************************************************/
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);
        }
    }
}

我们这里直接查看对象方法的实现_class_resolveInstanceMethod(cls, sel, inst);

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //消息转发来了~~~ msg(cls, SEL_resolveInstanceMethod, sel);🙃
    BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

可以看到里面通过msg(cls, SEL_resolveInstanceMethod, sel);进行了消息的转发操作;通常消息转发和动态方法解析是互不相干的,在进入消息转发机制之前,respondsToSelector:instancesRespondToSelector:会被首先调用。您可以在这两个 方法中为传进来的选标提供一个IMP。如果您实现了resolveInstanceMethod:方法但是仍然希望正 常的消息转发机制进行,您只需要返回NO就可以了,否则消息转发流程不执行。

OC中有两个方法可以调用:resolveClassMethodresolveInstanceMethod,既然有OC的方法,果断上demo尝试一下啦!(demo和源码都放在最后面了)其实就是借着一开始的那个例子,LGPerson内写一个类方法 +(void)walk;然后.mwalk的实现不写,我们用resolveClassMethod代替,如下:

+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"来了 老弟");
    return  [super resolveClassMethod:sel];
}

//*******************************************
2019-08-10 16:03:33.168525+0800 002---runtime动态解析[5727:409195] 来了 老弟
2019-08-10 16:03:36.161490+0800 002---runtime动态解析[5727:409195] 来了 老弟

会补救2次,还是没有,消息转发流程,最后抛出异常,崩溃...
这里打个断点我们通过lldb打印一下看看:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001056f660b 002---runtime动态解析`+[LGPerson resolveInstanceMethod:](self=LGPerson, _cmd="resolveInstanceMethod:", sel="run") at LGPerson.m:23:5
    frame #1: 0x0000000105fdae76 libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 91
    frame #2: 0x0000000105fd54e5 libobjc.A.dylib`lookUpImpOrForward + 464
    frame #3: 0x0000000105fe30d4 libobjc.A.dylib`_objc_msgSend_uncached + 68

第二次执行

2019-08-10 16:10:07.579418+0800 002---runtime动态解析[5786:412162] 来了 老弟
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001056f660b 002---runtime动态解析`+[LGPerson resolveInstanceMethod:](self=LGPerson, _cmd="resolveInstanceMethod:", sel="run") at LGPerson.m:23:5
    frame #1: 0x0000000105fdae76 libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 91
    frame #2: 0x0000000105fd54e5 libobjc.A.dylib`lookUpImpOrForward + 464
    frame #3: 0x0000000105fd529a libobjc.A.dylib`class_getInstanceMethod + 50
    frame #4: 0x0000000106a48dfb CoreFoundation`__methodDescriptionForSelector + 299
    frame #5: 0x0000000106a48ea6 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
    frame #6: 0x0000000106a2f1fa CoreFoundation`___forwarding___ + 378
    frame #7: 0x0000000106a31418 CoreFoundation`__forwarding_prep_0___ + 120

看着打印是不是还是有点懵逼,没啥,👆的流程打印和图进行对照分析,就很清晰了,上图:(好尴尬,图不在这个电脑。。。后面补)


消息转发流程.png

消息转发

动态方法解析失败后----->进入消息的转发:
运行时系统会在抛出错误前,给该对象发送一条forwardInvocation:消息,该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了 原始的消息和消息的参数。您可以实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以以其它的某种 方式来避免错误被抛出。如 forwardInvocation:的名字所示,它通常用来将消息转发给其它的对象。

关于消息转发的作用,您可以考虑如下情景:假设,您需要设计一个能够响应 negotiate 消息的对象, 并且能够包括其它类型的对象对消息的响应。 通过在 negotiate 方法的实现中将 negotiate 消息 转发给其它的对象来很容易的达到这一目的。
更进一步,假设您希望您的对象和另外一个类的对象对negotiate的消息的响应完全一致。一种可能的 方式就是让您的类继承其它类的方法实现。 然后,有时候这种方式不可行,因为您的类和其它类可能需要
在不同的继承体系中响应 negotiate 消息。 虽然您的类无法继承其它类的negotiate方法,您仍然可以提供一个方法实现,这个方法实现只是简单
的将 negotiate 消息转发给其他类的对象,就好像从其它类那儿“借”来的现一样。(消息的转发,后面直接来个demo实例讲解下就能很好理解了,上面的官方语言只是让我们知道消息转发能干嘛)。
要转发消息给其它对象,forwardInvocation:方法所必须做的有:

以上就是通过方法的实质探查runtime的全部解析过程,这里大家再来看看思维导图对比一下,可能会清晰很多,两张图是两次总结Runtime画的,凑活看:


runtime
RunTime2

分析Runtime时,有一个很重要的东西,叫isa,关于isa,我会在后面文章中介绍。
最后,奉上demo下载地址(还没整理,后面补);
最后的最后,送大家一句名言共勉:
将来的你一定会感谢现在奋斗的自己。 ----李清照

上一篇下一篇

猜你喜欢

热点阅读