Runloop & KVO

runloop

runloop对于一个标准的iOS开发来说都不陌生,应该说熟悉runloop是标配,下面就随便列几个典型问题吧

1. app如何接收到触摸事件的
  1. 首先,手机中处理触摸事件的是硬件系统进程 ,当硬件系统进程识别到触摸事件后,会将这个事件进行封装,并通过machPort,将封装的事件发送给当前活跃的APP进程。
  2. 由于APP的主线程中runloop注册了这个machPort端口,就是用于接收处理这个事件的,所以这里APP收到这个消息后,开始寻找响应链
  3. 寻找到响应链后,开始分发事件,它会优先发送给手势集合,来过滤这个事件,一旦手势集合中其中一个手势识别了这个事件,那么这个事件将不会发送给响应链对象。
  4. 手势没有识别到这个事件,事件将会发送给响应链对象UIResponser
UIResponder

每个响应者都是一个UIResponder对象,即所有派生自UIResponder的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:

  • UIView
  • UIViewController
  • UIApplication
  • AppDelegate

响应者之所以能响应事件,因为其提供了4个处理触摸事件的方法:

//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

事件的三徒弟UIResponder、UIGestureRecognizer、UIControl

手势识别器比UIResponder具有更高的事件响应优先级!!

UIControl是系统提供的能够以target-action模式处理触摸事件的控件,iOS中UIButtonUISegmentedControlUISwitch等控件都是UIControl的子类。当UIControl跟踪到触摸事件时,会向其上添加的target发送事件以执行action。值得注意的是,UIConotrol是UIView的子类,因此本身也具备UIResponder应有的身份。

关于UIControl,此处介绍两点:

  1. target-action执行时机及过程
  2. 触摸事件优先级
image.png

UIControl比其父视图上的手势识别器具有更高的事件响应优先级
同一控件上,手势识别器比UIControl

参考文章

2. 为什么只有主线程的runloop是开启的

app启动前会调用main函数,具体如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

mian函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行,所以在主线程中开启一个runloop,让主线程常驻。

3. 为什么只在主线程刷新UI

原因一:安全+效率
UIKit不是线程安全的,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。

另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。

4. PerformSelector和runloop的关系
一 基础用法

performSelecor响应了OC语言的动态性:延迟到运行时才绑定方法。当我们在使用以下方法时:

[obj performSelector:@selector(xxx)];
[obj performSelector:@selector(xxx:) withObject:@"xxx"];
[obj performSelector:@selector(xxx: with:) withObject:@"xxx" withObject:@"xxx"];

编译阶段并不会去检查方法是否有效存在,只会给出警告。所以在实际开发中,为了避免运行时突然报错找不到方法等问题,少使用performSelector方法。

二 延迟执行
[obj performSelector:@selector(xxx:) withObject:@"xxx" afterDelay:4.f];

NSTimer:当一个NSTimer注册到Runloop后,Runloop会重复的在相应的时间点注册事件,当然Runloop为了节省资源并不会在准确的时间点触发事件。
performSelector:withObject:afterDelay:其实就是在内部创建了一个NSTimer,然后会添加到当前线程的Runloop中。所以当该方法添加到子线程中时,需要格外的注意两个地方:

① 在子线程中执行会不会调用test方法

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
});

会发现test方法并没有被调用,因为子线程中的runloop默认是没有启动的状态。使用run方法开启当前线程的runloop,但是一定要注意run方法和执行该延迟方法的顺序。

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
         [[NSRunLoop currentRunLoop] run];
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
});

会发现即便添加了run方法,但是test方法还是没有被调用,在最后打印当前线程的runloop,会发现:

timers = <CFArray 0x6000002a8100 [0x109f67bb0]>{type = mutable-small, count = 1, values = (
    0 : <CFRunLoopTimer 0x6000001711c0 [0x109f67bb0]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 544280547 (1.98647892 @ 3795501066754), callout = (Delayed Perform) lZLearningFromInterviewController test (0x105ea0d9c / 0x104b2e2c0) (), context = <CFRunLoopTimer context 0x600000470080>}

子线程的runloop中确实添加了一个CFRunLoopTimer的事件,但是到最后都不会被执行。
将run方法和performSelector延迟方法调换顺序后运行:

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
        [[NSRunLoop currentRunLoop] run];
});

