综合面试题(答案)

1. 单例写法

单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

  • 一般情况下, 如果一个类是单例, 那么都会提供一个类方法用于快速创建单例对象;
  • 而且这个类方法的名称是有一定的规则: share + 类名称 / default + 类名称 / 类名称开头。
//GCD 方式创建
static id _instance;   

+ (instancetype)sharedInstance  
{   
    static dispatch_once_t onceToken;   
    dispatch_once(&onceToken, ^{   
        _instance = [[self alloc] init];   
    });   
    return _instance;   
}   

+ (instancetype)allocWithZone:(struct _NSZone *)zone  
{   
    static dispatch_once_t onceToken;   
    dispatch_once(&onceToken, ^{   
        _instance = [super allocWithZone:zone];   
    });   
    return _instance;   
}   

- (id)copyWithZone:(NSZone *)zone   
{   
    return _instance;   
}  

- (id)mutableCopyWithZone:(NSZone *)zone {   
    return _instance;   
}
2. 深拷贝浅拷贝 举出使用实例

iOS提供了copy和mutablecopy方法,顾名思义,copy就是复制了一个imutable的对象,而mutablecopy就是复制了一个mutable的对象。

  • 系统的非容器类对象(这里指的是NSString,NSNumber等一类的对象)
    • 如果对一不可变对象复制,copy是浅拷贝,mutableCopy就是深拷贝。
    • 如果是对可变对象复制,都是深拷贝,但是copy返回的对象是不可变的。
  • 系统的容器类对象
    • 对于容器类本身,上面讨论的结论也是适用的,需要探讨的是复制后容器内对象的变化
    • 容器内的元素内容都是指针复制
3.@property 相关问题总汇:

实例变量+基本数据类型变量=成员变量
属性 (property)有两大概念:ivar(实例变量)+存取方法(getter + setter)

3.1 ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些
  • 对应基本数据类型默认关键字是:
    atomic,readwrite,assign
  • 对于普通的OC对象:
    atomic,readwrite,strong
3.2 什么情况使用 weak 关键字,相比 assign 有什么不同
  • 什么情况使用 weak 关键字?
    • 在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如:delegate代理属性
    • 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak, 自定义View的子控件属性一般也使用weak;但是也可以使用strong
  • 不同点:
    • weak当对象销毁的时候,指针会被自动设置为nil,而assign不会
    • assigin 可以用非OC对象,而weak必须用于OC对象
3.3 NSString 属性什么时候用copy,什么时候用strong(ARC环境)

我们定义一个类,并为其声明两个字符串属性,如下所示:

@interface TestStringClass ()
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, copy) NSString *copyedString;
@end

首先,我们用一个不可变字符串来为这两个属性赋值,

- (void)test {
    NSString *string = [NSString stringWithFormat:@"abc"];
    self.strongString = string;
    self.copyedString = string;
    NSLog(@"origin string: %p, %p", string, &string);
    NSLog(@"strong string: %p, %p", _strongString, &_strongString);
    NSLog(@"copy string: %p, %p", _copyedString, &_copyedString);
}

其输出结果是:

origin string: 0x7fe441592e20, 0x7fff57519a48
strong string: 0x7fe441592e20, 0x7fe44159e1f8
copy string: 0x7fe441592e20, 0x7fe44159e200

我们可以看到,这种情况下,不管是strong还是copy属性的对象,其指向的地址都是同一个,即为string指向的地址。如果我们换作MRC环境,打印string的引用计数的话,会看到其引用计数值是3,即strong操作和copy操作都使原字符串对象的引用计数值加了1。
接下来,我们把string由不可变改为可变对象,看看会是什么结果。即将下面这一句

NSString *string = [NSString stringWithFormat:@"abc"];

改成:

NSMutableString *string = [NSMutableString stringWithFormat:@"abc"];

其输出结果是:

origin string: 0x7ff5f2e33c90, 0x7fff59937a48
strong string: 0x7ff5f2e33c90, 0x7ff5f2e2aec8
copy string: 0x7ff5f2e2aee0, 0x7ff5f2e2aed0

