🔥 iOS Development ☛【面试题】

1、为什么说Objective-C是一门动态的语言?

 动态和静态是相对的, 动态是不需要在编译时确定所有的东西, 而是在运行时动态添加变量, 属性, 方法和类. 程序在运行时判断其该有的行为,而不是像C等静态语言在编译构建时就确定下来。
动态性主要体现在3个方面: 
1.动态类型:如id类型。实际上静态类型因为其固定性和可预知性而使用的特别广泛。静态类型是强类型,动态类型是弱类型,运行时决定接收者。 
2.动态绑定:让代码在运行时判断需要调用什么方法,而不是在编译时。与其他面向对象语言一样,方法调用和代码并没有在编译时连接在一起,而是在消息发送时才进行连接。运行时决定调用哪个方法。 
3.动态载入。让程序在运行时添加代码模块以及其他资源。用户可以根据需要执行一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。

2、Objective-C的类可以多重继承么?可以实现多个接口么?

Objective-C的类不可以多重继承, 它仅仅是单继承, 
可以实现多个接口;

多继承即一个子类可以有多个父类,它继承了多个父类的特性。

多重继承实现方法一:
现在ClassC需要继承ClassA中methodA、ClassB中methodB,代码实现为:

-------------------------------------------

// 定义ClassA以及其methodA
@interface ClassA : NSObject {
}

- (void)methodA;

@end

-------------------------------------------

//定义ClassB以及其methodB
@interface ClassB : NSObject {
}

- (void)methodB;

@end

-------------------------------------------

//定义ClassC以及其需要的methodA,methodB
@interface ClassC : NSObject {
  ClassA *a;
  ClassB *b;
}

- (id)initWithClassA:(ClassA *)classA classB:(ClassB *)classB;

- (void)methodA;
- (void)methodB;

@end

// ClassC的实现
@implementation  ClassC

- (id)initWithClassA:(ClassA *)classA classB:(ClassB *)classB {
    a = [[ClassA alloc] initWithClassA:classA];
    b = [[ClassB alloc] initWithClassB:classB]; 
}

- (void)methodA {
    [a methodA];
}

- (void)methodB {
    [b methodB];
}

-------------------------------------------

多重继承实现方法二:
用协议来实现多继承。但是协议只能提供接口,而没有提供实现方式,如果只是想多继承基类的接口,那么遵守多协议无疑是最好的方法,而既需要多继承接口,又要多继承其实现,那么协议是无能为力了.

3、Category是什么?重写一个类的方式用继承好还是分类好?为什么?

Category是类别,也叫类目, 是对一个类的方法起到丰富性的作用; 
用Category重写类的方法,它仅仅只对本类有效,并不会影响到其他类和原有类的关系,如果是要在不修改原有类的基础上增加其他原有类没有的方法,就要用类目,继承是可以重写父类的方法,只是子类继承父类的方法来使用。

4、nonatomic 和 atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?

🍎 nonatomic:表示非原子,系统自动生成的[getter/setter]方法不会进行加锁操作不安全,但是效率高。 
🍎 atomic:表示原子,系统自动生成的[getter/setter]方法会进行加锁操作, 安全,但是效率低。 
---------------------------------------------------------------------
🍎 atomic,也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。
atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。
eg:  (atomic 环境下)
如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。所以atomic可并不能保证对象的线程安全。

5、 浅拷贝 与 深拷贝

本 质: 内存地址是否相同;

· copy 返回的是不可变对象(immutableObject)
· mutableCopy 返回的是可变对象(mutableObject)。

[不可变对象 copy]: 返回不可变对象, 浅拷贝, 指针的拷贝;
[不可变对象 mutableCopy]: 返回可变对象, 深拷贝, 指针和内存的拷贝;

[可变对象 copy]: 返回不可变对象, 深拷贝, 指针和内存和拷贝;
[可变对象 mutableCopy]: 返回可变对象, 深拷贝, 指针和内存的拷贝;

