面试题搜集

来源:互联网 发布:大学生网络家教平台 编辑:程序博客网 时间:2024/06/05 08:28

一般性问题

最近这两天你有学到什么知识/技能么?最近有做过比较酷或者比较有挑战的项目么?最近看过的书/文章有哪些?如何向一个只接触过互联网的孩子解释「电视」?为什么要学习编程,编程对你而言的乐趣在哪儿?如果一个函数10次中有7次正确,3次错误,问题可能出现在哪里?自身最大优点是什么,怎么证明?有没有在 GitHub 上发布过开源代码,参与过开源项目?你最近遇到过的一个技术挑战是什么?怎么解决的?开发常用的工具有哪些?熟悉 CocoaPods 么?能大概讲一下工作原理么?最常用的版本控制工具是什么,能大概讲讲原理么?今年你最想掌握的一门技术是什么?为什么?目前已经做到了哪个程度?你一般是怎么用 Instruments 的?你一般是如何调试 Bug 的?你在你的项目中用到了哪些设计模式?如何实现单例,单例会有什么弊端?iOS 是如何管理内存的?

知识性问题

什么是响应链,它是怎么工作的?如何访问并修改一个类的私有属性?iOS Extension 是什么?能列举几个常用的 Extension 么?如何把一个包含自定义对象的数组序列化到磁盘?Apple Pay 是什么?它的大概工作流程是怎样的?iOS 的沙盒目录结构是怎样的? App Bundle 里面都有什么?iOS 的签名机制大概是怎样的?iOS 7的多任务添加了哪两个新的 API? 各自的使用场景是什么?Objective-C 的 class 是如何实现的?Selector 是如何被转化为 C 语言的函数调用的?UIScrollView 大概是如何实现的,它是如何捕捉、响应手势的?Objective-C 如何对已有的方法,添加自己的功能代码以实现类似记录日志这样的功能?+load 和 +initialize 的区别是什么?如何让 Category 支持属性?NSOperation 相比于 GCD 有哪些优势?strong / weak / unsafe_unretained 的区别?如何为 Class 定义一个对外只读对内可读写的属性?Objective-C 中,meta-class 指的是什么?UIView 和 CALayer 之间的关系?+[UIView animateWithDuration:animations:completion:] 内部大概是如何实现的?什么时候会发生「隐式动画」?如何处理异步的网络请求?frame 和 bounds 的区别是什么?如何把一张大图缩小为1/4大小的缩略图?一个 App 会处于哪些状态?Push Notification 是如何工作的?什么是 Runloop?Toll-Free Bridging 是什么?什么情况下会使用?当系统出现内存警告时会发生什么?什么是 Protocol,Delegate 一般是怎么用的?autorelease 对象在什么情况下会被释放?UIWebView 有哪些性能问题?有没有可替代的方案。为什么 NotificationCenter 要 removeObserver? 如何实现自动 remove?当 TableView 的 Cell 改变时,如何让这些改变以动画的形式呈现?什么是 Method Swizzle,什么情况下会使用?

经验类问题

为什么 UIScrollView 的滚动会导致 NSTimer 失效?为什么当 Core Animation 完成时,layer 又会恢复到原先的状态?你会如何存储用户的一些敏感信息,如登录的 token。有用过一些开源组件吧,能简单说几个么,大概说说它们的使用场景实现。什么时候会发生 EXC BAD ACCESS 异常?什么时候会使用 Core Graphics,有什么注意事项么?NSNotification 和 KVO 的使用场景?使用 Block 时需要注意哪些问题?performSelector:withObject:afterDelay: 内部大概是怎么实现的,有什么注意事项么?如何播放 GIF 图片,有什么优化方案么?使用 NSUserDefaults 时,如何处理布尔的默认值?(比如返回 NO,不知道是真的 NO 还是没有设置过)有哪几种方式可以对图片进行缩放,使用 CoreGraphics 缩放时有什么注意事项?哪些途径可以让 ViewController 瘦下来?有哪些常见的 Crash 场景?

综合类问题

设计一个可以无限滚动并且支持自动滚动的 SlideShow。设计一个进度条。设计一套大文件(如上百M的视频)下载方案。如果让你来实现 dispatch_once,你会怎么做?设计一个类似 iOS 主屏可以下拉出现 Spotlight 的系统。(对 UIScrollView 的理解程度)

编程实现

简述「Snakes and Ladders」的实现思路(这道题比较容易阐述清楚,且难度适中)

第二篇

category 和 extension 的区别

分类有名字,类扩展没有分类名字,是一种特殊的分类分类只能扩展方法(属性仅仅是声明,并没真正实现),类扩展可以扩展属性、成员变量和方法

define 和 const常量有什么区别?

define在预处理阶段进行替换,const常量在编译阶段使用宏不做类型检查,仅仅进行替换,const常量有数据类型,会执行类型检查define不能调试,const常量可以调试define定义的常量在替换后运行过程中会不断地占用内存,而const定义的常量存储在数据段只有一份copy,效率更高define可以定义一些简单的函数,const不可以

block和weak修饰符的区别?

__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,也可以修饰基本数据类型__weak只能在ARC模式下使用,只能修饰对象(NSString),不能修饰基本数据类型block修饰的对象可以在block中被重新赋值,weak修饰的对象不可以

static关键字的作用

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

堆和栈的区别

从管理方式来讲    对于栈来讲,是由编译器自动管理,无需我们手工控制;    对于堆来说,释放工作由程序员控制,容易产生内存泄露(memory leak)从申请大小大小方面讲    栈空间比较小    堆控件比较大从数据存储方面来讲    栈空间中一般存储基本类型,对象的地址    堆空间一般存放对象本身,block的copy等

风格纠错题

修改后的代码

typedef NS_ENUM(NSInteger, CYLSex)
{
CYLSexMan,
CYLSexWoman
};

@interface CYLUser : NSObject