所以在子线程中两者的顺序必须是先执行performSelector延迟方法之后再执行run方法。因为run方法只是尝试想要开启当前线程中的runloop,但是如果该线程中并没有任何事件(source、timer、observer)的话,并不会成功的开启.

② test方法中执行的线程

 [self performSelector:@selector(test) withObject:nil afterDelay:2];

如果在子线程中调用该performSelector延迟方法,会发现调用该延迟方法的子线程和test方法中执行的子线程是同一个,也就是说:
对于该performSelector延迟方法而言,如果在主线程中调用,那么test方法也是在主线程中执行;如果是在子线程中调用,那么test也会在该子线程中执行。

在回答完延迟方法之后,会将该方法和performSelector:withObject:作对比,那么performSelector:withObject:在不添加到子线程的Runloop中时是否能执行?

performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行。

三 异步执行

如何在不使用GCD和NSOperation的情况下,实现异步线程?

  1. NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[NSThread detachNewThreadSelector:@selector(test) toTarget:self withObject:nil];
[NSThread detachNewThreadWithBlock:^{

        NSLog(@"block中的线程 ---- %@",[NSThread currentThread]);
}];
  1. performSelectorInBackground
 [self performSelectorInBackground:@selector(test) withObject:nil];

该方法一目了然,开启新的线程在后台执行test方法

  1. performSelector:onThread:在指定线程执行
waitUntilDone 当这个参数为YES,时表示当前runloop循环中的时间马上响应这个事件,
如果为NO则runloop会将这个事件加入runloop队列在合适的时间执行这个事件

[self performSelector:@selector(test) 
onThread:[NSThread currentThread] 
withObject:nil 
waitUntilDone:YES];

要注意,在执行performSelector:onThread:withObject:waitUntilDone方法时候,如果是在另外一个线程执行,必须保证另外的线程是有一个runloop.具体的使用可以参考AFNetworking.

四 多参传递

performSelector如何进行多值传输?

  1. NSArray或者NSDictionary或者自定义Model的形式
//封装参数
    NSDictionary *dic = @{@"param1":@"this is a string",@"param2":@[@2,@3,@3],@"param3":@123};
//调用方法
    [self performSelector:@selector(testFunctionWithParams:) withObject:dic];

- (void)testFunctionWithParams:(NSDictionary *)paramDic {
    NSLog(@"%s dic:%@",__FUNCTION__, paramDic);
}
  1. objc_msgSend:
{
    NSNumber *age = [NSNumber numberWithInt:20];
    NSString *name = @"李周";
    NSString *gender = @"女";
    NSArray *friends = @[@"谢华华",@"亚呼呼"];

    SEL selector = NSSelectorFromString(@"getAge:name:gender:friends:");
    NSArray *array = @[age,name,gender,friends];

    ((void(*)(id,SEL,NSNumber*,NSString*,NSString*,NSArray*)) objc_msgSend)(self,selector,age,name,gender,friends);

}

- (void)getAge:(NSNumber *)age name:(NSString *)name gender:(NSString *)gender friends:(NSArray *)friends
{
    NSLog(@"%d----%@---%@---%@",[age intValue],name,gender,friends[0]);
}
  1. 用NSInvocation传递
//可以传多个参数的方法
- (id)performSelector:(SEL)selector withObjects:(NSArray *)objects
{
    // 方法签名(方法的描述)
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
    if (signature == nil) {
        
        //可以抛出异常也可以不操作。
    }
    
    // NSInvocation : 利用一个NSInvocation对象包装一次方法调用(方法调用者、方法名、方法参数、方法返回值)
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    
    // 设置参数
    NSInteger paramsCount = signature.numberOfArguments - 2; // 除self、_cmd以外的参数个数
    paramsCount = MIN(paramsCount, objects.count);
    for (NSInteger i = 0; i < paramsCount; i++) {
        id object = objects[i];
        if ([object isKindOfClass:[NSNull class]]) continue;
        [invocation setArgument:&object atIndex:i + 2];
    }
    
    // 调用方法
    [invocation invoke];
    
    // 获取返回值
    id returnValue = nil;
    if (signature.methodReturnLength) { // 有返回值类型,才去获得返回值
        [invocation getReturnValue:&returnValue];
    }
    
    return returnValue;
}