6、这个写法会出什么问题:@property (nonatomic, copy) NSMutableArray *array;

添加, 删除, 修改 数组内的元素的时候,程序会因为找不到对应的方法而崩溃。

原因:对NSMutableArray的copy为深拷贝, 是对指针和内存地址的拷贝, 但是返回的对象类型是不可变类型, 即:NSArray类型; 所以对一个不可变数组进行操作, 会发生异常而产生崩溃. 
如删除操作异常: [__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460

7、在Block内部修改局部变量需要做什么操作? 为什么?

使用 __block 关键字修饰变量
原因: Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
没有通过__block修饰则是将值传进去,而通过__block修饰之后是将变量的内存地址传进去,这样就可以修改变量的值。
1,局部变量 
局部自动变量,在Block中只读。Block定义时copy变量的值,在Block中作为常量使用,所以即使变量的值在Block外改变,也不影响他在Block中的值。 
2, static修饰符的静态变量或全局变量 
因为全局变量货静态变量在内存中的地址是固定的,Block在读取改变量值的时候是直接从其所在的内存读出的,获取到的是最新值,而不是在定义时copy的常量。 

8、如何让自定义的类具备Copy或者MutableCopy功能?

对于自定义的对象,默认是不遵守NSCopying/NSMutableCopying协议的,如果我们希望自定义对象可以调用copy/mutableCopy,则需要手动去实现。
· Copy需要遵守NSCopying协议,重写+copyWithZone
· NSMutableCopy需要遵守NSMutableCopying协议,重写+mutableCopyWithZone

9、Objective-C 如何对内存管理的,说说你的看法和解决方法?

答:的内存管理主要有三种方式ARC(自动内存计数)、MRC(手动内存计数)、ReleasePool(内存池)。
1). ARC:由Xcode自动在App编译阶段,在代码中添加内存管理代码。
2). MRC:遵循内存谁申请、谁释放;谁添加,谁释放的原则。
3). Release Pool:把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有的内存空间也被自动释放掉。内存池的释放操作分为自动和手动。自动释放受runloop机制影响。

10、我们说的OC是动态运行时语言是什么意思?

主要是将数据类型的确定由编译时,推迟到了运行时。简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。

11、delegate常用的属性修饰符是什么? 为什么?

原因: 防止发生循环引用导致对象无法释放, 从而发生内存泄露;
简单点来讲strong属性会使引用计数+1,而weak修饰的对象不会使引用计数改变. 

12、KVC的实现原理?

KVC: Key-value coding, 键值编码.
当一个对象调用setValue方法时,方法内部调用顺序:
 [set方法] -> [_key] -> [property key]
 如果以上都没有找到,则调用(valueForUndefinedKey:)和(setValue:forUndefinedKey:)抛出异常。

13、KV0的实现原理?

KVO: Key-Value Observing,  是基于Runtime实现的;

当观察对象A时,KVO动态创建了新的名为NSKVONotifying_xxx(Person则生成:NSKVONotifying_Person)的新类,该类为对象A的子类,并且KVO重写了新类的观察属性的setter方法,setter方法负责在调用原setter方法之前和之后,通知所有观察者该属性的变化情况

派生类NSKVONotifying_xxx的setter方法的重写:
- (void)setName:(NSString *)newName {
  [self willChangeValueForKey:@"name"];    // KVO在调用存取方法之前总调用
  [super setValue:newName forKey:@"name"]; // 调用父类的存取方法
  [self didChangeValueForKey:@"name"];     // KVO在调用存取方法之后总调用
}

14、什么是method swizzling?

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。 
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

利用 class_replaceMethod 来修改类,
利用 method_exchangeImplementations 来交换两个方法中的IMP,
利用 method_setImplementation 来直接设置某个方法的IMP, 
…… 

15、Category能否添加成员变量? 为什么? Category中能添加属性、方法吗?为什么?

· 不能动态添加成员变量, 原因: 在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改. 所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数;