@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSUInteger age;
@property (nonatomic, assign, readwrite) CYLSex sex;

  • (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
  • (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
  • (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;

@end

详细解说,这里就不列举了

Objective-C使用什么机制管理对象内存?

MRC 手动引用计数ARC 自动引用计数,现在通常ARC通过 retainCount 的机制来决定对象是否需要释放。 每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了

ARC通过什么方式帮助开发者管理内存?

通过编译器在编译的时候,插入类似内存管理的代码

ARC是为了解决什么问题诞生的?

首先解释ARC: automatic reference counting自动引用计数了解MRC的缺点    在MRC时代当我们要释放一个堆内存时,首先要确定指向这个堆空间的指针都被release了    释放指针指向的堆空间,首先要确定哪些指针指向同一个堆,这些指针只能释放一次(MRC下即谁创建,谁释放,避免重复释放)    模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁去释放    多线程操作时,不确定哪个线程最后使用完毕综上所述,MRC有诸多缺点,很容易造成内存泄露和坏内存的问题,这时苹果为尽量解决这个问题,从而诞生了ARC

ARC下还会存在内存泄露吗?

循环引用会导致内存泄露Objective-C对象与CoreFoundation对象进行桥接的时候如果管理不当也会造成内存泄露CoreFoundation中的对象不受ARC管理,需要开发者手动释放

什么情况使用weak关键字,相比assign有什么不同?

首先明白什么情况使用weak关键字?    在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如:delegate代理属性,代理属性也可使用assign    自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak,自定义IBOutlet控件属性一般也使用weak;当然,也可以使用strong,但是建议使用weakweak 和 assign的不同点    weak策略在属性所指的对象遭到摧毁时,系统会将weak修饰的属性对象的指针指向nil,在OC给nil发消息是不会有什么问题的;如果使用assign策略在属性所指的对象遭到摧毁时,属性对象指针还指向原来的对象,由于对象已经被销毁,这时候就产生了野指针,如果这时候在给此对象发送消息,很容造成程序奔溃    assigin 可以用于修饰非OC对象,而weak必须用于OC对象

@property 的本质是什么?

@property其实就是在编译阶段由编译器自动帮我们生成ivar成员变量,getter方法,setter方法

ivar、getter、setter是如何生成并添加到这个类中的?

使用“自动合成”( autosynthesis)这个过程由编译器在编译阶段执行自动合成,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码除了生成getter、setter方法之外,编译器还要自动向类中添加成员变量(在属性名前面加下划线,以此作为实例变量的名字)为了搞清属性是怎么实现的,反编译相关的代码,他大致生成了五个东西  // 该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远  OBJC_IVAR_$类名$属性名称  // 方法对应的实现函数  setter与getter  // 成员变量列表  ivar_list  // 方法列表  method_list  // 属性列表  prop_list    每次增加一个属性,系统都会在ivar_list中添加一个成员变量的描述    在method_list中增加setter与getter方法的描述    在prop_list中增加一个属性的描述    计算该属性在对象中的偏移量    然后给出setter与getter方法对应的实现,在setter方法中从偏移量的位置开始赋值,在getter方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转

@protocol 和 category 中如何使用 @property

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

objc_setAssociatedObject
objc_getAssociatedObject

@property后面可以有哪些修饰符?

原子性---nonatomic特质    如果不写默认情况为atomic(系统会自动加上同步锁,影响性能)    在iOS开发中尽量指定为nonatomic,这样有助于提高程序的性能读/写权限---readwrite(读写)、readooly (只读)内存管理语义---assign、strong、 weak、unsafe_unretained、copy方法名---getter=、setter=

@property (nonatomic, getter=isOn) BOOL on;

// setter=这种不常用,也不推荐使用。故不在这里给出写法

不常用的:nonnull,null_resettable,nullable

使用atomic一定是线程安全的吗?

不是,atomic的本意是指属性的存取方法是线程安全的,并不保证整个对象是线程安全的。举例:声明一个NSMutableArray的原子属性stuff,此时self.stuff 和self.stuff = othersulf都是线程安全的。但是,使用[self.stuff objectAtIndex:index]就不是线程安全的,需要用互斥锁来保证线程安全性

@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 = instance.var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定

ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?

基本数据:atomic,readwrite,assign普通的OC对象:atomic,readwrite,strong

@synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量么?

先回答第二个问题:不会@synthesize合成成员变量的规则,有以下几点:    如果指定了成员变量的名称,会生成一个指定的名称的成员变量    如果这个成员已经存在了就不再生成了    如果指定@synthesize foo;就会生成一个名称为foo的成员变量,也就是说:会自动生成一个属性同名的成员变量    @interface XMGPerson : NSObject    @property (nonatomic, assign) int age;    @end    @implementation XMGPerson    // 不加这语句默认生成的成员变量名为_age    // 如果加上这一句就会生成一个跟属性名同名的成员变量    @synthesize age;    @end    如果是 @synthesize foo = _foo; 就不会生成成员变量了

在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?

首先的搞清楚什么情况下不会autosynthesis(自动合成)    同时重写了setter和getter时    重写了只读属性的getter时    使用了@dynamic时    在 @protocol 中定义的所有属性    在 category 中定义的所有属性    重载的属性,当你在子类中重载了父类中的属性,必须 使用@synthesize来手动合成ivar应用场景    当你同时重写了setter和getter时,系统就不会生成ivar)。这时候有两种选择        手动创建ivar        使用@synthesize foo = _foo;,关联@property与ivar    可以用来修改成员变量名,一般不建议这么做,建议使用系统自动生成的成员变量

怎么用 copy 关键字?

NSString、NSArray、NSDictionary等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,为确保对象中的属性值不会无意间变动,应该在设置新属性值时拷贝一份,保护其封装性block也经常使用copy关键字    block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.    在ARC中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但是建议写上copy,因为这样显示告知调用者“编译器会自动对 block 进行了 copy 操作”

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

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

复制详解

浅复制(shallow copy):在浅复制操作时,对于被复制对象的每一层都是指针复制。深复制(one-level-deep copy):在深复制操作时,对于被复制对象,至少有一层是深复制。完全复制(real-deep copy):在完全复制操作时,对于被复制对象的每一层都是对象复制。非集合类对象的copy与mutableCopy[不可变对象 copy] // 浅复制[不可变对象 mutableCopy] //深复制[可变对象 copy] //深复制[可变对象 mutableCopy] //深复制集合类对象的copy与mutableCopy[不可变对象 copy] // 浅复制[不可变对象 mutableCopy] //单层深复制[可变对象 copy] //单层深复制[可变对象 mutableCopy] //单层深复制这里需要注意的是集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制

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

因为copy策略拷贝出来的是一个不可变对象,然而却把它当成可变对象使用,很容易造成程序奔溃这里还有一个问题,该属性使用了同步锁,会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明nonatomic可以节省这些虽然很小但是不必要额外开销,在iOS开发中应该使用nonatomic替代atomic

如何让自定义类可以用 copy 修饰符?如何重写带 copy 关键字的 setter?

若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyiog与NSMutableCopying协议,不过一般没什么必要,实现NSCopying协议就够了

// 实现不可变版本拷贝
- (id)copyWithZone:(NSZone *)zone;

// 实现可变版本拷贝
- (id)mutableCopyWithZone:(NSZone *)zone;

// 重写带 copy 关键字的 setter
- (void)setName:(NSString *)name
{
_name = [name copy];
}

+(void)load; +(void)initialize;有什么用处?

+(void)load;    当类对象被引入项目时, runtime 会向每一个类对象发送 load 消息    load 方法会在每一个类甚至分类被引入时仅调用一次,调用的顺序:父类优先于子类, 子类优先于分类    由于 load 方法会在类被import 时调用一次,而这时往往是改变类的行为的最佳时机,在这里可以使用例如method swizlling 来修改原有的方法    load 方法不会被类自动继承+(void)initialize;    也是在第一次使用这个类的时候会调用这个方法,也就是说 initialize也是懒加载总结:    在Objective-C中,runtime会自动调用每个类的这两个方法    +load会在类初始加载时调用    +initialize会在第一次调用类的类方法或实例方法之前被调用    这两个方法是可选的,且只有在实现了它们时才会被调用    两者的共同点:两个方法都只会被调用一次

Foundation对象与Core Foundation对象有什么区别

Foundation框架是使用OC实现的,Core Foundation是使用C实现的Foundation对象 和 Core Foundation对象间的转换:俗称桥接    ARC环境桥接关键字:    // 可用于Foundation对象 和 Core Foundation对象间的转换    __bridge    // 用于Foundation对象 转成 Core Foundation对象    __bridge_retained    // Core Foundation对象 转成 Foundation对象    __bridge_transfer        Foundation对象 转成 Core Foundation对象            使用__bridge桥接                如果使用__bridge桥接,它仅仅是将strOC的地址给了strC, 并没有转移对象的所有权,也就是说, 如果使用__bridge桥接, 那么如果strOC释放了,strC也不能用了                注意:在ARC条件下,如果是使用__bridge桥接,那么strC可以不用主动释放, 因为ARC会自动管理strOC和strC            NSString *strOC1 = [NSString stringWithFormat:@"abcdefg"];            CFStringRef strC1 = (__bridge CFStringRef)strOC1;            NSLog(@"%@ %@", strOC1, strC1);            使用__bridge_retained桥接                如果使用__bridge_retained桥接,它会将对象的所有权转移给strC, 也就是说, 即便strOC被释放了, strC也可以使用                注意:在ARC条件下,如果是使用__bridge_retained桥接,那么strC必须自己手动释放,因为桥接的时候已经将对象的所有权转移给了strC,而C语言的东西不是不归ARC管理的            NSString *strOC2 = [NSString stringWithFormat:@"abcdefg"];            //    CFStringRef strC2 = (__bridge_retained CFStringRef)strOC2;            CFStringRef strC2 = CFBridgingRetain(strOC2);// 这一句, 就等同于上一句            CFRelease(strC2);        Core Foundation对象 转成 Foundation对象            使用__bridge桥接                如果使用__bridge桥接,它仅仅是将strC的地址给了strOC, 并没有转移对象的所有权                也就是说如果使用__bridge桥接,那么如果strC释放了,strOC也不能用了            CFStringRef strC3 = CFStringCreateWithCString(CFAllocatorGetDefault(), "12345678", kCFStringEncodingASCII);            NSString *strOC3 = (__bridge NSString *)strC3;            CFRelease(strC3);            使用__bridge_transfer桥接                如果使用__bridge_transfer桥接,它会将对象的所有权转移给strOC, 也就是说, 即便strC被释放了, strOC也可以使用                如果使用__bridge_transfer桥接, 他会自动释放strC, 也就是以后我们不用手动释放strC            CFStringRef strC4 = CFStringCreateWithCString(CFAllocatorGetDefault(), "12345678", kCFStringEncodingASCII);            //     NSString *strOC = (__bridge_transfer NSString *)strC;            NSString *strOC4 = CFBridgingRelease(strC4); // 这一句, 就等同于上一句    MRC环境:直接强转    -(void)bridgeInMRC    {      // 将Foundation对象转换为Core Foundation对象,直接强制类型转换即可      NSString *strOC1 = [NSString stringWithFormat:@"xxxxxx"];      CFStringRef strC1 = (CFStringRef)strOC1;      NSLog(@"%@ %@", strOC1, strC1);      [strOC1 release];      CFRelease(strC1);      // 将Core Foundation对象转换为Foundation对象,直接强制类型转换即可      CFStringRef strC2 = CFStringCreateWithCString(CFAllocatorGetDefault(), "12345678", kCFStringEncodingASCII);      NSString *strOC2 = (NSString *)strC2;      NSLog(@"%@ %@", strOC2, strC2);      [strOC2 release];      CFRelease(strC2);    }

addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?

/**
1. self.person:要监听的对象
2. 参数说明
1> 观察者,负责处理监听事件的对象
2> 要监听的属性
3> 观察的选项(观察新、旧值,也可以都观察)
4> 上下文,用于传递数据,可以利用上下文区分不同的监听
*/
[self.person addObserver:self forKeyPath:@”name” options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@”Person Name”];

/**
* 当监控的某个属性的值改变了就会调用
*
* @param keyPath 监听的属性名
* @param object 属性所属的对象
* @param change 属性的修改情况(属性原来的值、属性最新的值)
* @param context 传递的上下文数据,与监听的时候传递的一致,可以利用上下文区分不同的监听
*/
- (void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary )change context:(void *)context
{
NSLog(@”%@对象的%@属性改变了:%@”, object, keyPath, change);
}

KVO内部实现原理

KVO是基于runtime机制实现的当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制如果原类为Person,那么生成的派生类名为NSKVONotifying_Person每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

如何手动触发一个value的KVO

自动触发的场景:在注册KVO之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了想知道如何手动触发,必须知道自动触发 KVO 的原理,见上面的描述手动触发演示

@property (nonatomic, strong) NSDate *now;

  • (void)viewDidLoad
    {
    [super viewDidLoad];

    // “手动触发self.now的KVO”,必写。
    [self willChangeValueForKey:@”now”];

    // “手动触发self.now的KVO”,必写。
    [self didChangeValueForKey:@”now”];
    }

若一个类有实例变量NSString *_foo,调用setValue:forKey:时,是以foo还是_foo作为key?

都可以

KVC的keyPath中的集合运算符如何使用?

必须用在集合对象上或普通对象的集合属性上简单集合运算符有@avg, @count , @max , @min ,@sum格式 @"@sum.age" 或 @"集合属性.@max.age"???

KVC和KVO的keyPath一定是属性么?

可以是成员变量

如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?

如何自己动手实现 KVO

apple用什么方式实现对一个对象的KVO?

此题就是问KVO的实现原理

第三篇

风格纠错题优化部分硬伤部分什么情况使用 weak 关键字,相比 assign 有什么不同?怎么用 copy 关键字?这个写法会出什么问题: @property (copy) NSMutableArray *array;如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的@protocol 和 category 中如何使用 @propertyruntime 如何实现 weak 属性@property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?weak属性需要在dealloc中置nil么?@synthesize和@dynamic分别有什么作用?ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?对非集合类对象的copy操作集合类对象的copy与mutableCopy@synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量么?在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?objc中向一个nil对象发送消息将会发生什么?objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?什么时候会报unrecognized selector的异常?一个objc对象如何进行内存布局?(考虑有父类的情况)一个objc对象的isa的指针指向什么?有什么作用?下面的代码输出什么?

@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@”%@”, NSStringFromClass([self class]));
NSLog(@”%@”, NSStringFromClass([super class]));
}
return self;
}
@end