可以发现,此时copy属性字符串已不再指向string字符串对象,而是深拷贝了string字符串,并让_copyedString对象指向这个字符串。在MRC环境下,打印两者的引用计数,可以看到string对象的引用计数是2,而_copyedString对象的引用计数是1。

此时,我们如果去修改string字符串的话,可以看到:因为_strongString与string是指向同一对象,所以_strongString的值也会跟随着改变(需要注意的是,此时_strongString的类型实际上是NSMutableString,而不是NSString);而_copyedString是指向另一个对象的,所以并不会改变。
结论 由于NSMutableString是NSString的子类,所以一个NSString指针可以指向NSMutableString对象——让我们的strongString指针指向一个可变字符串是可以的。
当源字符串是NSString时:由于字符串是不可变的,所以,不管是strong还是copy属性的对象,都是指向源对象,copy操作只是做了次浅拷贝。
当源字符串是NSMutableString时:strong属性只是增加了源字符串的引用计数,而copy属性则是对源字符串做了次深拷贝,产生一个新的对象,且copy属性对象指向这个新的对象。另外需要注意的是,这个copy属性对象的类型始终是NSString,而不是NSMutableString,因此其是不可变的。
这里还有一个性能问题,即在源字符串是NSMutableString,strong是单纯的增加对象的引用计数,而copy操作是执行了一次深拷贝,所以性能上会有所差异。而如果源字符串是NSString时,则没有这个问题。
所以,在声明NSString属性时,到底是选择strong还是copy,可以根据实际情况来定。不过,一般我们将对象声明为NSString时,都不希望它改变,所以大多数情况下,建议使用copy,以免因可变字符串的修改导致的一些非预期问题。

3.4 用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

因为父类指针可以指向子类对象NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作。使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可变对象,我本身持有的就是一个不可变的副本。
如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。

copy此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。
当属性类型为NSString时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例,这个类是NSString的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” 的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” ,就应该在设置新属性值时拷贝一份。

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

两个问题:
1、添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃.因为copy就是复制一个不可变NSArray的对象;
2、使用了atomic属性会严重影响性能。

4. #include #import @class

#includeC/C++导入头文件的关键字
#importObjective-c导入头文件的关键字,,使用#import头文件会自动只导入一次,不会重复导入,相当于#include#pragma once#import<>用来包含系统的头文件,#import””用来包含用户头文件。
@class:告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含

5.循环引用相关问题汇总:

循环引用可以简单理解为A引用了B,而B又引用了A,双方都同时保持对方的一个引用,导致任何时候引用计数都不为0,始终无法释放。若当前对象是一个ViewController,则在dismiss或者pop之后其dealloc无法被调用,在频繁的push或者present之后内存暴增,然后APP就挂了

  • block的使用有时会导致循环引用
  • 定时器的使用有时会导致循环引用
5.1 使用block时什么情况会发生引用循环,如何解决

一个对象中强引用了block,在block中又使用了该对象,就会发射循环引用。 解决方法是将该对象使用 _weak或者_block修饰符修饰之后再在block中使用。
id weak weakSelf = self; 或者 __weak typeof(self) weakSelf = self;该方法可以设置宏
id __block weakSelf = self;

5.2 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?

系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑:

所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,比如这些:
1.
[UIView animateWithDuration:duration animations:^{ 
    [self.superview layoutIfNeeded]; 
}]; 
2.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
    self.someProperty = xyz; 
}]; 
3.
[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" 
                                                  object:nil 
                                                   queue:[NSOperationQueue mainQueue]                                                   
                                              usingBlock:^(NSNotification * notification) {
                                                    self.someProperty = xyz; 
                                               }];
这些情况不需要考虑“引用循环”。

但如果你使用一些参数中可能含有实例变量的系统 api 如 GCD 、NSNotificationCenter就要小心一点:

比如GCD 内部如果引用了 self,而且 GCD 的其他参数是实例变量,则要考虑到循环引用:
1.
__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^{
    __typeof__(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doSomethingElse];
} );

