《招聘一个靠谱的 iOS》笔记

这里只是对《招聘一个靠谱的 iOS》个人觉得比较重要的一些笔记摘抄以便日后复习,有需要的建议看原文

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

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

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

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

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

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

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

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

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

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

或使用伪代码来展示下:

// 
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
int main(int argc, char * argv[]) {
 //程序一直运行状态
 while (AppIsRunning) {
      //睡眠状态,等待唤醒事件
      id whoWakesMe = SleepForWakingUp();
      //得到唤醒事件
      id event = GetEvent(whoWakesMe);
      //开始处理事件
      HandleEvent(event);
 }
 return 0;
}

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

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

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

Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。

释放的时机总结起来,可以用下图来表示:

autoreleasepool与 runloop 的关系图

下面对这张图进行详细的解释:

从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。

我们都知道: 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。

但是如果每次都放进应用程序的 main.m 中的 autoreleasepool 中,迟早有被撑满的一刻。这个过程中必定有一个释放的动作。何时?

在一次完整的运行循环结束之前,会被销毁。

那什么时间会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。

子线程的 runloop 默认是不工作,无法主动创建,必须手动创建。

自定义的 NSOperation 和 NSThread 需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。

但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。

@autoreleasepool 当自动释放池被销毁或者耗尽时,会向自动释放池中的所有对象发送 release 消息,释放自动释放池中的所有对象。

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

参考链接:《黑幕背后的Autorelease》

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

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

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

这是 微博@唐巧_boy的《iOS开发进阶》中的第11.2.3章节中的描述。你同样可以在面试中这样回答,但你并没有答到“点子上”。真正的原因,并没有书这本书里写的这么“神奇”,而且这种说法也有点牵强。面试官肯定会追问“为什么写操作就生效了?”真正的原因是这样的:

我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。

我们可以打印下内存地址来进行验证:

   __block int a = 0;
   NSLog(@"定义前:%p", &a);         //栈区
   void (^foo)(void) = ^{
       a = 1;
       NSLog(@"block内部:%p", &a);    //堆区
   };
   NSLog(@"定义后:%p", &a);         //堆区
   foo();
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义后:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block内部: 0x155b22fc8

“定义后”和“block内部”两者的内存地址是一样的,我们都知道 block 内部的变量会被 copy 到堆区,“block内部”打印的是堆地址,因而也就可以知道,“定义后”打印的也是堆的地址。

那么如何证明“block内部”打印的是堆地址?

把三个16进制的内存地址转成10进制就是:

  1. 定义后前:6171559672
  2. block内部:5732708296
  3. 定义后后:5732708296

中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。

这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。

__block 关键字修饰后,int类型也从4字节变成了32字节,这是 Foundation 框架 malloc 出来的。这也同样能证实上面的结论。(PS:居然比 NSObject alloc 出来的 16 字节要多一倍)。

理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要,要不然你如何解释下面这个现象:

以下代码编译可以通过,并且在block中成功将a的从Tom修改为Jerry。

NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
   NSLog(@"\n 定以前:------------------------------------\n\
         a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
   void (^foo)(void) = ^{
       a.string = @"Jerry";
       NSLog(@"\n block内部:------------------------------------\n\
        a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
       a = [NSMutableString stringWithString:@"William"];
   };
   foo();
   NSLog(@"\n 定以后:------------------------------------\n\
         a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
image

这里的a已经由基本数据类型,变成了对象类型。block会对对象类型的指针进行copy,copy到堆中,但并不会改变该指针所指向的堆中的地址,所以在上面的示例代码中,block体内修改的实际是a指向的堆中的内容。

但如果我们尝试像上面图片中的65行那样做,结果会编译不通过,那是因为此时你在修改的就不是堆中的内容,而是栈中的内容。

上文已经说过:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。栈区是红灯区,堆区才是绿灯区。

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

image

添加Observer
通过runtime偷偷实现了一个子类,并且以NSKVONotifying_+类名来命名
将之前那个对象的isa指针指向了这个子类,并重写class方法伪装成原类。
重写了观察的对象setter方法,并且在重写的中添加了willChangeValueForKey:以及didChangeValueForKey:
移除Observer
只是简单的将对象的isa指向原来的类对象中。

请参考:

  1. 《如何自己动手实现 KVO》
  2. KVO for manually implemented properties

推荐阅读更多精彩内容