22--55题,请看下篇。
  1. 风格纠错题

enter image description here 修改完的代码:

修改方法有很多种,现给出一种做示例:

// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代码,这是第一种修改方法,后面会给出第二种修改方法

typedef NS_ENUM(NSInteger, CYLSex) {   CYLSexMan,   CYLSexWoman};@interface CYLUser : NSObject<NSCopying>@property (nonatomic, readonly, copy) NSString *name;@property (nonatomic, readonly, assign) NSUInteger age;@property (nonatomic, readonly, assign) CYLSex sex;- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;@end

下面对具体修改的地方,分两部分做下介绍:硬伤部分 和 优化部分 。因为硬伤部分没什么技术含量,为了节省大家时间,放在后面讲,大神请直接看优化部分。
优化部分

enum 建议使用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型,参见官方的 Adopting Modern Objective-C 一文:

//定义一个枚举

 typedef NS_ENUM(NSInteger, CYLSex) {       CYLSexMan,       CYLSexWoman   };

(仅仅让性别包含男和女可能并不严谨,最严谨的做法可以参考 这里 。)

age 属性的类型:应避免使用基本类型,建议使用 Foundation 数据类型,对应关系如下:
 int -> NSInteger   unsigned -> NSUInteger   float -> CGFloat   动画时间 -> NSTimeInterval

同时考虑到 age 的特点,应使用 NSUInteger ,而非 int 。 这样做的是基于64-bit 适配考虑,详情可参考出题者的博文《64-bit Tips》。

如果工程项目非常庞大,需要拆分成不同的模块,可以在类、typedef宏命名的时候使用前缀。doLogIn方法不应写在该类中:虽然LogIn的命名不太清晰,但笔者猜测是login的意思, (勘误:Login是名词,LogIn 是动词,都表示登陆的意思。见: Log in vs. login )登录操作属于业务逻辑,观察类名 UserModel ,以及属性的命名方式,该类应该是一个 Model 而不是一个“ MVVM 模式下的 ViewModel ”:无论是 MVC 模式还是 MVVM 模式,业务逻辑都不应当写在 Model 里:MVC 应在 C,MVVM 应在 VM。

(如果抛开命名规范,假设该类真的是 MVVM 模式里的 ViewModel ,那么 UserModel 这个类可能对应的是用户注册页面,如果有特殊的业务需求,比如: -logIn 对应的应当是注册并登录的一个 Button ,出现 -logIn 方法也可能是合理的。)

doLogIn 方法命名不规范:添加了多余的动词前缀。 请牢记:如果方法表示让对象执行一个动作,使用动词打头来命名,注意不要使用 do,does 这种多余的关键字,动词本身的暗示就足够了。

应为 -logIn (注意: Login 是名词, LogIn 是动词,都表示登陆。 见 Log in vs. login )

-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中不要用 with 来连接两个参数: withAge: 应当换为age:,age: 已经足以清晰说明参数的作用,也不建议用 andAge: :通常情况下,即使有类似 withA:withB: 的命名需求,也通常是使用withA:andB: 这种命名,用来表示方法执行了两个相对独立的操作(从设计上来说,这时候也可以拆分成两个独立的方法),它不应该用作阐明有多个参数,比如下面的:

//错误,不要使用”and”来连接参数
- (int)runModalForDirectory:(NSString )path andFile:(NSString )name andTypes:(NSArray *)fileTypes;
//错误,不要使用”and”来阐明有多个参数
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正确,使用”and”来表示两个相对独立的操作
- (BOOL)openFile:(NSString )fullPath withApplication:(NSString )appName andDeactivate:(BOOL)flag;

由于字符串值可能会改变,所以要把相关属性的“内存管理语义”声明为 copy 。(原因在下文有详细论述:***用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?***)“性别”(sex)属性的:该类中只给出了一种“初始化方法” (initializer)用于设置“姓名”(Name)和“年龄”(Age)的初始值,那如何对“性别”(Sex)初始化?

Objective-C 有 designated 和 secondary 初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化方法的初始化方法。举例说明:

// .m文件   // http://weibo.com/luohanchenyilong/   // https://github.com/ChenYilong   //   @implementation CYLUser   - (instancetype)initWithName:(NSString *)name                            age:(NSUInteger)age                            sex:(CYLSex)sex {       if(self = [super init]) {           _name = [name copy];           _age = age;           _sex = sex;       }       return self;   }   - (instancetype)initWithName:(NSString *)name                            age:(NSUInteger)age {       return [self initWithName:name age:age sex:nil];   }   @end

上面的代码中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。因为仅仅是调用类实现的 designated 初始化方法。

因为出题者没有给出 .m 文件,所以有两种猜测:1:本来打算只设计一个 designated 初始化方法,但漏掉了“性别”(sex)属性。那么最终的修改代码就是上文给出的第一种修改方法。2:不打算初始时初始化“性别”(sex)属性,打算后期再修改,如果是这种情况,那么应该把“性别”(sex)属性设为 readwrite 属性,最终给出的修改代码应该是:

// .h文件   // http://weibo.com/luohanchenyilong/   // https://github.com/ChenYilong   // 第二种修改方法(基于第一种修改方法的基础上)   typedef NS_ENUM(NSInteger, CYLSex) {       CYLSexMan,       CYLSexWoman   };   @interface CYLUser : NSObject<NSCopying>   @property (nonatomic, readonly, copy) NSString *name;   @property (nonatomic, readonly, assign) NSUInteger age;   @property (nonatomic, readwrite, assign) CYLSex sex;   - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;   - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;   + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;   @end

.h 中暴露 designated 初始化方法,是为了方便子类化 (想了解更多,请戳–》 《禅与 Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译)》。)

按照接口设计的惯例,如果设计了“初始化方法” (initializer),也应当搭配一个快捷构造方法。而快捷构造方法的返回值,建议为 instancetype,为保持一致性,init 方法和快捷构造方法的返回类型最好都用 instancetype。如果基于第一种修改方法:既然该类中已经有一个“初始化方法” (initializer),用于设置“姓名”(Name)、“年龄”(Age)和“性别”(Sex)的初始值: 那么在设计对应 @property 时就应该尽量使用不可变的对象:其三个属性都应该设为“只读”。用初始化方法设置好属性值之后,就不能再改变了。在本例中,仍需声明属性的“内存管理语义”。于是可以把属性的定义改成这样   @property (nonatomic, readonly, copy) NSString *name;   @property (nonatomic, readonly, assign) NSUInteger age;   @property (nonatomic, readonly, assign) CYLSex sex;

由于是只读属性,所以编译器不会为其创建对应的“设置方法”,即便如此,我们还是要写上这些属性的语义,以此表明初始化方法在设置这些属性值时所用的方式。要是不写明语义的话,该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法之前自行拷贝属性值。这种操作多余而且低效。

initUserModelWithUserName 如果改为 initWithName 会更加简洁,而且足够清晰。UserModel 如果改为 User 会更加简洁,而且足够清晰。UserSex如果改为Sex 会更加简洁,而且足够清晰。第二个 @property 中 assign 和 nonatomic 调换位置。 推荐按照下面的格式来定义属性

@property (nonatomic, readwrite, copy) NSString *name;

属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。 这样做你的属性更容易修改正确,并且更好阅读。这在《禅与Objective-C编程艺术 >》里有介绍。而且习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作。
硬伤部分

在-和(void)之间应该有一个空格enum 中驼峰命名法和下划线命名法混用错误:枚举类型的命名规则和函数的命名规则相同:命名时使用驼峰命名法,勿使用下划线命名法。enum 左括号前加一个空格,或者将左括号换到下一行enum 右括号后加一个空格UserModel :NSObject 应为UserModel : NSObject,也就是:右侧少了一个空格。@interface 与 @property 属性声明中间应当间隔一行。两个方法定义之间不需要换行,有时为了区分方法的功能也可间隔一行,但示例代码中间隔了两行。-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名与参数之间多了空格。而且 - 与 (id) 之间少了空格。-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名与参数之间多了空格:(NSString*)name 前多了空格。-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中 (NSString*)name,应为 (NSString *)name,少了空格。doLogIn方法中的 `LogIn` 命名不清晰:笔者猜测是login的意思,应该是粗心手误造成的。

(勘误: Login 是名词, LogIn 是动词,都表示登陆的意思。见: Log in vs. login )

  1. 什么情况使用 weak 关键字,相比 assign 有什么不同?

什么情况使用 weak 关键字?

在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。在下文也有论述:***《IBOutlet连出来的视图属性为什么可以被设置成weak?》***

不同点:

weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。assign 可以用非 OC 对象,而 weak 必须用于 OC 对象
  1. 怎么用 copy 关键字?

用途:

NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;block 也经常使用 copy 关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks:

block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写依然写。如果你这样想,其实是你“日用而不知”,你平时开发中是经常在用我说的这种做法的,比如下面的属性不写copy也行,但是你会选择写还是不写呢?

@property (nonatomic, copy) NSString *userId;- (instancetype)initWithUserId:(NSString *)userId {   self = [super init];   if (!self) {       return nil;   }   _userId = [userId copy];   return self;}

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

用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

该问题在下文中也有论述:用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;

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

第1条的相关原因在下文中有论述《用@property声明的NSString(或NSArray,NSDictionary)经常使用 copy 关键字,为什么?如果改用strong关键字,可能造成什么问题?》 以及上文《怎么用 copy 关键字?》也有论述。

比如下面的代码就会发生崩溃

// .h文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 下面的代码就会发生崩溃@property (nonatomic, copy) NSMutableArray *mutableArray;// .m文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 下面的代码就会发生崩溃NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];self.mutableArray = array;[self.mutableArray removeObjectAtIndex:0];接下来就会奔溃: -[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460

第2条原因,如下:

该属性使用了同步锁,会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明 nonatomic 可以节省这些虽然很小但是不必要额外开销。

在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用同步锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的”(atomic))。

在iOS开发中,你会发现,几乎所有属性都声明为 nonatomic。

一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。

因此,开发iOS程序时一般都会使用 nonatomic 属性。但是在开发 Mac OS X 程序时,使用 atomic 属性通常都不会有性能瓶颈。
5. 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。

具体步骤:

需声明该类遵从 NSCopying 协议实现 NSCopying 协议。该协议只有一个方法:
  • (id)copyWithZone:(NSZone *)zone;

注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是 “copyWithZone” 方法。

以第一题的代码为例:

// .h文件   // http://weibo.com/luohanchenyilong/   // https://github.com/ChenYilong   // 修改完的代码   typedef NS_ENUM(NSInteger, CYLSex) {       CYLSexMan,       CYLSexWoman   };   @interface CYLUser : NSObject<NSCopying>   @property (nonatomic, readonly, copy) NSString *name;   @property (nonatomic, readonly, assign) NSUInteger age;   @property (nonatomic, readonly, assign) CYLSex sex;   - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;   + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;   @end

然后实现协议中规定的方法:

- (id)copyWithZone:(NSZone *)zone {    CYLUser *copy = [[[self class] allocWithZone:zone]                      initWithName:_name                                  age:_age                                  sex:_sex];    return copy;}

但在实际的项目中,不可能这么简单,遇到更复杂一点,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。举个例子,假如 CYLUser 中含有一个数组,与其他 CYLUser 对象建立或解除朋友关系的那些方法都需要操作这个数组。那么在这种情况下,你得把这个包含朋友对象的数组也一并拷贝过来。下面列出了实现此功能所需的全部代码:

// .h文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 以第一题《风格纠错题》里的代码为例typedef NS_ENUM(NSInteger, CYLSex) {    CYLSexMan,    CYLSexWoman};@interface CYLUser : NSObject<NSCopying>@property (nonatomic, readonly, copy) NSString *name;@property (nonatomic, readonly, assign) NSUInteger age;@property (nonatomic, readonly, assign) CYLSex sex;- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;- (void)addFriend:(CYLUser *)user;- (void)removeFriend:(CYLUser *)user;@end// .m文件// .m文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong//@implementation CYLUser {   NSMutableSet *_friends;}- (void)setName:(NSString *)name {   _name = [name copy];}- (instancetype)initWithName:(NSString *)name                        age:(NSUInteger)age                        sex:(CYLSex)sex {   if(self = [super init]) {       _name = [name copy];       _age = age;       _sex = sex;       _friends = [[NSMutableSet alloc] init];   }   return self;}- (void)addFriend:(CYLUser *)user {   [_friends addObject:user];}- (void)removeFriend:(CYLUser *)user {   [_friends removeObject:user];}- (id)copyWithZone:(NSZone *)zone {   CYLUser *copy = [[[self class] allocWithZone:zone]                    initWithName:_name                    age:_age                    sex:_sex];   copy->_friends = [_friends mutableCopy];   return copy;}- (id)deepCopy {   CYLUser *copy = [[[self class] alloc]                    initWithName:_name                    age:_age                    sex:_sex];   copy->_friends = [[NSMutableSet alloc] initWithSet:_friends                                            copyItems:YES];   return copy;}@end

以上做法能满足基本的需求,但是也有缺陷:

如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。

【注:深浅拷贝的概念,在下文中有介绍,详见下文的:用@property声明的 NSString(或NSArray,NSDictionary)经常使用 copy 关键字,为什么?如果改用 strong 关键字,可能造成什么问题?

在例子中,存放朋友对象的 set 是用 “copyWithZone:” 方法来拷贝的,这种浅拷贝方式不会逐个复制 set 中的元素。若需要深拷贝的话,则可像下面这样,编写一个专供深拷贝所用的方法:

- (id)deepCopy {   CYLUser *copy = [[[self class] alloc]                    initWithName:_name                    age:_age                    sex:_sex];   copy->_friends = [[NSMutableSet alloc] initWithSet:_friends                                            copyItems:YES];   return copy;}

至于如何重写带 copy 关键字的 setter这个问题,

如果抛开本例来回答的话,如下:

- (void)setName:(NSString *)name {    //[_name release];    _name = [name copy];}

不过也有争议,有人说“苹果如果像下面这样干,是不是效率会高一些?”

- (void)setName:(NSString *)name {   if (_name != name) {       //[_name release];//MRC       _name = [name copy];   }}

这样真得高效吗?不见得!这种写法“看上去很美、很合理”,

我和总理的意见基本一致:

老百姓 copy 一下,咋就这么难?

你可能会说:

之所以在这里做if判断 这个操作:是因为一个 if 可能避免一个耗时的copy,还是很划算的。 (在刚刚讲的:《如何让自己的类用 copy 修饰符?》里的那种复杂的copy,我们可以称之为 “耗时的copy”,但是对 NSString 的 copy 还称不上。)

但是你有没有考虑过代价:

你每次调用 setX: 都会做 if 判断,这会让 setX: 变慢,如果你在 setX:写了一串复杂的 if+elseif+elseif+... 判断,将会更慢。

要回答“哪个效率会高一些?”这个问题,不能脱离实际开发,就算 copy 操作十分耗时,if 判断也不见得一定会更快,除非你把一个“ @property他当前的值 ”赋给了他自己,代码看起来就像:

[a setX:x1];
[a setX:x1]; //你确定你要这么干?与其在setter中判断,为什么不把代码写好?

或者

[a setX:[a x]]; //队友咆哮道:你在干嘛?!!

不要在 setter 里进行像 if(_obj != newObj) 这样的判断。(该观点参考链接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure )

什么情况会在 copy setter 里做 if 判断? 例如,车速可能就有最高速的限制,车速也不可能出现负值,如果车子的最高速为300,则 setter 的方法就要改写成这样:

-(void)setSpeed:(int)_speed{    if(_speed < 0) speed = 0;    if(_speed > 300) speed = 300;    _speed = speed;}

回到这个题目,如果单单就上文的代码而言,我们不需要也不能重写 name 的 setter :由于是 name 是只读属性,所以编译器不会为其创建对应的“设置方法”,用初始化方法设置好属性值之后,就不能再改变了。( 在本例中,之所以还要声明属性的“内存管理语义”–copy,是因为:如果不写 copy,该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法之前自行拷贝属性值。这种操作多余而低效)。

那如何确保 name 被 copy?在初始化方法(initializer)中做:

  • (instancetype)initWithName:(NSString *)name
    age:(NSUInteger)age
    sex:(CYLSex)sex {
    if(self = [super init]) {
    _name = [name copy];
    _age = age;
    _sex = sex;
    _friends = [[NSMutableSet alloc] init];
    }
    return self;
    }

    1. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

@property 的本质是什么?

@property = ivar + getter + setter;

下面解释下:

“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。

“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为 Objective-C 2.0 的一部分。 而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。 正因为有了这种严格的命名规范,所以 Objective-C 这门语言才能根据名称自动创建出存取方法。其实也可以把属性当做一种关键字,其表示:

编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。 所以你也可以这么说:@property = getter + setter;

例如下面这个类:

@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

上述代码写出来的类与下面这种写法等效:

@interface Person : NSObject- (NSString *)firstName;- (void)setFirstName:(NSString *)firstName;- (NSString *)lastName;- (void)setLastName:(NSString *)lastName;@end

更新:

property在runtime中是objc_property_t定义如下:

typedef struct objc_property *objc_property_t;

而objc_property是一个结构体,包括name和attributes,定义如下:

struct property_t {    const char *name;    const char *attributes;};

而attributes本质是objc_property_attribute_t,定义了property的一些属性,定义如下:

/// Defines a property attributetypedef struct {    const char *name;           /**< The name of the attribute */    const char *value;          /**< The value of the attribute (usually empty) */} objc_property_attribute_t;

而attributes的具体内容是什么呢?其实,包括:类型,原子性,内存语义和对应的实例变量。

例如:我们定义一个string的property@property (nonatomic, copy) NSString *string;,通过 property_getAttributes(property)获取到attributes并打印出来之后的结果为T@”NSString”,C,N,V_string

其中T就代表类型,可参阅Type Encodings,C就代表Copy,N代表nonatomic,V就代表对于的实例变量。

ivar、getter、setter 是如何生成并添加到这个类中的?

“自动合成”( autosynthesis)

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.

@implementation Person@synthesize firstName = _myFirstName;@synthesize lastName = _myLastName;@end

我为了搞清属性是怎么实现的,曾经反编译过相关的代码,他大致生成了五个东西

OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。setter 与 getter 方法对应的实现函数ivar_list :成员变量列表method_list :方法列表prop_list :属性列表

也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.
7. @protocol 和 category 中如何使用 @property

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

要实现 weak 属性,首先要搞清楚 weak 属性的特点:

weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

那么 runtime 如何实现 weak 变量的自动置nil?

runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

(注:在下文的《使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到__weak引用的解除时间。)

先看下 runtime 里源码的实现:

/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line==0, the set is instead a small inline array.
*/

#define WEAK_INLINE_COUNT 4struct weak_entry_t {   DisguisedPtr<objc_object> referent;   union {       struct {           weak_referrer_t *referrers;           uintptr_t        out_of_line : 1;           uintptr_t        num_refs : PTR_MINUS_1;           uintptr_t        mask;           uintptr_t        max_hash_displacement;       };       struct {           // out_of_line=0 is LSB of one of these (don't care which)           weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];       };   };};/*** The global weak references table. Stores object ids as keys,* and weak_entry_t structs as their values.*/struct weak_table_t {   weak_entry_t *weak_entries;   size_t    num_entries;   uintptr_t mask;   uintptr_t max_hash_displacement;};

具体完整实现参照 objc/objc-weak.h 。

我们可以设计一个函数(伪代码)来表示上述机制:

objc_storeWeak(&a, b)函数:

objc_storeWeak函数把第二个参数–赋值对象(b)的内存地址作为键值key,将第一个参数–weak修饰的属性变量(a)的内存地址(&a)作为value,注册到 weak 表中。如果第二个参数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除,

你可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。

在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

而如果a是由 assign 修饰的,则: 在 b 非 nil 时,a 和 b 指向同一个内存地址,在 b 变 nil 时,a 还是指向该内存地址,变野指针。此时向 a 发送消息极易崩溃。

下面我们将基于objc_storeWeak(&a, b)函数,使用伪代码模拟“runtime如何实现weak属性”:

// 使用伪代码模拟:runtime如何实现weak属性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong

id obj1;
objc_initWeak(&obj1, obj);
/obj引用计数变为0,变量作用域结束/
objc_destroyWeak(&obj1);

下面对用到的两个方法objc_initWeak和objc_destroyWeak做下解释:

总体说来,作用是: 通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。

下面分别介绍下方法的内部实现:

objc_initWeak函数的实现是这样的:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。

obj1 = 0;
obj_storeWeak(&obj1, obj);

也就是说:

weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)

然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。

objc_storeWeak(&obj1, 0);

前面的源代码与下列源代码相同。

// 使用伪代码模拟:runtime如何实现weak属性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong

id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* … obj的引用计数变为0,被置nil … */
objc_storeWeak(&obj1, 0);

objc_storeWeak 函数把第二个参数–赋值对象(obj)的内存地址作为键值,将第一个参数–weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从 weak 表中删除,在后面的相关一题会详解。

使用伪代码是为了方便理解,下面我们“真枪实弹”地实现下:

如何让不使用weak修饰的@property,拥有weak的效果。

我们从setter方法入手:

(注意以下的 cyl_runAtDealloc 方法实现仅仅用于模拟原理,如果想用于项目中,还需要考虑更复杂的场景,想在实际项目使用的话,可以使用我写的一个小库,可以使用 CocoaPods 在项目中使用: CYLDeallocBlockExecutor )

- (void)setObject:(NSObject *)object{   objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);   [object cyl_runAtDealloc:^{       _object = nil;   }];}

也就是有两个步骤:

在setter方法中做如下设置:   objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。做到这点,同样要借助 runtime:

//要销毁的目标对象
id objectToBeDeallocated;
//可以理解为一个“事件”:当上面的目标对象销毁时,同时要发生的“事件”。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
someUniqueKey,
objectWeWantToBeReleasedWhenThatHappens,
OBJC_ASSOCIATION_RETAIN);