//调用方法
NSArray *paramArray = @[@"112",@[@2,@13],@12];
    [self performSelector:@selector(textFunctionWithParam:param2:param3:) withObjects:paramArray];


//要调用的方法
-(void)textFunctionWithParam:(NSString *)param1 param2:(NSArray *)param2 param3:(NSInteger)param3 {
    NSLog(@"param1:%@, param2:%@, param3:%ld",param1, param2, param3);
}
5. 如何使线程保活
  • 在NSThread执行的方法中添加while(true){},这样是模拟runloop的运行原理,结合GCD的信号量,在{}中处理任务。参考这篇文章
  • 采用runloop的方式。参考这篇文章

KVO

1. 实现原理

KVO 的实现也依赖于 Objective-C 强大的 Runtime

简单概述下 KVO 的实现:
当你观察一个对象时,一个新的类NSKVONotifying_XXX会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前之后,通知所有观察对象值的更改。最后把这个对象的isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例

setter方法前后会调用:

  • willChangeValueForKey
  • didChangeValueForKey
2. 如何手动关闭kvo
  • 重写被观察对象的automaticallyNotifiesObserversForKey方法,返回NO
  • 重写automaticallyNotifiesObserversOf<key>,返回NO

注意:关闭kvo后,需要手动在赋值前后添加willChangeValueForKeydidChangeValueForKey,才可以收到观察通知。

参考这篇文章

3. 通过KVC修改属性会触发KVO么

4. 哪些情况下使用kvo会崩溃,怎么防护崩溃
  1. 多次重复移除同一个属性,移除了未注册的观察者
    解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。
  2. dealloc 没有移除 kvo 观察者
    解决方案:创建一个中间对象,将其作为某个属性的观察者,然后dealloc的时候去做移除观察者,而调用者是持有中间对象的,调用者释放了,中间对象也释放了,dealloc 也就移除观察者了;
  3. 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context:方法,导致崩溃;
  4. 添加或者移除时 keypath == nil,导致崩溃;

以下解决方案出自 iOS 开发:『Crash 防护系统』(二)KVO 防护 一文。
其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象,参考KVOController

5. kvo的优缺点

缺点补充:

  • 只能通过重写 -observeValueForKeyPath:ofObject:change:context:方法来获得通知。
  • 不同通过指定selector的方式获取通知。
  • 不能通过block的方式获取通知。

参考这篇文章

6. 如何去实现带Block的KVO?

首先,我们创建 NSObject 的 Category,并在头文件中添加两个 API:

typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block;

- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

接下来,实现 ·PG_addObserver:forKey:withBlock: ·方法。逻辑并不复杂:

  1. 检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
  2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;
  3. 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
  4. 添加这个观察者
- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block
{
    // Step 1: Throw exception if its class or superclasses doesn't implement the setter
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        // throw invalid argument exception
    }
    
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    
    // Step 2: Make KVO class if this is first time adding observer and 
    //          its class is not an KVO class yet
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    // Step 3: Add our kvo setter method if its class (not superclasses) 
    //          hasn't implemented the setter
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
// Step 4: Add this observation info to saved observation objects
    PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

第一步里,先通过 setterForGetter()方法获得相应的 setter 的名字(SEL)。也就是把 key 的首字母大写,然后前面加上 set 后面加上 :,这样 key 就变成了 setKey:。然后再用 class_getInstanceMethod 去获得 setKey: 的实现(Method)。如果没有,自然要抛出异常。

