2017年5月iOS招人心得答案总结(中级篇)

技术 中级 Block
1.block的实质是什么?一共有几种block?都是什么情况下生成的?
block定义:

struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); }; struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ };

Clang(LLVM编译器)将含有Block语法的源代码转换为我们可读源代码的功能。通过“-rewrite-objc”选项就能将含有Block语法的源代码变换为C++的源代码(本质是使用了struct结构的C语言源代码)
而Block实质上就是Objective-C对象;
根据isa指针,block一共有3种类型的block
  • _NSConcreteGlobalBlock 全局静态
  • _NSConcreteStackBlock 保存在栈中,出函数作用域就销毁
  • _NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁而ARC和MRC中,还略有不同;
    《Objective-C高级编程》Blocks 阅读笔记 item2(Block的实质)
2.为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
Block只捕获Block中会用到的变量。由于只捕获了自动变量(自动变量是以值传递方式传递到Block的构造函数里面)的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。
深入研究Block捕获外部变量和__block实现原理
3.模拟一下循环引用的一个情况?block实现界面反向传值如何实现?
#import "ViewController.h" #import "Student.h"@interface ViewController () @end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; Student *student = [[Student alloc]init]; student.name = @"Hello World"; student.study = ^{ NSLog(@"my name is = %@",student.name); }; }

student的study的Block里面强引用了student自身。根据上篇文章的分析,可以知道,_NSConcreteMallocBlock捕获了外部的对象,会在内部持有它。retainCount值会加一。
更多例子参考:
iOS循环引用问题集合
block界面反向传值实例:
1、在第二个视图控制器的.h文件中定义声明Block属性:
// NextViewController.h // 定义block @property (nonatomic,copy) void (^NextViewControllerBlock) (NSString *tfText);

// NextViewController.m @interface NextViewController () @property (weak, nonatomic) IBOutlet UITextField *inputTF; @end- (IBAction)BtnAction:(id)sender { //判断block是否为空 if (self.NextViewControllerBlock) { self.NextViewControllerBlock(self.inputTF.text); } [self.navigationController popViewControllerAnimated:YES]; }

2、在第一个视图中获得第二个视图控制器,并且用第二个视图控制器来调用定义的属性:
// AViewController.m @interface AViewController () @property (weak, nonatomic) IBOutlet UILabel *nextVCInfoLabel; @end- (IBAction)btnClicked:(id)sender { NextViewController *nextVC = [[NextViewController alloc]init]; nextVC.NextViewControllerBlock = ^(NSString *tfText){ self.nextVCInfoLabel.text = tfText; }; [self.navigationController pushViewController:nextVC animated:YES]; }

Runtime
1.objc在向一个对象发送消息时,发生了什么?
Objc Runtime使得C具有了面向对象能力,在程序运行时创建,检查,修改类、对象和它们的方法,可以使用runtime的一系列方法实现。
附上OC中一个类的底层数据结构:
mac电脑上的路径/usr/include/objc/runtime.h
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指针查找消息对象,这时会在methodLists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 #endif} OBJC2_UNAVAILABLE;

OC中一个类的对象实例的数据结构(/usr/include/objc/objc.h)
typedef struct objc_class *Class; /// Represents an instance of a class. struct objc_object { Class isaOBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id;

就是定义了一个向object发送消息时,Runtime库会根据object的isa指针找到这个实例object所属于的类,然后在类的方法列表以及父类方法列表寻找对应的方法运行。id是一个objc_object结构类型的指针,这个类型的对象能够转换成任何一种对象。
消息发送函数:objc_msgSend
objc_msgSend()是[obj foo]的具体实现;
在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。
  • 去obj的对应的类中找方法
  • 先找缓存,找不到再去找方法列表
  • 再找父类,如此向上传递
  • 最后再找不到就要转发
2.什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
什么时候会报unrecognized selector的异常?
预防 app crash 之 unrecognized selector
3.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
  • 不能向编译后得到的类中增加实例变量;
  • 能向运行时创建的类中添加实例变量;
解释下:
  • 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;
  • 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。
