• 招聘一个靠谱的iOS


    1. 风格纠错题

    01.png

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

    40.png

    最终改为:

    下面对具体修改的地方,21.png

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

    什么情况使用 weak 关键字?

    1)在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如:delegate代理属性

    2)自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak,自定义IBOutlet控件属性一般也使用weak;当然,也可以使用strong。在下文也有论述:《IBOutlet连出来的视图属性为什么可以被设置成weak?》

    不同点:

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

    2)assigin 可以用非OC对象,而weak必须用于OC对象

    3. 怎么用 copy 关键字?

    用途:

    1)NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;

    2)block也经常使用copy关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks

    block使用copy是从MRC遗留下来的“传统”,在MRC中,方法内部的block是在栈区的,使用copy可以把它放到堆区.在ARC中写不写都行:对于block使用copy还是strong效果是一样的,但写上copy也无伤大雅,还能时刻提醒我们:编译器自动对block进行了copy操作。

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

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

    5. 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

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

    具体步骤:

    1)需声明该类遵从NSCopying协议

    2)实现NSCopying协议。该协议只有一个方法:

    1
    - (id)copyWithZone: (NSZone*) zone

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

    6. @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;

    例如下面这个类:

    1
    2
    3
    4
    @interface Person : NSObject 
    @property NSString *firstName; 
    @property NSString *lastName; 
    @end

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

    1
    2
    3
    4
    5
    6
    @interface Person : NSObject 
    - (NSString *)firstName; 
    - (void)setFirstName:(NSString *)firstName; 
    - (NSString *)lastName; 
    - (void)setLastName:(NSString *)lastName; 
    @end

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

    “自动合成”( autosynthesis)

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

    1
    2
    3
    4
    @implementation Person 
    @synthesize firstName = _myFirstName; 
    @synthesize lastName = myLastName; 
    @end

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

    1)OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。

    2)setter与getter方法对应的实现函数

    3)ivar_list :成员变量列表

    4)method_list :方法列表

    5)prop_list :属性列表

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

    7. @protocol 和 category 中如何使用 @property

    1)在protocol中使用property只会生成setter和getter方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性

    2)category 使用 @property 也是只会生成setter和getter方法的声明,如果我们真的需要给category增加属性的实现,需要借助于运行时的两个函数:

    ①objc_setAssociatedObject

    ②objc_getAssociatedObject

    8. 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。

    9. @property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?

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

    • 原子性---nonatomic特质

    在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不使用同步锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备nonatomic特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。

    • 读/写权限---readwrite(读写)、readooly (只读)

    • 内存管理语义---assign、strong、 weak、unsafe_unretained、copy

    • 方法名---getter=、setter=

    getter=的样式:

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

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

    • 不常用的:nonnull,null_resettable,nullable

    10. weak属性需要在dealloc中置nil么?

    不需要。

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    - (void)setObject:(NSObject *)object
    {
        objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
        [object cyl_runAtDealloc:^{
            _object = nil;
        }];
    }

    也即:在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

    11. @synthesize和@dynamic分别有什么作用?

    1)@property有两个对应的词,一个是@synthesize,一个是@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;

    2)@synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。

    3)@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成。(当然对于readonly的属性只需提供getter即可)。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var = someVar,由于缺setter方法会导致程序崩溃;或者当运行到 someVar = var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

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

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

    atomic,readwrite,assign

    • 对于普通的OC对象

    atomic,readwrite,strong

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

    1)因为父类指针可以指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.

    2)如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

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

    为了理解这种做法,首先要知道,对非集合类对象的copy操作:

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

    • [immutableObject copy] // 浅复制

    • [immutableObject mutableCopy] //深复制

    • [mutableObject copy] //深复制

    • [mutableObject mutableCopy] //深复制

    比如以下代码:

    1
    2
    NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy
    NSString *stringCopy = [string copy];

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

    1
    [string appendString:@"origion!"]

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

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

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

    实例变量 = 成员变量 = ivar

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

    1
    2
    3
    4
    @interface CYLPerson : NSObject 
    @property NSString *firstName; 
    @property NSString *lastName; 
    @end

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

    1
    2
    3
    4
    @implementation CYLPerson 
    @synthesize firstName = _myFirstName; 
    @synthesize lastName = _myLastName; 
    @end

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

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

    1)如果指定了成员变量的名称,会生成一个指定的名称的成员变量,

    2)如果这个成员已经存在了就不再生成了.

    3)如果是 @synthesize foo; 还会生成一个名称为foo的成员变量,也就是说:如果没有指定成员变量的名称会自动生成一个属性同名的成员变量。

    4)如果是 @synthesize foo = _foo; 就不会生成成员变量了.

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

    687474703a2f2f692e696d6775722e636f6d2f743238676534572e706e67.png

    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。

    16. objc中向一个nil对象发送消息将会发生什么?

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

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

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

    如果spouse对象为nil,那么发送给nil的消息mother也将返回nil。

    1)如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0。

    2)如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0。

    3)如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

    具体原因如下:

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

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

    1
    2
    3
    // runtime.h(类在runtime中的定义)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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()函数调用.如果我没有记错的大概是这样的:

    1
    ((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));

    也就是说:

    [obj foo];在objc动态编译时,会被转意为:objc_msgSend(obj, @selector(foo));。

    18. 什么时候会报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:消息给目标对象。

    19. 一个objc对象如何进行内存布局?(考虑有父类的情况)

    • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中.

    • 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的

    1)对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)

    2)成员变量的列表

    3)属性列表

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

    687474703a2f2f692e696d6775722e636f6d2f376d4a6c556a312e706e67.png

    1)根对象就是NSobject,它的superclass指针指向nil。

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

    如图: 

    14.png

    20. 一个objc对象的isa的指针指向什么?有什么作用?

    指向他的类对象,从而可以找到对象上的方法

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end

    答案:

    都输出 Son

     

    22. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

    每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.

    23. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

    无论在MRC下还是ARC下均不需要

    24. objc中的类方法和实例方法有什么本质区别和联系?

    类方法:

    • 类方法是属于类对象的

    • 类方法只能通过类对象调用

    • 类方法中的self是类对象

    • 类方法可以调用其他的类方法

    • 类方法中不能访问成员变量

    • 类方法中不定直接调用对象方法

    实例方法:

    • 实例方法是属于实例对象的

    • 实例方法只能通过实例对象调用

    • 实例方法中的self是实例对象

    • 实例方法中可以访问成员变量

    • 实例方法中直接调用实例方法

    • 实例方法中也可以调用类方法(通过类名)

    25. _objc_msgForward函数是做什么的,直接调用它将会发生什么?

    _objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

    26. runtime如何实现weak变量的自动置nil?

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

    上篇中的《runtime 如何实现 weak 属性》有论述。(注:在上篇的《使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到__weak引用的解除时间。)

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

    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属性”:

    1
    2
    3
    4
    5
    6
    7
    8
    // 使用伪代码模拟:runtime如何实现weak属性
     
     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函数。

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

    也就是说:

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

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

    1
    objc_storeWeak(&obj1, 0);

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 使用伪代码模拟:runtime如何实现weak属性
     
    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表中删除。

    27. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

    • 不能向编译后得到的类中增加实例变量;

    • 能向运行时创建的类中添加实例变量;

    解释下:

    • 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;

    • 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

    28. runloop和线程有什么关系?

    总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。

    runloop 和线程的关系:

    1. 主线程的run loop默认是启动的。

    iOS的应用程序里面,程序启动后会有一个如下的main()函数

    1
    2
    3
    4
    int main(int argc, char * argv[]) {
    @autoreleasepool {    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    }

    重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

    2. 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

    3. 在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。

    1
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];

    参考链接:《Objective-C之run loop详解》

    29. runloop的mode作用是什么?

    model 主要是用来指定事件在运行循环中的优先级的,分为:

    • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态

    • UITrackingRunLoopMode:ScrollView滑动时

    • UIInitializationRunLoopMode:启动时

    • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

    苹果公开提供的 Mode 有两个:

    1. NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

    2. NSRunLoopCommonModes(kCFRunLoopCommonModes)

    30. 以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

    RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响scrllView的滑动。

    如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

    同时因为mode还是可定制的,所以:

    Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 
    // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
     
    //将timer添加到NSDefaultRunLoopMode中
    [NSTimer scheduledTimerWithTimeInterval:1.0
         target:self
         selector:@selector(timerTick:)
         userInfo:nil
         repeats:YES];
    //然后再添加到NSRunLoopCommonModes里
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
         target:self
         selector:@selector(timerTick:)
         userInfo:nil
         repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    31. 猜想runloop内部是如何实现的?

    一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

    1
    2
    3
    4
    5
    6
    7
    function loop() {
        initialize();
        do {
            var message = get_next_message();
            process_message(message);
        while (message != quit);
    }

    32. objc使用什么机制管理对象内存?

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

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

    编译时根据代码上下文,插入 retain/release

    34. 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

    分两种情况:手动干预释放时机、系统自动去释放。

    1. 手动干预释放时机--指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。

    2. 系统自动去释放--不手动指定autoreleasepool

      Autorelease对象会在当前的 runloop 迭代结束时释放。

      如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

    35. BAD_ACCESS在什么情况下出现?

    访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环

    36. 苹果是如何实现autoreleasepool的?

    autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.

    1. objc_autoreleasepoolPush

    2. objc_autoreleasepoolPop

    3. objc_aurorelease

    看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作。

    37. 使用block时什么情况会发生引用循环,如何解决?

    一个对象中强引用了block,在block中又使用了该对象,就会发射循环引用。 解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用。

    1. id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏

    2. id __block weakSelf = self;

    38. 在block内如何修改block外部变量?

    默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上__block来让其写操作生效,示例代码如下:

    1
    2
    3
    4
    5
    6
    __block int a = 0;
    void  (^foo)(void) = ^{ 
        a = 1; 
    }
    f00(); 
    //这里,a的值被修改为1

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

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

    所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,比如这些:

    1
    2
    3
    4
    5
    6
    [UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }]; 
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }]; 
    [[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" 
                                                      object:nil 
                               queue:[NSOperationQueue mainQueue]                                              usingBlock:^(NSNotification * notification) {
                                                        self.someProperty = xyz; }];

    这些情况不需要考虑“引用循环”。

    但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:

    1
    2
    3
    4
    5
    6
    7
    __weak __typeof__(self) weakSelf = self;
    dispatch_group_async(_operationsGroup, _operationsQueue, ^
    {
    __typeof__(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doSomethingElse];
    } );

    类似的:

    1
    2
    3
    4
    5
    6
    7
    8
    __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 显然这也是一个循环引用。

    40. GCD的队列(dispatch_queue_t)分哪两种类型?

    1. 串行队列Serial Dispatch Queue

    2. 并行队列Concurrent Dispatch Queue

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    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(), ^{
            // 合并图片
    });

    42. dispatch_barrier_async的作用是什么?

    在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。 dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等 dispatch_barrier_async 追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。

    打个比方:比如你们公司周末跟团旅游,高速休息站上,司机说:大家都去上厕所,速战速决,上完厕所就上高速。超大的公共厕所,大家同时去,程序猿很快就结束了,但程序媛就可能会慢一些,即使你第一个回来,司机也不会出发,司机要等待所有人都回来后,才能出发。 dispatch_barrier_async 函数追加的内容就如同 “上完厕所就上高速”这个动作。

    43. 苹果为什么要废弃dispatch_get_current_queue?

    dispatch_get_current_queue容易造成死锁

    44. 以下代码运行结果如何?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        NSLog(@"1");
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2");
        });
        NSLog(@"3");
    }

    只输出:1 。发生主线程锁死。

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

    1
    2
    3
    4
    5
    6
    7
    8
    // 添加键值观察
    /*
    1 观察者,负责处理监听事件的对象
    2 观察的属性
    3 观察的选项
    4 上下文
    */
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];

    observer中需要实现一下方法:

    1
    2
    3
    4
    5
    6
    7
    8
    // 所有的 kvo 监听到事件,都会调用此方法
    /*
     1. 观察的属性
     2. 观察的对象
     3. change 属性变化字典(新/旧)
     4. 上下文,与监听的时候传递的一致
     */
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

    46. 如何手动触发一个value的KVO

    所谓的“手动触发”是区别于“自动触发”:

    自动触发是指类似这种场景:在注册 KVO 之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。

    想知道如何手动触发,必须知道自动触发 KVO 的原理:

    键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。

    那么“手动触发”的使用场景是什么?一般我们只在希望能控制“回调的调用时机”时才会这么做。

    具体做法如下:

    如果这个 value 是 表示时间的 self.now ,那么代码如下:最后两行代码缺一不可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //  .m文件
    //  微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/).
    //  手动触发 value 的KVO,最后两行代码缺一不可。
     
    //@property (nonatomic, strong) NSDate *now;
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        [self willChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
        [self didChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
    }

    但是平时我们一般不会这么干,我们都是等系统去“自动触发”。“自动触发”的实现原理:

    比如调用 setNow: 时,系统还会以某种方式在中间插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的调用。

    大家可能以为这是因为 setNow: 是合成方法,有时候我们也能看到人们这么写代码:

    1
    2
    3
    4
    5
    - (void)setNow:(NSDate *)aDate {
        [self willChangeValueForKey:@"now"]; // 没有必要
        _now = aDate;
        [self didChangeValueForKey:@"now"];// 没有必要
    }

    这是完全没有必要的代码,不要这么做,这样的话,KVO代码会被调用两次。KVO在调用存取方法之前总是调用 willChangeValueForKey: ,之后总是调用 didChangeValueForkey: 。怎么做到的呢?答案是通过 isa 混写(isa-swizzling)。下文《apple用什么方式实现对一个对象的KVO?》会有详述。

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

    都可以。

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

    1. 必须用在集合对象上或普通对象的集合属性上

    2. 简单集合运算符有@avg, @count , @max , @min ,@sum,

    3. 格式 @"@sum.age"或 @"集合属性.@max.age"

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

    KVO支持实例变量

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

    请参考:《如何自己动手实现 KVO

    52. IBOutlet连出来的视图属性为什么可以被设置成weak?

    参考链接: Should IBOutlets be strong or weak under ARC?

    文章告诉我们:

    因为既然有外链那么视图在xib或者storyboard中肯定存在,视图已经对它有一个强引用了。

    不过这个回答漏了个重要知识,使用storyboard(xib不行)创建的vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryboard的私有数组强引用所有top level的对象,所以这时即便outlet声明成weak也没关系

    53. IB中User Defined Runtime Attributes如何使用?

    它能够通过KVC的方式配置一些你在interface builder 中不能配置的属性。当你希望在IB中作尽可能多得事情,这个特性能够帮助你编写更加轻量级的viewcontroller

    54. 如何调试BAD_ACCESS错误

    1. 重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object

    2. 通过 Zombie

    blob.png

    3. 设置全局断点快速定位问题代码所在行

    4. Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选?Enable Address Sanitizer

    blob.png

    55. lldb(gdb)常用的调试命令?

    • breakpoint 设置断点定位到某一个函数

    • n 断点指针下一步

    • po打印对象

    更多 lldb(gdb) 调试命令可查看

  • 相关阅读:
    mariadb配置双主多从
    mq系列rabbitmq-02集群+高可用配置
    mq系列rabbitmq-01简介,安装,api操作
    持续集成框架jenkins介绍02-持久集成git仓库+maven项目
    git仓库相关知识03-搭建远程仓库服务器
    RecyclerView瀑布流优化方案探讨
    Android实际开发bug大总结
    Android打造万能自定义阴影控件
    PagerAdapter深度解析和实践优化
    Java博客大汇总
  • 原文地址:https://www.cnblogs.com/linxiu-0925/p/5637981.html
Copyright © 2020-2023  润新知