第二步,我们先看类名有没有我们定义的前缀。如果没有,我们就去创建新的子类,并通过 object_setClass() 修改 isa 指针。

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

动态创建新的类需要用 objc/runtime.h 中定义的 objc_allocateClassPair() 函数。传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个类。然后就给这个类添加方法,也可以添加变量。这里,我们只重写了 class 方法。哈哈,跟 Apple 一样,这时候我们也企图隐藏这个子类的存在。最后 objc_registerClassPair() 告诉 Runtime 这个类的存在。

第三步,重写 setter 方法。新的 setter 在调用原 setter 方法后,通知每个观察者(调用之前传入的 block )

static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        // throw invalid argument exception
    }
    
    id oldValue = [self valueForKey:getterName];
    
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    for (PGObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

细心的同学会发现我们对 objc_msgSendSuper 进行类型转换。在 Xcode 6 里,新的 LLVM 会对 objc_msgSendSuper 以及 objc_msgSend 做严格的类型检查,如果不做类型转换。Xcode 会抱怨有 too many arguments 的错误。(在 WWDC 2014 的视频 What new in LLVM 中有提到过这个问题。)

最后一步,把这个观察的相关信息存在 associatedObject 里。观察的相关信息(观察者,被观察的 key, 和传入的 block )封装在 PGObservationInfo 类里。

@interface PGObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PGObservingBlock block;

@end

就此,一个基本的 KVO 就可以 work 了。当然,这只是一个一天多做出来的小东西,会有 bug,也有很多可以优化完善的地方。

完整的例子可以从这里下载:ImplementKVO

7. KVOController的原理?

Facebook开源框架

KVO 存在的问题
KVO 本身写起来并不友好,存在一些问题:

  • 需要手动移除观察者
  • 处理观察事件需要和注册观察事件割裂开

那么如何解决呢?

没有什么是一个中间变量不能解决的。可以创建一个实例,观察的事件由它分发,在其 dealloc 方法中移除观察者。这样就不用在外部业务方法中移除了。KVOController 也是这么做的。

总结
整个库的流程是:

  1. KVOController观察的对象作为其 NSMapTable 属性 _objectInfomap 的,把整个回调环境组成的对象 KVOInfo 作为保存起来。同时通过一个单例的 KVOSharedController 执行具体的注册与监听方法。

  2. KVO 自动取消监听的核心在于让 KVOController 这类的中间类的生命周期和被监听的 object 同步,而不是和 Observer 同步。因为,只有在被监听对象回收的时候取消监听才能真正避免 crash 的危险。

NSMapTable
NSMapTable 相比较 NSDictionary 的优势有:

  • NSDictionary 必须是 key-obj 的形式,key 必须是满足 NSCopying 协议的;NSMapTable 则是 obj-obj 的形式
  • NSDictionary 的 obj 是强引用;NSMapTable 的 key 和 value 都可以自己决定是强引用还是弱引用。如果弱引用回收后,会自动删除
[[NSMapTable alloc] initWithKeyOptions:keyOptions 
valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality 
capacity:0];

NSMapTable 的选项
NSMapTableStrongMemory (a “memory option”)
NSMapTableWeakMemory (a “memory option”)
NSMapTableObjectPointerPersonality (a “personality option”)
NSMapTableCopyIn (a “copy option”)

参考文章

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

推荐阅读更多精彩内容

  • 面向对象的三大特征,并作简单的介绍。 面向对象的三个基本特征是:封装、继承、多态。 1.封装是面向对象的特征之一,...
    xiny123阅读 1,422评论 0 6
  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    iOS菜鸟大大阅读 700评论 0 1
  • 面向对象的三大特性:封装、继承、多态 OC内存管理 _strong 引用计数器来控制对象的生命周期。 _weak...
    运气不够技术凑阅读 1,083评论 0 10
  • 翻译来源: RunLoops Run Loops RunLoops是与线程紧密相关的基础架构的一部分,简称运行循环...
    AlexCorleone阅读 563评论 0 1
  • 反射机制实在【运行状态】中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意...
    孔嘚嘚儿阅读 253评论 0 1