答面试题·答J_Knight_《2017年5月iOS招人心得(附面试题)》中的面试题(二)

1,999 阅读9分钟

离职找工作中,刷一刷网上的面试题。原文链接

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在向一个对象发送消息时,发生了什么?

简单的说大概是:

  1. 判断该对象是否为nil,为nil则返回nil
  2. 不为nil,就从该对象的isa指针找到该对象的类
  3. 从类的cache里面查找方法,如果命中则调用该方法
  4. 未命中则从类的方法列表里面查找
  5. 依然未命中,则从父类,父类的父类...方法列表里面查找
  6. 还是未命中则走消息转发机制
    1. 调用+(BOOL)resolveInstanceMethord:(SEL)sel;
    2. 调用+(BOOL)forwardingTargetForSelector:(SEL)sel
    3. 嗲用+(void)methodSignatureForSelector:(SEL)sel
    4. 调用+(void)forwardInvocation:(NSInvocation *)invocation

具体的可以看这篇文章,他从底层代码分析了Objective-C 消息发送与转发机制原理

25. 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?

当一个类未实现调用的方法的时候就会报unrecognized selector。要解决这个问题可以从上一题的消息转发机制入手。

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

不能向编译后的类增加实例变量。但是可以用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

给一个类添加变量会更改该类的实例的内存布局。所以,不能动态的修改类的实例变量。

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的结构。结合 category 工作原理分析 OC2.0 中的 runtime

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_objectbit字段会添加成员变量,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迭代中都加入了自动释放池PushPop.

每当进行一次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_setAssociatedObjectobjc_getAssociatedObject来关联属性所对应的变量。

37. objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)

什么也不会发生。返回值是对象的话,就返回nil。返回标量的就是默认值,比如CGRect就为(0,0,0,0)。返回值是结构体的话,就返回结构体体的初始值。