KVO子类的创建过程

KVO的实现原理是利用runtime动态生成一个被观察对象的子类,重新子类的4个方法,实现通知监听者。

一。子类的生成

1.runtime动态生成类

先调用 objc_allocateClassPair函数

/* Adding Classes */

/** 
 * Creates a new class and metaclass.
 * 
 * @param superclass The class to use as the new class's superclass, or \c Nil to create a new root class.
 * @param name The string to use as the new class's name. The string will be copied.
 * @param extraBytes The number of bytes to allocate for indexed ivars at the end of 
 *  the class and metaclass objects. This should usually be \c 0.
 * 
 * @return The new class, or Nil if the class could not be created (for example, the desired name is already in use).
 * 
 * @note You can get a pointer to the new metaclass by calling \c object_getClass(newClass).
 * @note To create a new class, start by calling \c objc_allocateClassPair. 
 *  Then set the class's attributes with functions like \c class_addMethod and \c class_addIvar.
 *  When you are done building the class, call \c objc_registerClassPair. The new class is now ready for use.
 * @note Instance methods and instance variables should be added to the class itself. 
 *  Class methods should be added to the metaclass.
 *
 *  创建一个类或者元类
 *  superclass: 父类的名称
 *  name : 创建的新的类名
 *  extraBytes:ivars 分配的字节
 *
 *  如果类名已经存在,则返回nil,否则返回新的class
 *
 *  class_addMethod 添加方法
 *  class_addIvar 添加成员变量
 *  执行完成之后需要调用objc_registerClassPair函数
 */
OBJC_EXPORT Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, 
                       size_t extraBytes) 



/***********************************************************************
* objc_allocateClassPair
* fixme
* Locking: acquires runtimeLock
* 动态创建一个类
**********************************************************************/
Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
{
    Class cls, meta;
    const char *superName = object_getClassName(superclass);
    printf("\n objc_allocateClassPair: 父类:%s-----子类:%s\n",superName,name);
    
    
    // Fail if the class name is in use.
    // 查找名称为name的类是否存在,如果存在,返回nil
    if (look_up_class(name, NO, NO)) return nil;

    mutex_locker_t lock(runtimeLock);

    // Fail if the class name is in use.
    // Fail if the superclass isn't kosher.
    // 父类非法,返回nil
    if (getClassExceptSomeSwift(name)  ||
        !verifySuperclass(superclass, true/*rootOK*/))
    {
        return nil;
    }

    // Allocate new classes.
    // 创建一个新类
    cls  = alloc_class_for_subclass(superclass, extraBytes);
    meta = alloc_class_for_subclass(superclass, extraBytes);
    
    // fixme mangle the name if it looks swift-y?
    objc_initializeClassPair_internal(superclass, name, cls, meta);

    return cls;
}

1.1先调用 objc_allocateClassPair函数中调用look_up_class,判断需要创建的类是否存在

Class 
look_up_class(const char *name, 
              bool includeUnconnected __attribute__((unused)), 
              bool includeClassHandler __attribute__((unused)))
{
    if (!name) return nil;

    Class result;
    bool unrealized;
    {
        runtimeLock.lock();
        result = getClassExceptSomeSwift(name);
        // 判断name是否存在
        unrealized = result  &&  !result->isRealized();
        if (unrealized) {
            result = realizeClassMaybeSwiftAndUnlock(result, runtimeLock);
            // runtimeLock is now unlocked
        } else {
            runtimeLock.unlock();
        }
    }

    if (!result) {
        // Ask Swift about its un-instantiated classes.

        // We use thread-local storage to prevent infinite recursion
        // if the hook function provokes another lookup of the same name
        // (for example, if the hook calls objc_allocateClassPair)

        auto *tls = _objc_fetch_pthread_data(true);

        // Stop if this thread is already looking up this name.
        for (unsigned i = 0; i < tls->classNameLookupsUsed; i++) {
            if (0 == strcmp(name, tls->classNameLookups[i])) {
                return nil;
            }
        }

        // Save this lookup in tls.
        if (tls->classNameLookupsUsed == tls->classNameLookupsAllocated) {
            tls->classNameLookupsAllocated =
                (tls->classNameLookupsAllocated * 2 ?: 1);
            size_t size = tls->classNameLookupsAllocated *
                sizeof(tls->classNameLookups[0]);
            tls->classNameLookups = (const char **)
                realloc(tls->classNameLookups, size);
        }
        tls->classNameLookups[tls->classNameLookupsUsed++] = name;

        // Call the hook.
        Class swiftcls = nil;
        if (GetClassHook.get()(name, &swiftcls)) {
            ASSERT(swiftcls->isRealized());
            result = swiftcls;
        }

        // Erase the name from tls.
        unsigned slot = --tls->classNameLookupsUsed;
        ASSERT(slot >= 0  &&  slot < tls->classNameLookupsAllocated);
        ASSERT(name == tls->classNameLookups[slot]);
        tls->classNameLookups[slot] = nil;
    }

    return result;
}