· 可以动态添加属性、方法, 实现方式: 
1) 关联属性 (类别中添加的属性, 不会自动生成getter 和 setter方法, 需手动实现)

@property (copy, nonatomic) NSString *name;

static char *nameKey = "nameKey";

- (void)setName:(NSString *)name {
  objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
  return objc_getAssociatedObject(self, nameKey);
}

2) 动态添加方法: 
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 实例方法
+ (BOOL)resolveClassMethod:(SEL)sel;    // 类方法

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(drive)) {
    class_addMethod([self class], sel, (IMP)startEngine, "v@:@");
    return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

16、简述ViewController生命周期

按照执行顺序排列:
1. initWithCoder:通过nib文件初始化时触发。
2. awakeFromNib:nib文件被加载的时,会发生一个awakeFromNib的消息到nib文件中的每个对象。      
3. loadView:开始加载视图控制器自带的view。
4. viewDidLoad:视图控制器的view被加载完成。  
5. viewWillAppear:视图控制器的view将要显示在window上。
6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
9. viewDidAppear:视图控制器的view已经展示到window上。 
10. viewWillDisappear:视图控制器的view将要从window上消失。
11. viewDidDisappear:视图控制器的view已经从window上消失。

17、OC中的反射机制?简单聊一下概念和使用

1). Class反射
    通过类名的字符串形式实例化对象。
        Class class = NSClassFromString(@"student"); 
        Student *stu = [[class alloc] init];
    将类名变为字符串。
        Class class =[Student class];
        NSString *className = NSStringFromClass(class);
2). SEL的反射
    通过方法的字符串形式实例化方法。
        SEL selector = NSSelectorFromString(@"setName");  
        [stu performSelector:selector withObject:@"Mike"];
    将方法变成字符串。
        NSStringFromSelector(@selector*(setName:));

18、什么是懒加载?

懒加载,亦叫延迟加载,即在第一次需要的时候才去加载,本质上就是对一个实例的getter方法的重写。
注意: 在getter方法内部不能使用self.去调用对象,因为点调用实际就是调用的getter方法,这样会造成循环调用,造成死循环。

优点: 
    使用懒加载可以讲对象的创建单独管理,维护起来更方便
    节省内存资源的耗损,只有当对象真正需要时才去创建。

19、Runtime如何实现weak 变量的自动置nil?

内部实现 —— Runtime维护了一个Weak哈希表;
Key  : 对象的内存地址;
Value: 所有Weak指针地址的数组;

简单来说,这个方法首先根据对象地址获取所以Weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从Weak表中删除。

PRIVATE_EXTERN void 
arr_clear_deallocating(weak_table_t *weak_table, id referent) {
    {
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == NULL) {
            /// XXX shouldn't happen, but does with mismatched CF/objc
            //printf("XXX no entry for clear deallocating %p\n", referent);
            return;
        }
        // zero out references
        for (int i = 0; i < entry->referrers.num_allocated; ++i) {
            id *referrer = entry->referrers.refs[i].referrer;
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable @ %p holds %p instead of %p\n", referrer, *referrer, referent);
                }
            }
        }
 
        weak_entry_remove_no_lock(weak_table, entry);
        weak_table->num_weak_refs--;
    }
}

20、如何访问并修改一个类的私有属性

1). 一种是通过KVC获取。
    setValue:@"" forKey:@"";
2). 通过runtime访问并修改私有属性。

    unsigned int count = 0; //记录属性的个数
    Ivar *members = class_copyIvarList([person class], &count); // 获取属性列表
    
    for (NSInteger i = 0; i < count; i++) { // 遍历属性列表
        Ivar ivar = members[i]; // 取到属性
        const char *memberName = ivar_getName(ivar); // 获取属性名
        NSString *ivarName = [NSString stringWithFormat:@"%s", memberName];
     
        //当属性名是_name的时候对其值进行修改
        if ([ivarName isEqualToString:@"_name"]) {
            object_setIvar(person, ivar, @"李四");
        }
    }