类似的:
2.
__weak __typeof__(self) weakSelf = self;
  _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
                                                    object:nil
                                                    queue:nil
                                                    usingBlock:^(NSNotification *note) {
                                    __typeof__(self) strongSelf = weakSelf;
                                   [strongSelf dismissModalViewControllerAnimated:YES];
  }];
self --> _observer --> block --> self 显然这也是一个循环引用。

这篇文章介绍了weakself和strongself的用法:到底什么时候才需要在ObjC的Block中使用weakSelf/strongSelf

5.3 NSTimer导致循环引用的例子

一方面,NSTimer经常会被作为某个类的成员变量,而NSTimer初始化时要指定self为target,容易造成循环引用。
另一方面,若timer一直处于validate的状态,则其引用计数将始终大于0。比如当定时器销毁的时机不对,在dealloc里面销毁的时候,内存就不会释放,就会造成循环引用

.h文件
#import <Foundation/Foundation.h>
@interface Friend : NSObject
{
    NSTimer *_timer;
}
- (void)cleanTimer;
@end

.m文件
@implementation Friend
- (id)init
{
    if (self = [super init]) {
     _timer = [NSTimer scheduledTimerWithTimeInterval:1 
                       target:self 
                       selector:@selector(handleTimer:)
                       userInfo:nil 
                       repeats:YES];
     }
    return  self;
 } 
- (void)handleTimer:(id)sender
{
   NSLog(@"%@ say: Hi!", [self class]);
 }
- (void)cleanTimer
{
   [_timer invalidate];
    _timer = nil;
}
 - (void)dealloc
 {
   [self cleanTimer];
  NSLog(@"[Friend class] is dealloced");
 }

在main.m中声明并且调用,通过函数让Friend类延时5秒后引用计数减一

#import "Friend.h"
//循环引用
//是一个很麻烦的一件事,完全靠经验
int main(int argc, const char * argv[]) {   
     Friend *friend = [[Friend alloc] init];
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 95*NSEC_PER_SEC), 
                    dispatch_get_main_queue(), ^{
                    [friend release];
    });
    return 0;
 }
我们所期待的结果是,初始化5秒后,friend对象被release,friend的dealloc方法被调用,在dealloc里面timer失效,对象被析构。但结果却是从未停下..

这是为什么呢?主要是因为从timer的角度,timer认为调用方(Friend对象)被析构时会进入dealloc,在dealloc可以顺便将timer的计时停掉并且释放内存;但是从Friend的角度,他认为timer不停止计时不析构,那我永远没机会进入dealloc。循环引用,互相等待,无穷尽。问题的症结在于-(void)cleanTimer函数的调用时机不对,显然不能想当然地放在调用者的dealloc中。

一个比较好的解决方法是开放这个函数,让Friend的调用者显式地调用来清理现场。如下:

int main(int argc, const char * argv[]) {   
     Friend *friend = [[Friend alloc] init];
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 95*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
             [friend cleanTimer];
             [friend release];
    });
    return 0;
 }
6.UIView和CALayer的区别与联系
  • 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,UIView的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View内部有SubViews。但是Layer比View多了个AnchorPoint。
  • 在View显示的时候,UIView做为Layer的CALayerDelegate,View的显示内容由内部的CALayer的display。
  • CALayer修改属性是支持隐式动画的,在给UIView的Layer做动画的时候,View作为Layer的代理,Layer向View请求相应的action(动画行为)
  • layer 内部维护着三份 layer tree,分别是 :
    presentLayer Tree(动画树)
    modeLayer Tree(模型树)
    Render Tree (渲染树)。
    在做iOS动画的时候,我们修改动画的属性,在动画的其实是Layer的presentLayer属性值,而最终展示在界面上的其实是提供View的modelLayer
  • 两者最明显的区别是View可以接受并处理事件,而Layer不可以
7.详细描述一下响应者链的含义(最好能图释)

iOS 系统检测到手指触摸 (Touch) 操作时会将其打包成一个 UIEvent 对象,并放入当前活动Application的事件队列,单例UIApplication会从事件队列中取出触摸事件并传递给单例UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。
hitTest:withEvent:方法的处理流程如下:

  • 首先调用当前视图的pointInside:withEvent: 方法判断触摸点是否在当前视图内;
  • 若返回NO, 则hitTest:withEvent: 返回 nil,若返回YES, 则向当前视图的所有子视图 (subviews) 发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
  • 若第一次有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;
  • 如所有子视图都返回空,则 hitTest:withEvent: 方法返回自身 (self)。