1.2 调用 objc_registerClassPair函数

/***********************************************************************
* objc_registerClassPair
* fixme
* Locking: acquires runtimeLock
 
 注册一个类
**********************************************************************/
void objc_registerClassPair(Class cls)
{
    
    
    const char *clsName = object_getClassName(cls);

    
    if (strstr(clsName, "NSKVONotifying_Person") ) {
        printf("\nobjc_registerClassPair:%s\n",clsName);

    }

    
    mutex_locker_t lock(runtimeLock);

    checkIsKnownClass(cls);

    //如果类已经存在,直接返回
    if ((cls->data()->flags & RW_CONSTRUCTED)  ||
        (cls->ISA()->data()->flags & RW_CONSTRUCTED)) 
    {
        _objc_inform("objc_registerClassPair: class '%s' was already "
                     "registered!", cls->data()->ro()->name);
        return;
    }
   
    
    // 必须调用过objc_allocateClassPair
    if (!(cls->data()->flags & RW_CONSTRUCTING)  ||  
        !(cls->ISA()->data()->flags & RW_CONSTRUCTING))
    {
        _objc_inform("objc_registerClassPair: class '%s' was not "
                     "allocated with objc_allocateClassPair!", 
                     cls->data()->ro()->name);
        return;
    }

    // Clear "under construction" bit, set "done constructing" bit
    cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);

    // Add to named class table.
    // 添加到table中
    addNamedClass(cls, cls->data()->ro()->name);
}

经过上面两步,一个新的子类会创建

二.KVO动态生成子类堆栈分析

在main函数中的测试代码

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Observer.h"


Observer *observer;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@" 进入 main 函数");
        observer = [[Observer alloc] init];
        
        Person *p = [[Person alloc] init];
        [p addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//
//        sleep(5);
//        [p removeObserver:observer forKeyPath:@"name"];
    }
    return 0;
}

给person对象的name属性添加KVO监听
在 objc_allocateClassPair函数中打上断点,可以看到如下堆栈

01.png

如图01所示:
当给一个类添加kvo后,runtime动态生成类的堆栈

  1. [NSObject(NSKeyValueObserverRegistration) addObserver:forKeyPath:options:context:]
  1. [NSObject(NSKeyValueObserverRegistration) _addObserver:forProperty:options:context:]:
  1. NSKeyValueUnnestedProperty isaForAutonotifying]
  1. [NSKeyValueUnnestedProperty _isaForAutonotifying]

5._NSKeyValueContainerClassGetNotifyingInfo

  1. _NSKVONotifyingCreateInfoWithOriginalClass
  1. objc_allocateClassPair

第6步的汇编代码


02.png

__NSSetObjectValueAndNotify 伪代码

//伪代码,仅供理解
void __NSSetObjectValueAndNotify(id self, SEL _cmd, id value) {
    //获取额外的变量
    void *indexedIvars = object_getIndexedIvars(object_getClass(self));
    //加锁
    pthread_mutex_lock(indexedIvars + 0x20);
    //从SEL获取KeyPath
    NSString *keyPath = [CFDictionaryGetValue(*(indexedIvars) + 0x18), _cmd) copyWithZone:0x0];
    //解锁
    pthread_mutex_unlock(indexedIvars + 0x20);
    
    //改变前发通知
    [self willChangeValueForKey:keyPath];
    //实现Setter方法
    IMP imp = class_getMethodImplementation(*indexedIvars, _cmd);
    (imp)(self, _cmd, value);
    //改变后发通知
    [self didChangeValueForKey:keyPath];
}

[NSObject addObserver:forKeyPath:options:context:] 伪代码

//伪代码,仅供理解
void -[NSObject addObserver:forKeyPath:options:context:]
(void * self, void * _cmd, void * arg2, void * arg3, unsigned long long arg4, void * arg5) {
    pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
    *__NSKeyValueObserverRegistrationLockOwner = pthread_self();
    rax = object_getClass(self);
    rax = _NSKeyValuePropertyForIsaAndKeyPath(rax, arg3);
    [self _addObserver:arg2 forProperty:rax options:arg4 context:arg5];
    *__NSKeyValueObserverRegistrationLockOwner = 0x0;
    pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
    
    return;
}