21、下面的代码输出什么?

@implementation Student : Person

- (id)init {
   if (self = [super init]) {
       NSLog(@"%@", NSStringFromClass([self class])); // 输出: Student
       NSLog(@"%@", NSStringFromClass([super class]));// 输出: Student
   }
   
   return self;
}
@end

// 解析:
self 是类的隐藏参数,指向当前调用方法的这个类的实例。
super是一个Magic Keyword,它本质是一个编译器标示符,和self是指向的同一个消息接收者。
不同的是:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类里的。
上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Student *obj 这个对象。

22、如何高性能的给 UIImageView 添加圆角

- (UIImage *)circleImage {
    // NO代表透明
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
    // 获得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    // 添加一个圆
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    CGContextAddEllipseInRect(ctx, rect);
    // 裁剪
    CGContextClip(ctx);
    // 将图片画上去
    [self drawInRect:rect];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    // 关闭上下文
    UIGraphicsEndImageContext();
    return image;
}

23、请简单的介绍下APNS发送系统消息的机制

APNS优势:杜绝了类似安卓那种为了接受通知不停在后台唤醒程序保持长连接的行为,由iOS系统和APNS进行长连接替代。
APNS的原理:
    1). 应用在通知中心注册,由iOS系统向APNS请求返回设备令牌(device Token)
    2). 应用程序接收到设备令牌并发送给自己的后台服务器
    3). 服务器把要推送的内容和设备发送给APNS
    4). APNS根据设备令牌找到设备,再由iOS根据APPID把推送内容展示

24、load 和 initialize有什么用处?

load:
对于加入运行期系统的类及分类,必定会调用此方法,且仅调用一次。
iOS会在应用程序启动的时候调用load方法,在main函数之前调用

执行子类的load方法前,会先执行所有超类的load方法,顺序为父类->子类->分类

在load方法中使用其他类是不安全的,因为会调用其他类的load方法,而如果关系复杂的话,就无法判断出各个类的载入顺序,类只有初始化完成后,类实例才能进行正常使用

load 方法不遵从继承规则,如果类本身没有实现load方法,那么系统就不会调用,不管父类有没有实现
load 方法中最常用的就是方法交换method swizzling

initialize: 
在首次使用该类之前由运行期系统(非人为)调用,且仅调用一次

惰性调用,只有当程序使用相关类时,才会调用

运行期系统会确保initialize方法是在线程安全的环境中执行,即,只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等待initialize执行完

如果类未实现initialize方法,而其超类实现了,那么会运行超类的实现代码,而且会运行两次(load 第5点)

initialize 遵循继承规则
初始化子类的的时候会先初始化父类,然后会调用父类的initialize方法,而子类没有覆写initialize方法,因此会再次调用父类的实现方法

25、 loadView的作用

当访问一个ViewController的view属性时,如果此时view的值是nil,那么,ViewController就会自动调用loadView这个方法:
1、如果你用了nib文件,重载这个方法就没有太大意义。因为loadView的作用就是加载nib。如果你重载了这个方法不调用super,那么nib文件就不会被加载。如果调用了super,那么view已经加载完了,你需要做的其他事情在viewDidLoad里面做更合适。

2、如果你没有用nib,这个方法默认就是创建一个空的view对象。如果你想自己控制view对象的创建,例如创建一个特殊尺寸的view,那么可以重载这个方法,自己创建一个UIView对象,然后指定 self.view = myView; 但这种情况也没有必要调用super,因为反正你也不需要在super方法里面创建的view对象。如果调用了super,那么就是浪费了一些资源而已 

26、为什么要在主线程更新UI? 回到主线程的方法是什么?

因为UIKit不是线程安全的。