如果最终 hit-test 没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果 UIWindow 实例和 UIApplication 实例都不能处理该事件,则该事件会被丢弃(这个过程即上面提到的响应值链);

8.如何高效的剪切圆角图片

CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

9.const/static分别用法,修饰类时又如何 #define
  • static
    • 函数体内 static 变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
    • 在模块内的 static "全局变量"可以被模块内所有函数访问,但不能被模块外其它函数访问;
    • 在模块内的 static "函数"只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
    • 在类中的 static "成员变量"属于整个类所拥有,对类的所有对象只有一份拷贝;
    • 在类中的 static "成员函数"属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的static 成员变量。

宏:

#define HSCoder @"汉斯哈哈哈"

变量:

NSString *HSCoder = @"汉斯哈哈哈";

常量:

四种写法:
static const NSString *HSCoder = @"汉斯哈哈哈";

const NSString *HSCoder = @"汉斯哈哈哈";   //"*HSCoder"不能被修改, "HSCoder"能被修改
NSString const *HSCoder = @"汉斯哈哈哈";   //"*HSCoder"不能被修改, "HSCoder"能被修改(与上个没啥区别)
NSString * const HSCoder = @"汉斯哈哈哈";  //"HSCoder"不能被修改,"*HSCoder"能被修改

结论:const右边的总不能被修改

所以一般我们定义一个常量又不想被修改应该选择最后一种方案:

NSString * const HSCoder = @"汉斯哈哈哈";

.h文件中这样写:

UIKIT_EXTERN NSString *const HSCoder
10.如何使用Objective-C实现多重继承?并给出代码示例
  • 通过组合实现“多继承”
  • 通过协议实现“多继承”
  • 通过category实现“单继承”(大部分网上文章将此方法误解成“多继承”)
11.关于app性能优化你是怎么理解的?请详细描述一下
  • 入门级(这是些你一定会经常用在你app开发中的建议,8个)
    • 用ARC管理内存
    • 不要block主线程
    • 打开gzip压缩
    • 在正确的地方使用reuseIdentifier
      自iOS6起,除了UICollectionView的cells和补充views,也应该在header和footer views中使用reuseIdentifiers。
    • 尽可能使Views不透明, 避免图层混合
      确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
      如无特殊需要,不要设置低于1的alpha值
      确保UIImage没有alpha通道
    • 避免庞大的XIB
      如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。
      需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.
    • 在Image Views中调整图片大小,避免临时转换
      确保图片大小和frame一致,不要在滑动时缩放图片
      确保图片颜色格式被GPU支持,避免劳烦CPU转换
    • 选择正确的Collection
      Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢, 插入/删除很慢。
      Dictionaries: 存储键值对。 用键来查找比较快。
      Sets: 无序的一组值。用值来查找很快,插入/删除很快。
  • 中级(这些是你可能在一些相对复杂情况下可能用到的)
    • 权衡渲染方法

    • 优化你的Table View

    • 重用和延迟加载Views
      这里我们用到的技巧就是模仿UITableView和UICollectionView的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。

    • Cache, Cache, 还是Cache!
      NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。

    • 处理内存警告
      如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.
      UIKit提供了几种收集低内存警告的方法:

      • 在app delegate中使用applicationDidReceiveMemoryWarning: 的方法
      • 在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning
      • 注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知
    • 避免反复处理数据,选择正确的数据格式
      如需要数据来展示一个table view,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
      如需要从特定key中取数据,那么就使用键值对的dictionary。

    • 正确地设定Background Images

    • 如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存

    • 如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存

    • 重用大开销的对象
      一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,想要避免使用这个对象的瓶颈你就需要重用他们,可以通过1.添加属性到你的class里 或者2.创建静态变量来实现。
      如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。
      下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:

// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;

// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
    if (! _formatter) {
        _formatter = [[NSDateFormatter alloc] init];
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"; // twitter date format
    }
    return _formatter;
}
  • 进阶级(这些建议只应该在你确信他们可以解决问题和得心应手的情况下采用)

    • 加速启动时间
    • 使用Autorelease Pool
    • 选择是否缓存图片
    • 尽量避免日期格式转换
  • UIKit方面

    • 慎用离屏渲染
      绝大多数时候离屏渲染会影响性能
      重写drawRect方法,设置圆角、阴影、模糊效果,光栅化都会导致离屏渲染
      设置阴影效果是加上阴影路径
      滑动时若需要圆角效果,开启光栅化
  • drawrect
    绘制图形性能的优化最好办法就是不去绘制。
    利用专有图层代替绘图需求。
    不得不用到绘图尽量缩小视图面积,并且尽量降低重绘频率。
    异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。

12. tableview优化

为了保证table view平滑滚动,确保你采取了以下的措施:

  • 正确使用reuseIdentifier来重用cells
  • 尽量使所有的view opaque,包括cell自身
  • 避免渐变,图片缩放
  • 缓存行高
  • 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
  • 使用shadowPath来画阴影
  • 减少subviews的数量
  • 尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
  • 使用正确的数据结构来存储数据
  • 使用rowHeight, sectionFooterHeight 和 sectionHeaderHeight来设定固定的高,不要请求delegate
13. 对象没销毁的原因有哪些
  • 控制器中NSTimer没有被销毁
    当控制器中存在NSTimer时,就需要注意,因为当
[NSTimer scheduledTimerWithTimeInterval:1.0 
           target:self 
           selector:@selector(updateTime:) 
           userInfo:nil 
           repeats:YES];

时,这个target:self就增加了VC的RetainCount,如果你不将这个timer invalidate,就别想调用dealloc。需要在viewWillDisappear之前需要把控制器用到的NSTimer销毁。

[timer invalidate]; // 销毁timer
timer = nil; // 置nil
  • 控制器中的代理不是weak属性
    例如@property (nonatomic, weak) id<HCAppViewDelegate> delegate;代理要使用弱引用,因为自定义控件是加载在视图控制器中的,视图控制器view对自定义控件是强引用,如果代理属性设置为strong,则意味着delegate对视图控制器也进行了强引用,会造成循环引用。导致控制器无法被释放,最终导致内存泄漏。
  • 控制器中block的循环引用
14.多线程操作
  • 14.1 多线程处理方式及优缺
  • pThread c语言框架
    一套通用的多线程API,适用于Linux\Windows\Unix,跨平台,可移植,使用C语言,生命周期需要程序员管理,IOS开发中使用很少。
  • NSThread apple 封装过,面向对象,可直接操控线程对象
    优点:NSThread 比其他两个轻量级。
    缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。
1.先创建线程类,再启动
NSThread *thread = [[NSThread alloc] initWithTarget:self 
                                     selector:@selector(run:) 
                                     object:nil];
[thread start];

2.创建并自动启动
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];

3.使用 NSObject 的方法创建并自动启动
[self performSelectorInBackground:@selector(run:) withObject:nil];
  • GCD为多核并行运算提出的解决方案,自动管理线程的生命周期(创建线程、调度任务、销毁线程)
    • 任务:即操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。任务有两种执行方式: 同步执行 和 异步执行,他们之间主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕!
      • 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
      • 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
    • 队列:用于存放任务。一共有两种队列, 串行队列 和 并行队列。
      • 串行队列:GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。
      • 并行队列:放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。
创建队列
1.主队列:这是一个特殊的 `串行队列`
dispatch_queue_t queue = dispatch_get_main_queue();
2.全局并行队列:只要是并行任务一般都加入到这个队列。这是系统提供的一个并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3.自己创建的队列:
 //串行队列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
  //并行队列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);

创建任务
1.同步任务: 会阻塞当前线程 (SYNC)
 dispatch_sync(<#queue#>, ^{
      //code here
      NSLog(@"%@", [NSThread currentThread]);
  });
2.异步任务:不会阻塞当前线程 (ASYNC)
dispatch_async(<#queue#>, ^{
      //code here
      NSLog(@"%@", [NSThread currentThread]);
  });