知道了思路,我们就开始实现 cyl_runAtDealloc 方法,实现过程分两部分:

第一部分:创建一个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助 block 执行“事件”。

// .h文件

// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。

typedef void (^voidBlock)(void);

@interface CYLBlockExecutor : NSObject

  • (id)initWithBlock:(voidBlock)block;

@end

// .m文件

// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。

#import "CYLBlockExecutor.h"@interface CYLBlockExecutor() {   voidBlock _block;}@implementation CYLBlockExecutor- (id)initWithBlock:(voidBlock)aBlock{   self = [super init];   if (self) {       _block = [aBlock copy];   }   return self;}- (void)dealloc{   _block ? _block() : nil;}@end

第二部分:核心代码:利用runtime实现cyl_runAtDealloc方法

// CYLNSObject+RunAtDealloc.h文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 利用runtime实现cyl_runAtDealloc方法#import "CYLBlockExecutor.h"const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;@interface NSObject (CYLRunAtDealloc)- (void)cyl_runAtDealloc:(voidBlock)block;@end// CYLNSObject+RunAtDealloc.m文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 利用runtime实现cyl_runAtDealloc方法#import "CYLNSObject+RunAtDealloc.h"#import "CYLBlockExecutor.h"@implementation NSObject (CYLRunAtDealloc)- (void)cyl_runAtDealloc:(voidBlock)block{   if (block) {       CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];       objc_setAssociatedObject(self,                                runAtDeallocBlockKey,                                executor,                                OBJC_ASSOCIATION_RETAIN);   }}@end

使用方法: 导入

#import “CYLNSObject+RunAtDealloc.h”

然后就可以使用了:

NSObject *foo = [[NSObject alloc] init];

[foo cyl_runAtDealloc:^{
NSLog(@”正在释放foo!”);
}];

如果对 cyl_runAtDealloc 的实现原理有兴趣,可以看下我写的一个小库,可以使用 CocoaPods 在项目中使用: CYLDeallocBlockExecutor

参考博文: Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object
9. @property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?

属性可以拥有的特质分为四类:

原子性--- nonatomic 特质在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用自旋锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。读/写权限---readwrite(读写)、readonly (只读)内存管理语义---assign、strong、 weak、unsafe_unretained、copy方法名---getter=<name> 、setter=<name>

getter=的样式:

   @property (nonatomic, getter=isOn) BOOL on;

setter=这种不常用,也不推荐使用。故不在这里给出写法。)

setter=一般用在特殊的情境下,比如:

在数据反序列化、转模型的过程中,服务器返回的字段如果以 init 开头,所以你需要定义一个 init 开头的属性,但默认生成的 setter 与 getter 方法也会以 init 开头,而编译器会把所有以 init 开头的方法当成初始化方法,而初始化方法只能返回 self 类型,因此编译器会报错。

这时你就可以使用下面的方式来避免编译器报错:

@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;

另外也可以用关键字进行特殊说明,来避免编译器报错:

@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy attribute((objc_method_family(none)));

不常用的:nonnull,null_resettable,nullable

注意:很多人会认为如果属性具备 nonatomic 特质,则不使用 “同步锁”。其实在属性设置方法中使用的是自旋锁,自旋锁相关代码如下:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy){   if (offset == 0) {       object_setClass(self, newValue);       return;   }   id oldValue;   id *slot = (id*) ((char*)self + offset);   if (copy) {       newValue = [newValue copyWithZone:nil];   } else if (mutableCopy) {       newValue = [newValue mutableCopyWithZone:nil];   } else {       if (*slot == newValue) return;       newValue = objc_retain(newValue);   }   if (!atomic) {       oldValue = *slot;       *slot = newValue;   } else {       spinlock_t& slotlock = PropertyLocks[slot];       slotlock.lock();       oldValue = *slot;       *slot = newValue;               slotlock.unlock();   }   objc_release(oldValue);}void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {   bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);   bool mutableCopy = (shouldCopy == MUTABLE_COPY);   reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);}
  1. weak属性需要在dealloc中置nil么?

不需要。

在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理

即便是编译器不帮我们做这些,weak也不需要在 dealloc 中置nil:

正如上文的:runtime 如何实现 weak 属性 中提到的:

我们模拟下 weak 的 setter 方法,应该如下:

- (void)setObject:(NSObject *)object{   objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);   [object cyl_runAtDealloc:^{       _object = nil;   }];}

如果对 cyl_runAtDealloc 的实现原理有兴趣,可以看下我写的一个小库,可以使用 CocoaPods 在项目中使用: CYLDeallocBlockExecutor

也即:

在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
  1. @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 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

  2. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?

    对应基本数据类型默认关键字是

atomic,readwrite,assign 2. 对于普通的 Objective-C 对象

atomic,readwrite,strong

参考链接:

Objective-C ARC: strong vs retain and weak vs assignVariable property attributes or Modifiers in iOS
  1. 用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

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

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

举例说明:

定义一个以 strong 修饰的 array:

@property (nonatomic ,readwrite, strong) NSArray *array;然后进行下面的操作:   NSArray *array = @[ @1, @2, @3, @4 ];   NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];   self.array = mutableArray;   [mutableArray removeAllObjects];;   NSLog(@"%@",self.array);   [mutableArray addObjectsFromArray:array];   self.array = [mutableArray copy];   [mutableArray removeAllObjects];;   NSLog(@"%@",self.array);打印结果如下所示:2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] ()2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] (   1,   2,   3,   4)

(详见仓库内附录的 Demo。)

为了理解这种做法,首先要知道,两种情况:

对非集合类对象的 copy 与 mutableCopy 操作;对集合类对象的 copy 与 mutableCopy 操作。
  1. 对非集合类对象的copy操作:

在非集合类对象中:对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。用代码简单表示如下:

[immutableObject copy] // 浅复制[immutableObject mutableCopy] //深复制[mutableObject copy] //深复制[mutableObject mutableCopy] //深复制

比如以下代码:

NSMutableString *string = [NSMutableString stringWithString:@”origin”];//copy
NSString *stringCopy = [string copy];

查看内存,会发现 string、stringCopy 内存地址都不一样,说明此时都是做内容拷贝、深拷贝。即使你进行如下操作:

[string appendString:@”origion!”]