试想下面这几种情况: 
· 两个线程同时设置同一个背景图片,那么很有可能因为当前图片被释放了两次而导致应用崩溃。 
· 两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A, 而此时在UIView逻辑树上的背景颜色属性为B。 
· 两个线程同时操作view的树形结构:在线程A中for循环遍历并操作当前View的所有subView,然后此时线程B中将某个subView直接删除,这就导致了错乱还可能导致应用崩溃。

回到主线程的三种方式:
// 1.NSThread
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];

// 2.NSOperationQueue
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    self.alert.text = @"Thanks!";
    }];

// 3.GCD
dispatch_async(dispatch_get_main_queue(), ^{
   self.alert.text = @"Thanks!";
});

27、 常见的object-c的数据类型有那些, 和C的基本数据类型有什么区别?如:NSInteger和int

object-c的数据类型有NSString,NSNumber,NSArray,NSMutableArray,NSData等等,这些都是class,创建后便是对象,而C语言的基本数据类型int,只是一定字节的内存空间,用于存放数值; NSInteger是基本数据类型,并不是NSNumber的子类,当然也不是NSObject的子类。NSInteger是基本数据类型Int或者Long的别名(NSInteger的定义typedef long NSInteger),它的区别在于,NSInteger会根据系统是32位还是64位来决定是本身是int还是Long。

28、 id 和 instancetype 声明的对象有什么异同

1、相同点
    都可以作为方法的返回类型

2、不同点
    1)instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象;
    2)instancetype只能作为返回值,不能像id那样作为参数,比如下面的写法:

29、 如何对iOS设备进行性能测试?

Profile-> Instruments ->Time Profiler

30、 设计模式是什么? 你知道哪些设计模式,并简要叙述?

设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的事情。
1). MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。
2). MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。
3). 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。
4). 观察者模式:KVO,观察某个属性的状态,状态发生变化时通知观察者。
5). 委托模式:代理+协议的组合。实现1对1的反向传值操作。
6). 工厂模式:通过一个类方法,批量的根据已有模板生产对象。

31.如何实现一个单例, 实现的原理是什么?

一个单例类,在整个程序中只有一个实例,并且提供一个类方法供全局调用,在编译时初始化这个类,然后一直保存在内存中,到程序(APP)退出时由系统自动释放这部分内存。

对象指针是保存在静态区的,单例对象在堆中分配的内存空间;
实现代码: (使用GCD保证了线程安全)

@implementation BHSingletonObject

static dispatch_once_t onceToken = 0;
static BHSingletonObject *singletonObject = nil;

+ (instancetype)sharedInstance {
    /*dispatch_once主要是根据onceToken的值来决定怎么去执行代码。
     当onceToken = 0 时,线程执行block中代码 (表示: 从未执行过)
     当onceToken = -1 时,线程跳过block中代码不执行 (表示: 已经执行过一次)
     当onceToken = 其他值 时,线程被线程被阻塞,等待onceToken值改变 (多线程中可见)
     */
    dispatch_once(&onceToken, ^{
        singletonObject = [[BHSingletonObject alloc] init];
    });

    return singletonObject;
}

// 销毁
+ (void)deallocInstance {
    onceToken = 0;
    singletonObject = nil;
}

32、Block有几种形式? 分别是什么?

根据Block对象创建时所处数据区不同而进行区别:
(1)全局静态 block(_NSConcreteGlobalBlock),不会访问任何外部变量,执行完就销毁。

(2)在栈中的 block(_NSConcreteStackBlock),当函数返回时会被销毁,和第一种的区别就是调用了外部变量。
 [UIView animateWithDuration:3 animations:^{
    self.view.backgroundColor = [UIColor redColor];
 }];
 
(3)在堆中的 block(_NSConcreteMallocBlock),当引用计数为 0 时会被销毁。例如按钮的点击事件,一直存在,即使执行过,也不销毁,因为按钮还可能被点击。直到持有按钮的View被销毁,它才会被销毁。
#import <UIKit/UIKit.h>

typedef void(^ButtonClickBlcok)();

@interface TestView : UIView