//伪代码,仅供理解
- (void *)_addObserver:(id)observer 
           forProperty:(NSKeyValueProperty *)property 
               options:(NSKeyValueObservingOptions)option 
               context:(void *)context {
    //需要注册通知
    if (option & NSKeyValueObservingOptionInitial) {
        //获取属性名路径
        NSString *keyPath = [property keyPath];
        //解锁
        pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
        //如果注册了获得新值,就获取数值
        id value = nil;
        if (option & NSKeyValueObservingOptionNew) {
            value = [self valueForKeyPath:keyPath];
            if (value == nil) {
                value = [NSNull null];
            }
        }
        //发送注册通知
        _NSKeyValueNotifyObserver(observer, keyPath, self, context, value, 
        0 /*originalObservable*/, 1 /*NSKeyValueChangeSetting*/);
        //加锁
        pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
    } 
    //获取属性的观察信息
    Info *info = __NSKeyValueRetainedObservationInfoForObject(self, property->_containerClass);
    //判断是否需要获取新的数值
    id _additionOriginalObservable = nil;
    if (option & NSKeyValueObservingOptionNew) {
        //0�x15没有找到定义,猜测为保存是否可观察的数组

        id tsd = _CFGetTSD(0x15);
        if (tsd != nil) {
            _additionOriginalObservable = *(tsd + 0x10);
        }
    }
    //在原有信息上生成新的信息
    Info *newInfo = __NSKeyValueObservationInfoCreateByAdding
    (info, observer, property, option, context, _additionOriginalObservable, 0, 1);
    //替换属性的观察信息
    __NSKeyValueReplaceObservationInfoForObject(self, property->_containerClass, info, newInfo);
    //属性添加后递归添加关联属性
    [property object:self didAddObservance:newInfo recurse:true];
    //获取新的isa
   // 如果一个对象是第一次被观察,需要替换isa对象
    Class cls = [property isaForAutonotifying];
    if ((cls != NULL) && (object_getClass(self) != cls)) {
        //如果是第一次就替换isa
        object_setClass(self, cls);
    }
    //释放观察信息
    [newInfo release];
    if (info != nil) {
        [info release];
    }
    return;
}


03.png

给一个类动态的添加method

// 给一个类添加方法
BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;
    
    
    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}
04.png

如图4所示,动态添加了_isKVOA

三. GNU addObserver 实现

// 添加观察者
- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  GSKVOInfo             *info;
  GSKVOReplacement      *r;
  NSKeyValueObservationForwarder *forwarder;
  NSRange               dot;

  setup();
  [kvoLock lock];

  // Use the original class
 // 先保存原来的类
  r = replacementForClass([self class]);

  /*
   * Get the existing observation information, creating it (and changing
   * the receiver to start key-value-observing by switching its class)
   * if necessary.
   */
    
 // 获取对象的观察者信息
  info = (GSKVOInfo*)[self observationInfo];
  // 如果info 为空
  if (info == nil)
    {
      //创建观察者信息
      info = [[GSKVOInfo alloc] initWithInstance: self];
      // 保存观察者信息
      [self setObservationInfo: info];
      // [r replacement] 就是子类,将self设置为子类类型(修改isa指向)
      object_setClass(self, [r replacement]);
    }

  /*
   * Now add the observer.
   
   */
    // 如果是深层路径
  dot = [aPath rangeOfString:@"."];
  if (dot.location != NSNotFound)
    {
      forwarder = [[NSKeyValueObservationForwarder alloc]
        initWithKeyPath: aPath
           ofObject: self
         withTarget: anObserver
        context: aContext];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: forwarder];
    }
  else
    {
      // 重新setter方法
      [r overrideSetterFor: aPath];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
    }

  [kvoLock unlock];
}

三移除KVO

   [p removeObserver:observer forKeyPath:@"name"];
06

如图6所示:当移除KVO时,如果该对象没有监听者,会销毁NSKeyValueObservance

释放对象

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

GNU removeObserver 实现

