欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unit 5

程序员文章站 2022-06-11 20:15:52
...

什么是垃圾回收机制

垃圾回收(Garbage Collection,简称GC)的理论主要基于一个事实:大部分对象的生命周期都很短。所以GC将内存中的对象主要分成两个区域:Young区和Old区。对象先在Young区被创建,然后如果经过一段时间还存活着,则被移动到Old区。由于这两个区里的对象特点不同,采用的内存回收算法也不同。

Young区的对象因为大部分生命周期都很短,每次回收之后只有少部分能够存活,所以采用的算法叫Copying算法,即直接将活着的对象复制到另一个地方。而Young区内部又分成了三块区域:Eden区、From区、To区。每次执行Copying算法时,就将存活的对象从Eden区和From区复制到To区,然后交换From区和To区的名字,即From区变成To区,To区变成From区。

因为Old区的对象都是存活下来的老司机了,所以如果用Copying算法的话,很可能90%的对象都得复制一遍,所以Old区的回收算法叫Mark-Sweep(标记-清除)算法。简单来说,就先把不用的对象标记(Mark)出来,然后回收(Sweep),活着的对象就原封不动地保留下来。因为大部分对象都活着,所以回收的对象并不多。但是这个算法会有一个问题:产生内存碎片(分配给对象的内存之间的内存空洞,也就是说下一个对象的内存地址起始点不是上一个对象内存地址的结束点),故它需要整理内存碎片的逻辑——Compact(紧凑,这个此用没有觉得熟悉,嗯!对,sizeClass)算法,粗略地讲就是将对象插入到这些空洞里,当然这里面还有很多细节,比如空洞的内存大小是否足够容纳将要插入的对象,这里就不一一展开了。

接下来的问题就是GC如何找到需要回收的垃圾对象了。为了避免ARC解决不了的问题——循环引用,GC引入了一个叫可达性的概念。[注解]

GC工作时,GC认为当前的一些对象是有效的,包括:全局变量、栈里面的变量等,然后GC从这些变量出发,去标记这些变量可达的其他变量。这个标记是一个递归过程,最后就像从树根的内存对象开始,把所有的枝叶都标成可达的了。那除了这些可达的变量,别的变量就都是需要被回收了。

而实际上AppleOS X 10.5时也是采用的GC,在10.7时就换成了ARCApple不能忍受的问题是:垃圾回收时,整个程序需要暂停——Stop the World,这便是Android手机有时会卡的原因。当所有对象都需要回收时(关闭应用,新开应用最容易造成卡顿!),那种体验可想而知。而后面的系统升级,GC的优化一直没有停歇过,所以现实情况并没有太糟。


注解:iOS开发中导致循环引用是因为两个对象之间相互强引用,在ARC的内存管理方式下两者均不能被释放,而GC不是这样的。在《Java Platform Performance: Strategies and Tactics》这本书的附录A中有一处说明:

“It’s important to note that not just any strong reference will hold an object in memory. These must be references that chain from a garbage collection root. GC roots are a special class of variable that includes

  • Temporary variables on the stack (of any thread)
  • Static variables (from any class)
  • Special references from JNI native code”。

意思是说:强引用本身并不一定会导致对象的内存不能被回收,当这些引用是由GC roots直接或间接指向的,对象才不会被销毁。GC roots是特殊的一类变量,包括:线程中的局部变量、任何类的静态变量(这就是为什么要少用静态变量的原因)、被JNI引用的变量。

下面这两段代码有什么问题?
//第一段
@property (copy) NSMutableArray<NSImage *> *photos;

- (void)replaceWithStockPhoto:(NSImage *)stockPhoto {
    self.photos = [NSMutableArray<NSImage *> new];

    [self.photos addObject:stockPhoto];
}