@property (nonatomic, copy) ButtonClickBlcok buttonClickBlcok;

@end

33、在执行main函数之前做了什么?

1.动态库链接库
2.ImageLoader加载可执行文件, 里边是被编译过的符号,代码等
3.runtime与+load

34、内存中的区域划分

1.栈:栈区(stack)由系统自动分配和释放,存放方法(函数)的参数值,局部变量的值等。采用“先进后出”或者“后进先出”的原则。特点:有序、速度快、容量小

2.堆:一般由程序员分配和释放,如果不释放,则出现内存泄露。程序退出时,系统会回收你的内存。特点:无序、速度慢、容量大

3.静态存储区:全局变量(外部变量)和静态变量都存放在静态区域。这里我们需要注意,未初始化的全局变量(外部变量)和静态变量存放在一块,已初始化的存放在一起。当程序结束时,系统回收

4.常量区:存放常量的内存区域,程序结束时,系统回收

5.代码区:存放二进制代码的区域

35、关键字const有什么含意?

1).欲阻止一个变量被改变,可以使用 const 关键字。在定义该 const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
2).对指针来说,可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为const;

36、解释一下Runtime消息发送机制

使用消息机制前提,必须导入 #import <objc/message.h>

一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:
首先,通过obj的isa指针找到它的 class ;
在 class 的 method list 找 foo ;
如果 class 中没到 foo,继续往它的 superclass 中找 ;
一旦找到 foo 这个函数,就去执行它的实现IMP 。

若是每次调用方法, 都去查找method list, 就会很耗费性能和时间, 所以, object_cache是一个重要的角色:
当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list

37、解释一下Runtime消息转发机制

发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?在未找到方法时候, 进行三次挽救:

1) + (BOOL)resolveInstanceMethod:或者 +(BOOL)resolveClassMethod:, 若返回NO, 则进入下一步
2) - (id)forwardingTargetForSelector:(SEL)aSelector , 返回nil, 则进入下一步
3) - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector, 
    若返回为nil: 则: doesNotRecognizeSelector 发生崩溃
    若返回不为nil, 则调用 - (void)forwardInvocation:(NSInvocation *)anInvocation,进行挽救

38、解释对Runloop的理解

一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其实内部就是do-while循环,这个循环内部不断地处理各种任务(比 如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要;

iOS的应用程序里面,程序启动后会有一个如下的main()函数, 生成自动创建一个主线程, 并伴随着一个主run loop的产生;

// 获取主线程的RunLoop对象
[NSRunLoop mainRunLoop];

// 获取当前的线程的RunLoop对象,注意RunLoop是懒加载,currentRunLoop时会自动创建对象, 子线程中, run loop默认是没有启动的,可以使用[runloop run]进行开启;
[NSRunLoop currentRunLoop];

runloop变化的可监测节点: 
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};


Runloop模式: 系统默认注册了5个Mode:
1.NSDefaultRunLoopMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行(默认情况下运行)
2.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响(操作 UI 界面的情况下运行)
3.UIInitializationRunLoopMode:在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用
4.GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到(绘图服务)
5.NSRunLoopCommonModes:这是一个占位用的 Mode,不是一种真正的 Mode (RunLoop无法启动该模式,设置这种模式下,默认和操作 UI 界面时线程都可以运行,但无法改变 RunLoop 同时只能在一种模式下运行的本质)

- (void)viewDidLoad {
    [super viewDidLoad];

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 在默认模式下添加的 timer 当我们拖拽 textView 的时候,不会运行 run 方法
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

    // 在 UI 跟踪模式下添加 timer 当我们拖拽 textView 的时候,run 方法才会运行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    // timer 可以运行在两种模式下,相当于上面两句代码写在一起
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
} 

39、对property属性的关键字的理解

assign:用于非指针变量,一般用于基础类型和C数据类型,这些类型不是对象,统一由系统栈进行内存管理。