队列组
//1.创建队列组
dispatch_group_t group = dispatch_group_create();
//2.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//3.多次使用队列组的方法执行任务, 只有异步方法
//3.1.执行3次循环
dispatch_group_async(group, queue, ^{
    for (NSInteger i = 0; i < 3; i++) {
        NSLog(@"group-01 - %@", [NSThread currentThread]);
    }
});

//3.2.主队列执行8次循环
dispatch_group_async(group, dispatch_get_main_queue(), ^{
    for (NSInteger i = 0; i < 8; i++) {
        NSLog(@"group-02 - %@", [NSThread currentThread]);
    }
});

//3.3.执行5次循环
dispatch_group_async(group, queue, ^{
    for (NSInteger i = 0; i < 5; i++) {
        NSLog(@"group-03 - %@", [NSThread currentThread]);
    }
});

//4.都完成后会自动通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"完成 - %@", [NSThread currentThread]);
});
  • NSOperation GCD的封装 它的实例封装了需要的操作以及操作所需要的数据
    优点:不需要关心线程管理, 数据同步的事情,可以把精力放在自己需要执行的操作上。NSOperation是个抽象类,可以使用它定义好的两个子类: NSInvocationOperationNSBlockOperation,或者用它的子类(比较高级,暂不提),创建NSOperation子类的对象。
    NSOperationNSOperationQueue 分别对应 GCD 的 任务队列
添加任务
1.NSInvocationOperation : 需要传入一个方法名
  //1.创建NSInvocationOperation对象
  NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
  //2.开始执行
 [operation start];

2.NSBlockOperation
  //1.创建NSBlockOperation对象
  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"%@", [NSThread currentThread]);
  }];
  //2.开始任务
 [operation start];
NSBlockOperation还有一个方法:addExecutionBlock: 通过这个方法可以给Operation添加多个执行Block。
这样Operation中的任务会并发执行,它会在主线程和其它的多个线程执行这些任务


创建队列:
只要添加到队列,会自动调用任务的 start() 方法

1.主队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];

2.其他队列
//1.创建一个其他队列    
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//2.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];
//3.添加多个Block
for (NSInteger i = 0; i < 5; i++) {
    [operation addExecutionBlock:^{
        NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
    }];
}
//4.队列添加任务
[queue addOperation:operation];

将NSOperationQueue与GCD的队列相比较就会发现,这里没有串行队列,那如果我想要10个任务在其他线程串行的执行怎么办?
这就是苹果封装的妙处,你不用管串行、并行、同步、异步这些名词。NSOperationQueue有一个参数maxConcurrentOperationCount最大并发数,用来设置最多可以让多少个任务同时执行。当你把它设置为1的时候,他不就是串行了嘛!
NSOperation 有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了

  • 14.2什么时候使用GCD,什么时候使用NSOperation?如何按照指定的执行顺序完成任务?例如,c任务需要在a、b任务完成之后执行,请分别使用GCD和NSOperation写书代码示例
  • 14.3 有个图片下载未完成划过后重新出发没下载图片会怎样
  • 14.4 GCD的队列(dispatch_queue_t)分哪两种类型?

串行队列和并行队列

  • 14.5 如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

总体上说: 使用 dispatch group,然后 wait forever 等待完成, 或者采取 group notify 来通知回调。
细节:
1. 创建异步队列
2. 创建dispatch_group dispatch_group_t = dispatch_group_create()
3. 通过组来执行异步下载任务 dispatch_group_async(queueGroup, aQueue, ^{
NSLog(@"下载图片."); });
4.等到所有任务完成 dispatch_group_wait(queueGroup, DISPATCH_TIME_FOREVER);
5.合成图片

  使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  dispatch_group_t group = dispatch_group_create();
  dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
  dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
  dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); 
  dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 合并图片
  });

######15. ARC内存管理的理解
《Effective Objective-C 2.0》
理解引用计数
- 引用计数工作原理
- 属性存取方法中的内存管理
- 自动释放池
- 保留环