/*
 * removes the observer
  移除监听者
 */
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
  GSKVOPathInfo *pathInfo;

  [iLock lock];
    // 获取观察者信息
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
  if (pathInfo != nil)
    {
       // 移除path对应的所有观察者
      unsigned  count = [pathInfo->observations count];

      pathInfo->allOptions = 0;
      while (count-- > 0)
        {
          GSKVOObservation      *o;

          o = [pathInfo->observations objectAtIndex: count];
          if (o->observer == anObserver || o->observer == nil)
            {
              [pathInfo->observations removeObjectAtIndex: count];
              if ([pathInfo->observations count] == 0)
                {
                  NSMapRemove(paths, (void*)aPath);
                }
            }
          else
            {
              pathInfo->allOptions |= o->options;
            }
    }
    }
  [iLock unlock];
}
        observer = [[Observer alloc] init];
        Person *p = [[Person alloc] init];
        [p addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    
        [p removeObserver:observer forKeyPath:@"name"];
        Class cls = objc_lookUpClass("NSKVONotifying_Person");
    
        const char *clsName = object_getClassName(cls);
        NSLog(@" ------- %s",clsName);

打印结果-------NSKVONotifying_Person
说明移除KVO之后,NSKVONotifying_Person没有被销毁

NSKVODeallocate 实现

使用hopper查看苹果实现

int _NSKVODeallocate(int arg0, int arg1) {
    r13 = rdi;
    var_-48 = **___stack_chk_guard;
    rax = object_getClass(rdi);
    r12 = __NSKVOUsesBaseClassObservationInfoImplementationForClass(rax);
    rax = object_getIndexedIvars(rax);
    r14 = rax;
    rbx = class_getInstanceMethod(*rax, rsi);
    if (r12 == 0x0) goto loc_7fff207b448e;

loc_7fff207b4461:
    if (**___stack_chk_guard == var_-48) {
            rdi = r13;
            rsi = rbx;
            rax = method_invoke(rdi, rsi);
    }
    else {
            rax = __stack_chk_fail();
    }
    return rax;

loc_7fff207b448e:
    rax = __NSKeyValueRetainedObservationInfoForObject(r13, 0x0);
    *var_-72 = r13;
    *(var_-72 + 0x8) = rax;
    *(var_-72 + 0x10) = 0x0;
    __NSKeyValueAddObservationInfoWatcher(var_-72);
    r12 = __NSKVOObservationInfoOverridenObjectMayThrowOnDealloc(r13);
    method_invoke(r13, rbx);
    if (var_-64 == 0x0) goto loc_7fff207b4570;

loc_7fff207b44d1:
    r15 = dyld_get_program_sdk_version();
    if (r12 != 0x0) {
            r12 = (*_objc_msgSend)(var_-64, *0x7fff86b9d448) ^ 0x1;
    }
    else {
            r12 = 0x0;
    }
    *(int8_t *)var_-73 = 0x0;
    rax = CFPreferencesGetAppBooleanValue(@"NSKVODeallocateCleansUpBeforeThrowing", **_kCFPreferencesCurrentApplication, var_-73);
    rcx = 0x0;
    CMP(r15, 0x7ffff);
    rdx = r12 & 0xff;
    rsi = 0x0;
    asm{ cmova      esi, edx };
    rbx = (var_-73 == rcx ? 0x1 : 0x0) | (rax == 0x0 ? 0x1 : 0x0);
    if (rbx == 0x0) {
            rsi = rdx;
    }
    if (rsi != 0x0) goto loc_7fff207b45b4;

loc_7fff207b4542:
    if ((r15 < 0x80000) || (r12 != 0x0)) {
            _NSLog(@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debu…", r13, *r14);
            _NSKVODeallocateBreak(r13);
    }
    goto loc_7fff207b4570;

loc_7fff207b4570:
    __NSKeyValueRemoveObservationInfoWatcher(var_-72);
    [var_-64 release];
    if (0x0 == 0x0) {
            rax = *___stack_chk_guard;
            rax = *rax;
            if (rax != var_-48) {
                    rax = __stack_chk_fail();
            }
    }
    else {
            rax = objc_exception_rethrow();
    }
    return rax;

loc_7fff207b45b4:
    r15 = (*_objc_msgSend)(var_-64, *0x7fff86b9a5e8);
    if (rbx == 0x0) {
            __NSKeyValueRemoveObservationInfoForObject(var_-72);
    }
    rax = (*_objc_msgSend)(@class(NSString), *0x7fff86b9a4b8);
    rax = (*_objc_msgSend)(@class(), *0x7fff86b9a700);
    rax = objc_exception_throw(rax);
    return rax;
}

DIS_KVC_KVO的DSKVODeallocate实现

void DSKVODeallocate(id object, SEL selector) {
    DSKeyValueObservationInfo *observationInfo = _DSKeyValueRetainedObservationInfoForObject(object, nil);
    
    ObservationInfoWatcher watcher = {object, observationInfo, NULL};
    _DSKeyValueAddObservationInfoWatcher(&watcher);
    
    DSKeyValueNotifyingInfo *notifyInfo = (DSKeyValueNotifyingInfo *)object_getIndexedIvars(object_getClass(object));
    
    Method originDellocMethod = class_getInstanceMethod(notifyInfo->originalClass, selector);
    ((id (*)(id,Method))method_invoke)(object, originDellocMethod);
    
    @try {
        if(watcher.observationInfo) {
            BOOL keyExistsAndHasValidFormat = false;
            BOOL cleansUpBeforeThrowing = false;
            
            cleansUpBeforeThrowing = (BOOL)CFPreferencesGetAppBooleanValue(CFSTR("NSKVODeallocateCleansUpBeforeThrowing"), kCFPreferencesCurrentApplication, (Boolean *)&keyExistsAndHasValidFormat);
            
            cleansUpBeforeThrowing = cleansUpBeforeThrowing && keyExistsAndHasValidFormat;
            
            if (dyld_get_program_sdk_version() > 0x7FFFF || cleansUpBeforeThrowing) {
                if (cleansUpBeforeThrowing) {
                    _DSKeyValueRemoveObservationInfoForObject(object, watcher.observationInfo);
                }
                [NSException raise:NSInternalInconsistencyException format:@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Current observation info: %@", object, notifyInfo->originalClass, watcher.observationInfo];
            }
            else {
                NSLog(@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:\n%@", object, notifyInfo->originalClass, watcher.observationInfo);
                DSKVODeallocateBreak(object);
            }
        }

    }
    @catch (NSException *exception) {
        [exception raise];
    }
    @finally {
        _DSKeyValueRemoveObservationInfoWatcher(&watcher);
        
        [watcher.observationInfo release];
    }    
}

kvo子类重写dealloc的目的?

存在这样一种情况,person对象已经被销毁了,但是观察者没有被移除。从上面的分析可以看到 NSKeyValueObservance就无法销毁。当person对象销毁时,会根据isa指针找到-dealloc 方法,由于观察者没有移除,isa指向 NSKVONotifying_Person类,这个类重写了-dealloc方法,会做一些清除kvo的操作,所以需要重新子类的-dealloc 方法。

KVO源码中添加观察者时整体的大致流程是什么?

  1. 将keyPath、class等信息封装成NSKeyValueProperty,分别解析一般属性(@”aa”)、可计算属性(@”@aa”)、属性链(@”aa.bb.@cc.dd“),进行子类化,缓存在CFMutableSet中方便下次快速取出。
  2. 将NSKeyValueProperty、context、options、observer等信息封装成NSKeyValueObservance,缓存在NSHashTable中。
  3. 倘若设置了NSKeyValueObservingOptionInitial选项,会在注册观察服务时调用一次触发方法。
  4. 动态创建名为NSKVONotifying_+原来类名的新类,重写其dealloc、_isKVOA方法,再重写class方法,利用object_setClass()函数将其isa指针指向原先的类。
  5. 重写willChangeValueForKey:和didChangeValueForKey:方法,重写被观察属性的setter方法,在setter中先调用willChangeValueForKey:方法,然后调用父类的 setter 方法对成员变量赋值,之后再调用 didChangeValueForKey: 方法。
  6. didChangeValueForKey: 方法中会调用observeValueForKeyPath:ofObject:change:context:方法。

引用文章


刨根问底KVO原理
Principle and application of KVO
DIS_KVC_KVO
KVO

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • iOS 底层探索系列iOS 底层探索 - alloc & initiOS 底层探索 - calloc 和 isai...
    leejunhui阅读 675评论 0 1
  • 序言在iOS开发中,苹果提供了许多机制给我们进行回调。KVO(key-value-observing)是一种十分有...
    陌尚煙雨遙阅读 456评论 0 0
  • 目录:1.KVC用法;2.KVC和对象的setter、getter方法的区别;3.key和keyPath的区别;4...
    伦伦子_f7b3阅读 551评论 0 1
  • [TOC] (一)KVO 初探 1. 基本用法 添加观察 监听观察 移除观察 通知使用完之后,一定要移除,否则会有...
    修_远阅读 241评论 0 5
  • 最近学习Runtime,顺便总结一下在Objective-C中KVO使用到的Runtime机制。 系统的KVO使用...
    aron1992阅读 167评论 0 0