weak:对对象的弱引用,不增加对象的引用计数,也不持有对象,当对象消失后指针自动指向nil,所以这里也就防止了野指针的存在

strong:对对象的强引用,会增加对象的引用计数.

copy:建立一个引用计数为1的新对象,赋值时对传入值进行一份拷贝,所以使用copy关键字的时候,你将一个对象复制给该属性,该属性并不会持有那个对象,而是会创建一个新对象,并将那个对象的值拷贝给它。而使用copy关键字的对象必须要实现NSCopying协议。 

40、开辟子线程的方式都有哪几种

1. 通过 NSThread 开辟子线程
// 创建线程对象.
NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(handleFun:) object:nil];
thread1.name = @"sum";

// 手动开启
[thread1 start];

// 创建线程对象自动开启
[NSThread detachNewThreadSelector:@selector(handleFun:) toTarget:self withObject:nil];

//取消线程
[thread1 cancel];

2. 通过 NSOperationQueue 开辟子线程
使用 NSOperation 和 NSOperationQueue 进行多线程开发类似于C#中的线程池,只要将一个NSOperation(实际开中需要使用其子类NSInvocationOperation、NSBlockOperation)放到NSOperationQueue这个队列中线程就会依次启动。NSOperationQueue负责管理、执行所有的NSOperation,在这个过程中可以更加容易的管理线程总数和控制线程之间的依赖关系。

NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是是后者使用Block形式进行代码组织,使用相对方便。

注意:操作本身只是封装了要执行的相关方法,并没有开辟线程,没有主线程之分,在哪个线程中都能执行。

// 1. 创建5个操作
    NSInvocationOperation *invo1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(printString:) object:@"11"];
    NSInvocationOperation *invo2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(printString:) object:@"22"];
    NSInvocationOperation *invo3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(printString:) object:@"33"];
    NSInvocationOperation *invo4 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(printString:) object:@"44"];
    NSInvocationOperation *invo5 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(printString:) object:@"55"];
    
    // 2. NSBlockOperation 创建2个block操作
    NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"666666");
    }];
    NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"777777");
    }];
    
    
    // 3. 创建 操作队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 4. 设置最大并发数
    queue.maxConcurrentOperationCount = 1; // 设置为 1,顺序执行
    
    // 最大并发数控制的是同一时间点,能够执行的任务数。如果为1,则同时执行1个,根据队列FIFO特点,一定是顺序执行。 如果不为1,则可以同时执行多个任务,同时执行任务,称为--并发执行。
    
    // 5. 添加操作--将操作添加到队列
    [queue addOperation:invo1];
    [queue addOperation:invo2];
    [queue addOperation:invo3];
    [queue addOperation:invo4];
    [queue addOperation:invo5];
    [queue addOperation:block1];
    [queue addOperation:block2];
 
    提示: 添加到队列中的任务会自动执行,队列内部会开辟子线程,任务放在子线程中执行。
    
    ****************************************
    
    //方法2:直接使用操队列添加操作,eg:block2
    [queue addOperationWithBlock:^{
        NSLog(@"777777");
    }];
}

3. 通过 GCD 开辟子线程
在 GCD 中,加入了两个非常重要的概念: 任务 和 队列。
任务有两种执行方式: 同步执行 和 异步执行,他们之间的区别是 是否会创建新的线程。

// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);

//并行队列
    dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    
    //异步
    __weak typeof(self)weakSelf = self;
    dispatch_async(queue, ^{
        [weakSelf handleFun:nil];
    });
    
    dispatch_async(queue, ^{
        [weakSelf handleFun:nil];
    });
    
同步任务+串行队列: 不会开启新线程,在当前线程执行任务。任务串行的,执行完一个,再执行下一个。
异步任务+串行队列: 会开启一个新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务。

同步任务+并行队列: 不会开启新线程,在当前线程执行任务。任务串行的,执行完一个,再执行下一个。
异步任务+并行队列: 可以开启多个线程,任务交替(同时)执行。