以arc简化引用计数
- 使用ARC时必须遵循的方法命名规则
- 变量的内存管理语义
- arc如何清理实例变量
- 覆写内存管理方法
- 用“僵尸对象”调试内存管理问题
- 不要使用retain count

在dealloc方法中只释放引用并解除监听
编写"异常安全代码"时注意内存管理问题
以弱引用避免保留环
以"自动释放池块"降低内存峰值

《Objective-C高级编程》
>Apple 在Objective-C中采用ARC机制,让编译器来进行内存管理。在新一代Apple LLVM编译器中设置ARC为有效状态,就无需再次键入retain或release代码,这在降低程序崩溃,内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此依赖,应用程序将具有可预测性,且能流程运行,速度也将大幅提升。

- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放


- ######15.1.内存泄漏有哪些情况
   - 循环引用
      - Delegate
我们在使用代理设计模式的时候,一定要注意将 delegate 变量声明为 weak 类型,像这样 @property (nonatomic, weak) id delegate;
      - Block
__weak typeof(self)weakSelf = self;
      - NSTimer
在一个ViewController里创建了一个定时器,并且repeats:值为YES,一定记得在 pop/dismiss当前ViewController将timer设为invalidate,否则会造成循环引用,这里要特别需要注意的一点是:我们不要在ViewController的dealloc方法里调用[timer invalidate]; 因为从来不会调到这里,我们应该在viewWillDisappear里边调用

   - performSelector延时调用导致的内存泄露
假设你延时10s触发某个方法,但是在3s的时候,用户点击了back,这时对象不能马上被回收,而是要等到10s后才有可能被回收。所以在项目中如果要延时触发方法,我不会选择该方法,而是使用GCD
         __weak typeof(self) weakSelf = self;
         double delayInSeconds = 10.0;
         dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds *NSEC_PER_SEC);
         dispatch_after(popTime, dispatch_get_main_queue(), ^{
             [weakSelf dodd];
         });

   - 代理未清空引起野指针
iOS的一些API,发现delegate都是assign的,这样就会引起野指针的问题,可能会引起一些莫名其妙的crash。那么这是怎么引起的,当一个对象被回收时,对应的delegate实体也就被回收,但是delegate的指针确没有被nil,从而就变成了游荡的野指针了。所以在delloc方法中要将对应的assign代理设置为nil
一般自己写的一些delegate,我们会用weak,而不是assign,weak的好处是当对应的对象被回收时,指针也会自动被设置为nil。

- ######15.2 什么时候用@autoreleasepool
   - 写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
   - 写循环,循环里面包含了大量临时创建的对象。(500000次循环,每次循环创建一个NSNumber实例和两个NSString实例)
   - 创建了新的线程。(非Cocoa程序创建线程时才需要)
   - 长时间在后台运行的任务。

- ######15.3. 页面使用过多内存过多闪退
- ######15.4. 1000张图片内存
- ######15.5.界面交互优化腾讯 banner100页怎么办

######16. 详细描述一下KVO的实现机制,代理 通知和kvo区别 代理和block区别 block 访问外部变量原理


######17. runtime的理解
- runtime作用
     - 发送消息
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现
     - 交换方法
开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
方式一:继承系统的类,重写方法.
方式二:使用runtime,交换方法.
           // 获取imageWithName方法地址
           Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
           // 获取imageWithName方法地址
           Method imageName = class_getClassMethod(self, @selector(imageNamed:));
           // 交换方法地址,相当于交换实现方式
           method_exchangeImplementations(imageWithName, imageName);
     - 动态添加方法