4.runtime如何实现weak变量的自动置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)。
5.给类添加一个属性后,在类结构体里哪些元素会发生变化?
class object/metaclass
咱们先看一下结构体objc_class的定义
struct objc_class { Class isaOBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_classOBJC2_UNAVAILABLE; const char *nameOBJC2_UNAVAILABLE; long versionOBJC2_UNAVAILABLE; long infoOBJC2_UNAVAILABLE; long instance_sizeOBJC2_UNAVAILABLE; struct objc_ivar_list *ivarsOBJC2_UNAVAILABLE; struct objc_method_list **methodListsOBJC2_UNAVAILABLE; struct objc_cache *cacheOBJC2_UNAVAILABLE; struct objc_protocol_list *protocolsOBJC2_UNAVAILABLE; #endif} OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */

创建一个类属性很简单,主要有以下几个步骤:
  1. 使用@property (class)来声明一个类属性;
  2. 为类属性创建一个存储变量,通常为全局变量;
  3. 实现类属性的getter与setter方法,如果是只读属性,只需要实现getter方法。
类相当于一个对象, 当对象执行这个方法即会发送一条消息, 之后根据isa指针查找消息对象,这时会在methodLists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
RunLoop
1.runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
runloop和线程有什么关系?
2.runloop的mode是用来做什么的?有几种mode?
model 主要是用来指定事件在运行循环中的优先级的,分为:
  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
  • UITrackingRunLoopMode:ScrollView滑动时
  • UIInitializationRunLoopMode:启动时
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的 Mode 有两个:
  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
  • NSRunLoopCommonModes(kCFRunLoopCommonModes)
3.为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的,所以:
Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong //将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];

4. 苹果是如何实现Autorelease Pool的?
autoreleasepool 以一个队列数组的形式实现,主要通过下列三个函数完成.
  • objc_autoreleasepoolPush
  • objc_autoreleasepoolPop
  • objc_autorelease
    看函数名就可以知道,对 autorelease 分别执行 push,和 pop 操作。销毁对象时执行release操作。
举例说明:我们都知道用类方法创建的对象都是 Autorelease 的,那么一旦 Person 出了作用域,当在 Person 的 dealloc 方法中打上断点,我们就可以看到这样的调用堆栈信息:

2017年5月iOS招人心得答案总结(中级篇)
文章图片
堆栈信息.png 类结构
1.isa指针?(对象的isa,类对象的isa,元类的isa都要说)
Objective-C 是一门面向对象的编程语言。每一个对象都是一个类的实例。在objective-c语言的内部,每一个对象都有一个名为 isa 的指针,指向该对象的类。每一个类描述了一系列它的实例的特点,包括成员变量的列表,成员函数的列表等。每一个对象都可以接受消息,而对象能够接收的消息列表是保存在它所对应的类中。

2017年5月iOS招人心得答案总结(中级篇)
文章图片
实例、类对象、元类关系图解.png
iOS class深入理解: 实例对象、类对象、元类和isa指针
2.类方法(class method)和实例方法(instance method)有什么区别?
类方法,也称静态方法,指的是用static关键字修饰的方法。此方法属类本身的方法,不属于类的某一个实例(对象)。类方法中不可直接使用实例变量。其调用方式有三种:可直接调用、类名.方法名、对象名.方法名。实例方法指的是不用static关键字修饰的方法。每个实例对象都有自身的实例方法,互相独立,不共享一个。其调用方式只能是对象名.方法名。
  • instance method 以减号 "-" 开头
  • class method 以加号 “+” 开头,相当于static方法
区别:
静态方法在程序开始时生成内存,实例方法在程序运行中生成内存,
所以静态方法可以直接调用,实例方法要先成生实例,通过实例调用方法,静态速度很快,但是多了会占内存。
静态内存是连续的,因为是在程序开始时就生成了,而实例申请的是离散的空间,所以当然没有静态方法快,
而且静态内存是有限制的,太多了程序会启动不了。
使用场景:
如果需要访问或者修改某个实例的成员变量时,将该方法定义成实例方法。
类方法正好相反,它不需要访问或者修改某个实例的成员变量。
类方法一般用于实现一些工具方法,比如对某个对象进行扩展,或者实现单例。
类方法常驻内存,实例方法不是,所以类方法效率高但占内存。
类方法在堆上分配内存,实例方法在堆栈上。
事实上所有的方法都不可能在堆或者堆栈上分配内存,方法作为代码是被加载到特殊的代码内存区域,这个内存区域是不可写的。
实例方法需要先创建实例才可以调用,比较麻烦,类方法不用,比较简单。
事实上如果一个方法与他所在类型的实例无关,那么它就应该是静态的,决不会有人把它写成实例方法。所以所有的实例方法都与实例有关,既然与实例有关,那么创建实例就是必然的步骤,没有麻烦简单一说。实际上上你可以把所有的实例方法都写成静态的,将实例作为参数传入即可。
3.介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?
类别(Category)主要有3个作用:
  • 将类的实现分散到多个不同文件或多个不同框架中。
  • 创建对私有方法的前向引用。
  • 向对象添加非正式协议。
声明:@interface 类名(分类名称) @end
实现:@implementation 类名(分类名称) @end
注意:
(1)在分类只能增加方法,不能增加成员变量,如果要增加成员变量的话该考虑用继承去实现
(2)在分类实现方法中可以访问类中的成员变量但是不能访问类中的属性@property
(3)在分类中可以重新实现原类中的方法,但会将原类中的方法覆盖而失效。
因为在执行对象成员方法的时候会优先去分类中查找,然后再去原类中去查找,最后去父类中去查找
(4)如果一个类有多个分类,而且分类中有同名的方法那么最后编译的分类会将前面编译的分类覆盖而执行输出
// 另外一份解释来自《招聘一个靠谱的 iOS》—参考答案(上)---24 // 类方法: - 类方法是属于类对象的 - 类方法只能通过类对象调用 - 类方法中的self是类对象 - 类方法可以调用其他的类方法 - 类方法中不能访问成员变量 - 类方法中不能直接调用对象方法//实例方法: -实例方法是属于实例对象的 -实例方法只能通过实例对象调用 -实例方法中的self是实例对象 -实例方法中可以访问成员变量 -实例方法中直接调用实例方法 -实例方法中也可以调用类方法(通过类名)

4.运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?
Category中不能动态添加成员变量;
在Objective-C提供的runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是阅读过苹果的官方文档的人应该会看到:
This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
大概的意思说,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。
然而如果在运行时动态生成一个类,就可以为其添加成员变量和方法, 如下图所示:
添加的方法必须是已经实现的,所以先手写这个方法

2017年5月iOS招人心得答案总结(中级篇)
文章图片

2017年5月iOS招人心得答案总结(中级篇)
文章图片

如上就动态创建了一个类。下面我们开始使用这个类。

2017年5月iOS招人心得答案总结(中级篇)
文章图片

打印结果如下:
2017年5月iOS招人心得答案总结(中级篇)
文章图片
5.objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)
objc中向一个nil对象发送消息将会发生什么?
附:
附:
2017年5月iOS招人心得答案总结(基础篇)
2017年5月iOS招人心得答案总结(中级篇)
2017年5月iOS招人心得答案总结(高级篇)
【2017年5月iOS招人心得答案总结(中级篇)】题库:
2017年5月iOS招人心得(附面试题)

    推荐阅读