离职找工作中,刷一刷网上的面试题。原文链接
23. block的实质是什么?一共有几种block?都是什么情况下生成的?
block是什么呢?
block是能够截获自动变量(局部变量)的匿名函数。写法如下:
^int (int count) {
return count + 1;
}
^{
printf("Blocks\n");
}
block的本质是什么呢?
定义一个简单的block
int main(int argc, const char * argv[]) {
void (^block)() = ^{
printf("Hello World!");
};
block();
return 0;
}
使用Clang命令
clang -rewrite-objc main.m
得到结果
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello World!");}
你定义完block之后,其实是创建了一个函数,在创建结构体的时候把函数的指针一起传给了block,所以之后可以拿出来调用。
其实,Block 是转化为 Block 结构体类型的自动变量,类型定义如下。通过void *isa;,可以知道 Block 也是一个 OC 的对象,这就是 Block 的本质。
一共有几种block?
根据isa指针,block一共有3种类型的block
* _NSConcreteGlobalBlock 全局静态,保存在数据.data 区域
* _NSConcreteStackBlock 保存在栈中,出函数作用域就销毁
* _NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁
这几种block是什么情况下生成的
遇到一个Block,我们怎么判断这个Block的存储位置呢?
- 定义在函数外面的block,或者block不访问外界变量(包括栈中和堆中的变量)为全局block。例如
typedef int (^blk_t)(int);
blk_t gBlk = ^(int count) {return count;};
int test(){
for(int i=0;i<10;i++){
blk_t blk = ^(int count) {return count;};
}
}
-
Block访问外界变量
- MRC 环境下:访问外界变量的 Block 默认存储栈中。
- ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。
引用于这篇文章
ibireme大神写的关于block的这篇文章
关于block的详解这篇文章也写得很好。
24. 为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
因为变量被拷贝的到block结构体中了。所以不能修改。对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值。
clang代码
__block int val = 10;
转换成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
会发现一个局部变量加上__block修饰符后竟然跟block一样变成了一个__Block_byref_val_0结构体类型的自动变量实例!
23. 模拟一下循环引用的一个情况?block实现界面反向传值如何实现?
循环引用
self.someBlock = ^{
[self dosomething];
};
block实现界面反向传值如何实现
这个没明白什么意思。难道是这样?
@interface OneCell : UITableViewCell
@property (noaotomic,copy)^someAction(OneCell *cell , Action type);
@end
24. objc在向一个对象发送消息时,发生了什么?
简单的说大概是:
- 判断该对象是否为nil,为nil则返回nil
- 不为nil,就从该对象的isa指针找到该对象的类
- 从类的cache里面查找方法,如果命中则调用该方法
- 未命中则从类的方法列表里面查找
- 依然未命中,则从父类,父类的父类...方法列表里面查找
- 还是未命中则走消息转发机制
- 调用
+(BOOL)resolveInstanceMethord:(SEL)sel; - 调用
+(BOOL)forwardingTargetForSelector:(SEL)sel - 嗲用
+(void)methodSignatureForSelector:(SEL)sel - 调用
+(void)forwardInvocation:(NSInvocation *)invocation
- 调用
具体的可以看这篇文章,他从底层代码分析了Objective-C 消息发送与转发机制原理
25. 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
当一个类未实现调用的方法的时候就会报unrecognized selector。要解决这个问题可以从上一题的消息转发机制入手。
26. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后的类增加实例变量。但是可以用class_addIvar向运行时创建的类添加实例变量。引用苹果官方的话说:
This function may only be called after
objc_allocateClassPairand beforeobjc_registerClassPair. Adding an instance variable to an existing class is not
给一个类添加变量会更改该类的实例的内存布局。所以,不能动态的修改类的实例变量。
27. runtime如何实现weak变量的自动置nil?
网上有很多种方法。我想得比较简单。用一个NSTableMap来装属性变量。
@interface ViewController(addWeak)
@property(nonatomic,readonly)NSMapTable *propertyMap;
@property(nonatomic,weak)NSObject *someObj;
@end
@implementation ViewController(addWeak)
static NSString *mapKey = @"mapKey";
-(NSMapTable *)propertyMap{
NSMapTable *table = objc_getAssociatedObject(self, &mapKey);
if (table==nil) {
table = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
}
return table;
}
-(NSObject *)someObj{
return [self.propertyMap objectForKey:@"someObj"];
}
-(void)setSomeObj:(NSObject *)someObj{
if (someObj == nil) {
[self.propertyMap removeObjectForKey:@"someObj"];
}else{
[self.propertyMap setObject:someObj forKey:@"someObj"];
}
}
@end
28. 给类添加一个属性后,在类结构体里哪些元素会发生变化?
bestswifter大神的这篇关于Catgory的文章第二段有详细说明OC2.0中Class的结构。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
};
其中class_rw_t 结构体如下:
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
}
在新加一个property的时候,objc_object的bit字段会添加成员变量,data字段中methors会增加响应的get,set方法,properties数组会添加元素。
29. runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
runloop是来做什么的
runploop是为了让线程一直接受任务不退出的。
runloop和线程有什么关系
Runloop和线程是一一对应的。1个线程只有1个Runloop。他们的关系是放在一个全局的Dictionary里面的。线程刚创建时没有 RunLoop,如果你不主动获取,那它一直都不会有。苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。 RunLoop只能在一个线程的内部获取。
主线程默认开启了runloop么?子线程呢?
主线程默认开启了runloop。子线程没有。
30. runloop的mode是用来做什么的?有几种mode?
在ibireme大神的深入理解RunLoop有详细的介绍runloop。一下文字也是引用自这篇文章。
CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
ode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。
31. 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
因为当我们滑动ScrollView的时候,当前runloop的model为UITrackingRunLoopMode。所以在NSDefaultRunLoopMode下的NSTimer就不动了。
要解决这个问题。可以用以下几种方式:
- 把timer的model添加
NSRunLoopCommonModes中。 - 把timer添加到子线程的runloop中。
32. 苹果是如何实现Autorelease Pool的?
关于Autorelease Poolsunny大神的的黑幕背后的Autorelease介绍得很好。以下回答也是从这篇文章找的。
AutoreleasePoolPage
AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址- 上面的
id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置 - 一个
AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
释放时机
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop.
每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:
根据传入的哨兵对象地址找到哨兵对象所处的page
在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
33. isa指针?(对象的isa,类对象的isa,元类的isa都要说)
对象的isa指向的是类,类的isa指向的元类,元类的isa指向的NSObject
34. 类方法和实例方法有什么区别?
不是很明白这题的意思。
- 类方法用类调用,实例方法需要实例调用
- 类方法存在于元类的方法列表里,实例方法存在于类的方法列表里
35. 介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?
分类的作用
- 可以把类的实现分开在几个不同的文件里面。
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的category里
- 可以由多个开发者共同完成一个类
- 可以按需加载想要的category 等等。
- 声明私有方法
分类的结构
struct category_t {
const char *name; // 类名
classref_t cls; // 分类所属的类
struct method_list_t *instanceMethods; // 实例方法列表
struct method_list_t *classMethods; // 类方法列表
struct protocol_list_t *protocols; // 遵循的协议列表
struct property_list_t *instanceProperties; // 属性列表
};
分类中的方法是添加到原类方法列表的前面的,并不会替换掉原来的方法。所以就算添加了分类以后依然可以调用原方法
Class currentClass = [MyClass class];
MyClass *my = [[MyClass alloc] init];
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
if ([@"printName" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL);
if (lastImp != NULL) {
fn f = (fn)lastImp;
f(my,lastSel);
}
free(methodList);
}
关于分类美团的这篇文章写得很详细
36. 运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?
这题和第26题有些重复吧。运行时不能动态增加成员变量,能动态增加属性。动态增加属性的时候需要用objc_setAssociatedObject和objc_getAssociatedObject来关联属性所对应的变量。
37. objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)
什么也不会发生。返回值是对象的话,就返回nil。返回标量的就是默认值,比如CGRect就为(0,0,0,0)。返回值是结构体的话,就返回结构体体的初始值。