stringCopy 的值也不会因此改变,但是如果不使用 copy,stringCopy 的值就会被改变。 集合类对象以此类推。 所以,

用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

2、集合类对象的copy与mutableCopy

集合类对象是指 NSArray、NSDictionary、NSSet … 之类的对象。下面先看集合类immutable对象使用 copy 和 mutableCopy 的一个例子:

NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];NSArray *copyArray = [array copy];NSMutableArray *mCopyArray = [array mutableCopy];

查看内容,可以看到 copyArray 和 array 的地址是一样的,而 mCopyArray 和 array 的地址是不同的。说明 copy 操作进行了指针拷贝,mutableCopy 进行了内容拷贝。但需要强调的是:此处的内容拷贝,仅仅是拷贝 array 这个对象,array 集合内部的元素仍然是指针拷贝。这和上面的非集合 immutable 对象的拷贝还是挺相似的,那么mutable对象的拷贝会不会类似呢?我们继续往下,看 mutable 对象拷贝的例子:

NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];NSArray *copyArray = [array copy];NSMutableArray *mCopyArray = [array mutableCopy];

查看内存,如我们所料,copyArray、mCopyArray和 array 的内存地址都不一样,说明 copyArray、mCopyArray 都对 array 进行了内容拷贝。同样,我们可以得出结论:

在集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。用代码简单表示如下:

[immutableObject copy] // 浅复制
[immutableObject mutableCopy] //单层深复制
[mutableObject copy] //单层深复制
[mutableObject mutableCopy] //单层深复制

这个代码结论和非集合类的非常相似。

参考链接:iOS 集合的深复制与浅复制
14. @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量么?

在回答之前先说明下一个概念:

实例变量 = 成员变量 = ivar

这些说法,笔者下文中,可能都会用到,指的是一个东西。

正如 Apple官方文档 You Can Customize Synthesized Instance Variable Names 所说: enter image description here

如果使用了属性的话,那么编译器就会自动编写访问属性所需的方法,此过程叫做“自动合成”( auto synthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法” (synthesized method)的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。

@interface CYLPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

在上例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字:

@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end

上述语法会将生成的实例变量命名为 _myFirstName 与 _myLastName ,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

总结下 @synthesize 合成实例变量的规则,有以下几点:

如果指定了成员变量的名称,会生成一个指定的名称的成员变量,如果这个成员已经存在了就不再生成了.如果是 @synthesize foo; 还会生成一个名称为foo的成员变量,也就是说:如果没有指定成员变量的名称会自动生成一个属性同名的成员变量,如果是 @synthesize foo = _foo; 就不会生成成员变量了.

假如 property 名为 foo,存在一个名为 _foo 的实例变量,那么还会自动合成新变量么? 不会。如下图:

enter image description here
15. 在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?

回答这个问题前,我们要搞清楚一个问题,什么情况下不会autosynthesis(自动合成)?

同时重写了 setter 和 getter 时重写了只读属性的 getter 时使用了 @dynamic 时在 @protocol 中定义的所有属性在 category 中定义的所有属性重载的属性

当你在子类中重载了父类中的属性,你必须 使用 @synthesize 来手动合成ivar。

除了后三条,对其他几个我们可以总结出一个规律:当你想手动管理 @property 的所有内容时,你就会尝试通过实现 @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic 来达到这个目的,这时编译器就会认为你打算手动管理 @property,于是编译器就禁用了 autosynthesis(自动合成)。

因为有了 autosynthesis(自动合成),大部分开发者已经习惯不去手动定义ivar,而是依赖于 autosynthesis(自动合成),但是一旦你需要使用ivar,而 autosynthesis(自动合成)又失效了,如果不去手动定义ivar,那么你就得借助 @synthesize 来手动合成 ivar。

其实,@synthesize 语法还有一个应用场景,但是不太建议大家使用:

可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字:

@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end

上述语法会将生成的实例变量命名为 _myFirstName 与 _myLastName,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

举例说明:应用场景:

//// .m文件// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)// https://github.com/ChenYilong// 打开第14行和第17行中任意一行,就可编译成功@import Foundation;@interface CYLObject : NSObject@property (nonatomic, copy) NSString *title;@end@implementation CYLObject {   //    NSString *_title;}//@synthesize title = _title;- (instancetype)init{   self = [super init];   if (self) {       _title = @"微博@iOS程序犭袁";   }   return self;}- (NSString *)title {   return _title;}- (void)setTitle:(NSString *)title {   _title = [title copy];}@end

结果编译器报错:

当你同时重写了 setter 和 getter 时,系统就不会生成 ivar(实例变量/成员变量)。这时候有两种选择:

要么如第14行:手动创建 ivar要么如第17行:使用@synthesize foo = _foo; ,关联 @property 与 ivar。

更多信息,请戳- 》 When should I use @synthesize explicitly?
16. objc中向一个nil对象发送消息将会发生什么?

在 Objective-C 中向 nil 发送消息是完全有效的——只是在运行时不会有任何作用:

如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:

Person * motherInlaw = [[aPerson spouse] mother];

如果 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。 2. 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。 2. 如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。 2. 如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。

具体原因如下:

objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

那么,为了方便理解这个内容,还是贴一个objc的源代码:

// runtime.h(类在runtime中的定义)
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong

struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
#if !OBJC2
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。
17. objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

具体原因同上题:该方法编译之后就是objc_msgSend()函数调用.

我们用 clang 分析下,clang 提供一个命令,可以将Objective-C的源码改写成C++语言,借此可以研究下[obj foo]和objc_msgSend()函数之间有什么关系。

以下面的代码为例,由于 clang 后的代码达到了10万多行,为了便于区分,添加了一个叫 iOSinit 方法,

////  main.m//  http://weibo.com/luohanchenyilong///  https://github.com/ChenYilong//  Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.//#import "CYLTest.h"int main(int argc, char * argv[]) {    @autoreleasepool {        CYLTest *test = [[CYLTest alloc] init];        [test performSelector:(@selector(iOSinit))];        return 0;    }}

在终端中输入

clang -rewrite-objc main.m

就可以生成一个main.cpp的文件,在最低端(10万4千行左右)

enter image description here

我们可以看到大概是这样的:

((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName(“foo”));

也就是说:

[obj foo];在objc编译时,会被转意为:objc_msgSend(obj, @selector(foo));。
  1. 什么时候会报unrecognized selector的异常?

简单来说:

当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决。

简单的流程如下,在上一题中也提到过:

objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:

Method resolution

objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。

Fast forwarding

如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。 3. Normal forwarding

这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

为了能更清晰地理解这些方法的作用,git仓库里也给出了一个Demo,名称叫“ _objc_msgForward_demo ”,可运行起来看看。
19. 一个objc对象如何进行内存布局?(考虑有父类的情况)

所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中.每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)成员变量的列表,属性列表,

它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass的指针,指向他的父类对象。

每个 Objective-C 对象都有相同的结构,如下图所示:

enter image description here

翻译过来就是
Objective-C 对象的结构图
ISA指针
根类的实例变量
倒数第二层父类的实例变量

父类的实例变量
类的实例变量

根对象就是NSObject,它的superclass指针指向nil类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。

如图: enter image description here
20. 一个objc对象的isa的指针指向什么?有什么作用?

指向他的类对象,从而可以找到对象上的方法
21. 下面的代码输出什么?

@implementation Son : Father   - (id)init   {       self = [super init];       if (self) {           NSLog(@"%@", NSStringFromClass([self class]));           NSLog(@"%@", NSStringFromClass([super class]));       }       return self;   }   @end

答案:

都输出 Son

NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son

这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。

我们都知道:self 是类的隐藏参数,指向当前调用方法的这个类的实例。那 super 呢?

很多人会想当然的认为“ super 和 self 类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者!他们两个的不同点在于:super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

这也就是为什么说“不推荐在 init 方法中使用点语法”,如果想访问实例变量 iVar 应该使用下划线( _iVar ),而非点语法( self.iVar )。

点语法( self.iVar )的坏处就是子类有可能覆写 setter 。假设 Person 有一个子类叫 ChenPerson,这个子类专门表示那些姓“陈”的人。该子类可能会覆写 lastName 属性所对应的设置方法:

////  ChenPerson.m//  ////  Created by https://github.com/ChenYilong on 15/8/30.//  Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.//#import "ChenPerson.h"@implementation ChenPerson@synthesize lastName = _lastName;- (instancetype)init{   self = [super init];   if (self) {       NSLog(@"��类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));       NSLog(@"��类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([super class]));   }   return self;}- (void)setLastName:(NSString*)lastName{   //设置方法一:如果setter采用是这种方式,就可能引起崩溃//    if (![lastName isEqualToString:@"陈"])//    {//        [NSException raise:NSInvalidArgumentException format:@"姓不是陈"];//    }//    _lastName = lastName;

//设置方法二:如果setter采用是这种方式,就可能引起崩溃
_lastName = @”陈”;
NSLog(@”��类名与方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, @”会调用这个方法,想一下为什么?”);

}