同步执行+主队列: 特点(主线程调用):互等卡主不执行。
异步执行+主队列: 只在主线程中执行任务,执行完一个任务,再执行下一个任务

41、GCD 栅栏方法dispatch_barrier

dispatch_barrier_async
第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务
/**
 * 栅栏方法 dispatch_barrier_async
 */
- (void)barrier {
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_barrier_async(queue, ^{
        // 追加任务 barrier
        for (int i = 0; i < 2; ++i) {
          [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
          NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    
    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];         // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务4
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];         // 模拟耗时操作
            NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
        }
    });
}

42、GCD 队列组:dispatch_group 和 dispatch_group_notify

分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组

- (void)groupEnterAndLeave
{
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];           // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);   // 打印当前线程
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];           // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);   // 打印当前线程
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程.
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];          // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);  // 打印当前线程
        }
        NSLog(@"group---end");
    });

43、GCD 信号量:dispatch_semaphore

dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
dispatch_semaphore_signal:发送一个信号,让信号总量加1
dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[self methodWithABlock:^(id result){
    //写block中做的事情
    dispatch_semaphore_signal(sem);
}];
[self methodWithABlock:^(id result){
    //写block中做的事情
    dispatch_semaphore_signal(sem);
}];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

44、http和https的区别是什么?

HTTP:超文本传输协议 (HTTP-Hypertext transfer protocol) 是一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。

HTTPS:使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版(HTTP + SSL);

***************************************************************

HTTPS和HTTP的区别:
1. https协议需要到ca申请证书,一般免费证书很少,需要交费。
2. http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议
3. http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。
4. http的连接很简单,是无状态的; HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全

********************
Http握手过程:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

HTTPS握手过程:
1. 客户端发起HTTPS请求

2. 服务端的配置

采用HTTPS协议的服务器必须要有一套数字证书,可以是自己制作或者CA证书。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用CA证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。公钥给别人加密使用,私钥给自己解密使用。

3. 传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等。

4. 客户端解析证书

这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值,然后用证书对该随机值进行加密。

5. 传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6. 服务段解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7. 传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。

8. 客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。

PS: 整个握手过程第三方即使监听到了数据,也束手无策。

44、RSA 和 AES 加密

RSA
非对称加密,公钥加密,私钥解密,反之亦然。由于需要大数的乘幂求模等算法,运行速度慢,不易于硬件实现。
通常私钥长度有512bit,1024bit,2048bit,4096bit,长度越长,越安全,但是生成密钥越慢,加解密也越耗时。
既然是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密;
同理,既然是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证。

AES
对称加密,密钥最长只有256个bit,执行速度快,易于硬件实现。由于是对称加密,密钥需要在传输前通讯双方获知。
基于以上特点,通常使用RSA来首先传输AES的密钥给对方,然后再使用AES来进行加密通讯。

45、讲一下MVC和MVVM,MVP?

MVC:
• Models(模型) —  数据层,或者负责处理数据的 数据接口层。比如 Person 和 PersonDataProvider 类 
• Views(视图) - 展示层(GUI)。对于 iOS 来说所有以 UI 开头的类基本都属于这层。 
• Controller(控制器) - 它是 Model 和 View 之间的胶水或者说是中间人。一般来说,当用户对 View 有操作时它负责去修改相应 Model;当 Model 的值发生变化时它负责去更新对应 View。

--------------------- 

MVVM:
在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件 
view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel) 
viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方

--------------------- 

MVP:
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。

46、SDWebImage内部实现过程

入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。

connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,076评论 1 32
  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述?设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型...
    龍飝阅读 2,126评论 0 12
  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类...
    司马DE晴空阅读 1,273评论 0 7
  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    卑微的戏子阅读 618评论 0 1
  • 如果我工作还不能如鱼得水 独立handle 说明我还有提升的空间 虽然有时候我觉得冰冰有些固执 有时候我需要用简洁...
    角落蜷缩阅读 107评论 0 0