//第二段
 - (BOOL)validateDictionary:(NSDictionary *)dict usingChecker:(Checker *)checker error:(NSError **)error {
    __block BOOL isValid = YES;

    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        if ([checker checkObject:obj forKey:key]) {
            return;
        }

        *stop = YES; 
        isValid = NO;

        if(error) {
            *error = [NSError errorWithDomain:...];
        }
    }];

    return isValid;
}

解答一:

  1. 声明属性时没有使用nonatomic修饰,默认为atomic,降低访问效率;
  2. 可变数组不可变赋值的结果是不可变的,所以当向不可变的数组对象发送添加元素的消息运行时会崩溃:unrecognized selector send to instance XXX。

解答二:
在枚举遍历方法内部系统会自动添加一个@autoreleasepool,而NSError **,该参数会被编译器自动重写为NSError * __autoreleasing * 形式。所以这个error 对象在离开这个自动释放池时就被释放了,如果外部访问这个error 对象时,就会出现EXC_BAD_ACCESS (野指针)错误。如果想要在外部访问这个error 对象,可以先在遍历前创建一个临时的NSError 实例,然后在遍历结束后对传入的error 赋值。就像这样:

//..
__block BOOL isValid = YES;
__block NSError *result = nil;

[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    if ([checker checkObject:obj forKey:key]) {
        return;
    }

    *stop = YES; 
    isValid = NO;

    if(error) {
        result = [NSError errorWithDomain:@"错误" code:0 userInfo:nil];
    }
}];

    *error = result;
//...

Objective-C对象内存结构中的isa指针有什么用?

在面向对象的世界里,一切都应该是对象。而Objective-C却没有完全做到这一点,像NSIntegerCGPoint这些都是基本的数值类型,并不具备对象的特征。所以Objective-C提供了将它们包装成类的NSNumberNSValue。在Objective-C中,我们新建的一个对象肯定是某个类的实例,并且类中定义的方法是其所有的对象共享的(节约内存)。为了在对象接收到消息时能够准确快速的响应,那么此时就需要建立对象与类的联系,Objective-C给出的解决方案是isa(按照苹果的命名,姑且将它读为艾萨,也就是is a kind of的意思)。其实打开运行时文件(shift + command + O,输入runtime.h),就可以看到类和对象的定义:

Unit 5

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

//在这里我们可以看到对象和类的本质是结构体

Class是这样定义的:

typedef struct objc_class *Class;

对象的艾萨自然而然就指向了它所属的类。那么现在类本身的艾萨又指向何处呢?根据前面所说的面向对象的设计原则,类本身也应当是对象,它的父类指针指向它的直接父类,它的艾萨指向的是一个被叫做元类(metaclass)的对象。根据这个方法,我们就可确定它的存在了:

OBJC_EXPORT Class objc_getMetaClass(const char *name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);

而这个叫做元类的家伙是用来保存类方法列表的(至于为什么不让类对象直接保存而新增这样子一个概念的原因不太明确,猜测是为了继承关系上的设计完整,还有就是为了实现一些黑魔法,如KVO)。当一个类对象收到消息时,元类后先查找本身是否有该类方法的实现,没有就沿着父类指针查找直到继承链的尽头。

既然元类也是对象,它也应当有父类指针和艾萨。它的父类指针指向的是它的类的父类的元类(淡定,后面会有图示),而它的艾萨就直接指向NSObject元类。NSObject的元类的艾萨指向它自己,形成一个闭环。它的父类指针就指向NSObject类本身。因为NSObject是根类(Swift中不是),所以它没有父类指针。图示如下:

Unit 5

总结之:艾萨指针可以帮助类对象找到消息的实现,并建立实例与类的联系(于2017-8-8补:对象的实例方法没有存储在对象的结构体中,对象将通过自己艾萨来查找所属类的实例方法列表,通过指向父类的指针super来查找父类方法),系统是通过isa - swizzling来实现KVO的,参见官方文档

参考并整理自:

iOS开发by唐巧(微信公众号):iOS面试题