@end

在基类 Person 的默认初始化方法中,可能会将姓氏设为空字符串。此时若使用点语法( self.lastName )也即 setter 设置方法,那么调用将会是子类的设置方法,如果在刚刚的 setter 代码中采用设置方法一,那么就会抛出异常,

为了方便采用打印的方式展示,究竟发生了什么,我们使用设置方法二。

如果基类的代码是这样的:

////  Person.m//  nil对象调用点语法////  Created by https://github.com/ChenYilong on 15/8/29.//  Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.//  #import "Person.h"@implementation Person- (instancetype)init{   self = [super init];   if (self) {       self.lastName = @"";       //NSLog(@"��类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));       //NSLog(@"��类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, self.lastName);   }   return self;}- (void)setLastName:(NSString*)lastName{   NSLog(@"��类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"根本不会调用这个方法");   _lastName = @"炎黄";}@end

那么打印结果将会是这样的:

��类名与方法名:-[ChenPerson setLastName:](在第36行),描述:会调用这个方法,想一下为什么?
��类名与方法名:-[ChenPerson init](在第19行),描述:ChenPerson
��类名与方法名:-[ChenPerson init](在第20行),描述:ChenPerson

我在仓库里也给出了一个相应的 Demo(名字叫:Demo_21题_下面的代码输出什么)。有兴趣可以跑起来看一下,主要看下他是怎么打印的,思考下为什么这么打印。

接下来让我们利用 runtime 的相关知识来验证一下 super 关键字的本质,使用clang重写命令:

$ clang -rewrite-objc test.m

将这道题目中给出的代码被转化为:

 NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));   NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

从上面的代码中,我们可以发现在调用 [self class] 时,会转化成 objc_msgSend函数。看下函数定义:

id objc_msgSend(id self, SEL op, …)

我们把 self 做为第一个参数传递进去。

而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:

id objc_msgSendSuper(struct objc_super *super, SEL op, …)

第一个参数是 objc_super 这样一个结构体,其定义如下:

struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};

结构体有两个成员,第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self 。第二个成员是记录当前类的父类是什么。

所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。

objc Runtime开源代码对- (Class)class方法的实现:

  • (Class)class {
    return object_getClass(self);
    }

而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。 第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。

第二步是去 Father这个类里去找 - (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用,

此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。

参考链接:微博@Chun_iOS的博文刨根问底Objective-C Runtime(1)- Self & Super
22. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
23. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

在ARC下不需要。在MRC中,对于使用retain或copy策略的需要 。在MRC下也不需要无论在MRC下还是ARC下均不需要。

2011年版本的Apple API 官方文档 - Associative References 一节中有一个MRC环境下的例子:

// 在MRC下,使用runtime Associate方法关联的对象,不需要在主对象dealloc的时候释放// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)// https://github.com/ChenYilong// 摘自2011年版本的Apple API 官方文档 - Associative References static char overviewKey;NSArray *array =    [[NSArray alloc] initWithObjects:@"One", @"Two", @"Three", nil];// For the purposes of illustration, use initWithFormat: to ensure// the string can be deallocatedNSString *overview =    [[NSString alloc] initWithFormat:@"%@", @"First three numbers"];objc_setAssociatedObject (    array,    &overviewKey,    overview,    OBJC_ASSOCIATION_RETAIN);[overview release];// (1) overview valid[array release];// (2) overview invalid

文档指出

At point 1, the string overview is still valid because the OBJC_ASSOCIATION_RETAIN policy specifies that the array retains the associated object. When the array is deallocated, however (at point 2), overview is released and so in this case also deallocated.

我们可以看到,在[array release];之后,overview就会被release释放掉了。

既然会被销毁,那么具体在什么时间点?

根据 WWDC 2011, Session 322 (第36分22秒) 中发布的内存销毁时间表,被关联的对象在生命周期内要比对象本身释放的晚很多。它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。

对象的内存销毁时间表,分四个步骤:

// 对象的内存销毁时间表
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 根据 WWDC 2011, Session 322 (36分22秒)中发布的内存销毁时间表

  1. 调用 -release :引用计数变为零
    • 对象正在被销毁,生命周期即将结束.
    • 不能再有新的 __weak 弱引用, 否则将指向 nil.
    • 调用 [self dealloc]
  2. 子类 调用 -dealloc
    • 继承关系中最底层的子类 在调用 -dealloc
    • 如果是 MRC 代码 则会手动释放实例变量们(iVars)
    • 继承关系中每一层的父类 都在调用 -dealloc
  3. NSObject 调 -dealloc
    • 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法
  4. 调用 object_dispose()
    • 为 C++ 的实例变量们(iVars)调用 destructors
    • 为 ARC 状态下的 实例变量们(iVars) 调用 -release
    • 解除所有使用 runtime Associate方法关联的对象
    • 解除所有 __weak 引用
    • 调用 free()

对象的内存销毁时间表:参考链接。
24. objc中的类方法和实例方法有什么本质区别和联系?

类方法:

类方法是属于类对象的类方法只能通过类对象调用类方法中的self是类对象类方法可以调用其他的类方法类方法中不能访问成员变量类方法中不能直接调用对象方法

实例方法:

实例方法是属于实例对象的实例方法只能通过实例对象调用实例方法中的self是实例对象实例方法中可以访问成员变量实例方法中直接调用实例方法实例方法中也可以调用类方法(通过类名)

下一篇文章将发布在这里,会对以下问题进行总结,并将本篇文章的勘误一并列出,欢迎指正!请持续关注微博@iOS程序犭袁

@property部分主要参考 Apple官方文档:Properties Encapsulate an Object’s Values runtime部分主要参考Apple官方文档:Declared Properties
25. _objc_msgForward函数是做什么的,直接调用它将会发生什么?
26. runtime如何实现weak变量的自动置nil?
27. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
28. runloop和线程有什么关系?
29. runloop的mode作用是什么?
30. 以+ scheduledTimerWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
31. 猜想runloop内部是如何实现的?
32. objc使用什么机制管理对象内存?
33. ARC通过什么方式帮助开发者管理内存?
34. 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
35. BAD_ACCESS在什么情况下出现?
36. 苹果是如何实现autoreleasepool的?
37. 使用block时什么情况会发生引用循环,如何解决?
38. 在block内如何修改block外部变量?
39. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?
40. GCD的队列(dispatch_queue_t)分哪两种类型?
41. 如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
42. dispatch_barrier_async的作用是什么?
43. 苹果为什么要废弃dispatch_get_current_queue?
44. 以下代码运行结果如何?

- (void)viewDidLoad{    [super viewDidLoad];    NSLog(@"1");    dispatch_sync(dispatch_get_main_queue(), ^{        NSLog(@"2");    });    NSLog(@"3");}

45 addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?
46. 如何手动触发一个value的KVO
47. 若一个类有实例变量NSString *_foo,调用setValue:forKey:时,可以以foo还是_foo作为key?
48. KVC的keyPath中的集合运算符如何使用?
49. KVC和KVO的keyPath一定是属性么?
50. 如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?
51. apple用什么方式实现对一个对象的KVO?
52. IBOutlet连出来的视图属性为什么可以被设置成weak?
53. IB中User Defined Runtime Attributes如何使用?
54. 如何调试BAD_ACCESS错误
55. lldb(gdb)常用的调试命令?

原创粉丝点击