开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
简单使用
           @implementation Person
           // void(*)()
           // 默认方法都有两个隐式参数,
           void eat(id self,SEL sel)
           {
               NSLog(@"%@ %@",self,NSStringFromSelector(sel));
           }
           // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
           // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
           + (BOOL)resolveInstanceMethod:(SEL)sel
           {
               if (sel == @selector(eat)) {
                   // 动态添加eat方法
                   // 第一个参数:给哪个类添加方法
                   // 第二个参数:添加方法的方法编号
                   // 第三个参数:添加方法的函数实现(函数地址)
                   // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
                   class_addMethod(self, @selector(eat), eat, "v@:");
               }
               return [super resolveInstanceMethod:sel];
           }
           @end
     - 给分类添加属性
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
           static const char *key = "name";
           @implementation NSObject (Property)
           - (NSString *)name
           {
               // 根据关联的key,获取关联的值。
               return objc_getAssociatedObject(self, key);
           }
           - (void)setName:(NSString *)name
           {
               // 第一个参数:给哪个对象添加关联
               // 第二个参数:关联的key,通过这个key获取
                // 第三个参数:关联的value
               // 第四个参数:关联的策略
               objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
           }
           @end
     - 字典转模型
- runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
- category中能不能使用声明属性?为什么?如果能,怎么实现?
给分类(Category)添加属性利用Runtime实现getter/setter 方法

@interface ClassName (CategoryName)
@property (nonatomic, strong) NSString *str;
@end

//实现文件

import "ClassName + CategoryName.h"

import <objc/runtime.h>

static void *strKey = &strKey;
@implementation ClassName (CategoryName)

-(void)setStr:(NSString *)str
{
objc_setAssociatedObject(self, & strKey, str, OBJC_ASSOCIATION_COPY);
}

-(NSString *)str
{
return objc_getAssociatedObject(self, &strKey);
}

@end

- 什么时候会报unrecognized selector的异常?
简单来说:当该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决。
简单的流程如下:objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
`Method resolution`
objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程,如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发(Message Forwarding)。
`Fast forwarding`
如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
`Normal forwarding`
这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。


######18. runloop的理解
- RunLoop 的概念
如果我们需要一个机制,让线程能随时处理事件但并不退出,这种模型通常被称作 Event Loop。Event Loop 在很多系统和框架里都有实现,比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
- RunLoop 与线程的关系
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。




######19. contentoffset contentinset contentsize
######20. 说说知道哪些设计模式,说说简单工厂和抽象工厂
######21.第三方分享的调用接口
######22.动画的实现方式
######23. 错误提示sympol
######24. 使用定时器注意问题
######25. 页面滑动丢帧的原因有多少种
######26.推送的原理具体实现
######27. 如果不用第三方库实现图片缓存设计一种方法
######28. 获得App闪退log怎么做
######29. 完整发布流程

- 如何重写带 copy 关键字的 setter?
重写copy的setter方法时候,一定要调用一下传入的对象的copy方法,然后在赋值给该setter的方法对应的成员变量

- @synthesize和@dynamic分别有什么作用?
   - @property有两个对应的词,一个是@synthesize,一个是@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;
   - @synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
   - @dynamic告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。(当然对于readonly的属性只需提供getter即可)。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var =someVar,由于缺setter方法会导致程序崩溃;或者当运行到 someVar = var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
- BAD_ACCESS在什么情况下出现,如何调试?
   1.  死循环了
   2.  访问一个僵尸对象
设置全局断点快速定位问题代码所在行

- 谈谈instancetype和id的异同
1、相同点
都可以作为方法的返回类型
2、不同点
①instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象;②instancetype只能作为返回值,不能像id那样作为参数

- @protocol 和 category 中如何使用 @property
1)在protocol中使用property只会生成setter和getter方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性
2)category 使用 @property 也是只会生成setter和getter方法的声明,如果我们真的需要给category增加属性的实现,需要借助于运行时的两个函数:
①objc_setAssociatedObject
②objc_getAssociatedObject



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

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,036评论 29 470
  • 多线程、特别是NSOperation 和 GCD 的内部原理。运行时机制的原理和运用场景。SDWebImage的原...
    LZM轮回阅读 1,982评论 0 12
  • ———————————————回答好下面的足够了---------------------------------...
    恒爱DE问候阅读 1,673评论 0 4
  • iOS面试小贴士 ———————————————回答好下面的足够了------------------------...
    不言不爱阅读 1,876评论 0 7
  • 重拾《生活大爆炸》 谢尔顿在霍华德的婚礼上致辞,他说:人穷尽一生去追寻另一个人类的故事,我一直都无法理解,或许我...
    谭闻乐见阅读 476评论 0 0