美文网首页ios面试汇总
iOS底层原理-内存管理

iOS底层原理-内存管理

作者: _曾梦想仗剑走天涯 | 来源:发表于2019-12-23 00:36 被阅读0次

    面试题

    使用CADisplayLink、NSTimer有什么注意点?
    CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

    解决方案使用block
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
    
    //最经典的解决方法 NSProxy消息转发
    @interface CCProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target;
    @end
    
    @implementation CCProxy
    + (instancetype)proxyWithTarget:(id)target {
        // NSProxy对象不需要调用init,因为它本来就没有init方法
        CCProxy *proxy = [CCProxy alloc];
        proxy.target = target;
        return proxy;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        return [self.target methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        [invocation invokeWithTarget:self.target];
    }
    @end
    
    所以最终我们以后在使用timer的时候,为了避免循环引用可以这么写
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[CCProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
    

    下面是通过对gcd封装的定时器

    static NSMutableDictionary *timers_;
    dispatch_semaphore_t semaphore_;
    + (void)initialize {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            timers_ = [NSMutableDictionary dictionary];
            semaphore_ = dispatch_semaphore_create(1);
        });
    }
    
    + (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async {
        if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
        
        // 队列
        dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
        
        // 创建定时器
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        // 设置时间
        dispatch_source_set_timer(timer,
                                  dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                                  interval * NSEC_PER_SEC, 0);
        
        
        dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
        // 定时器的唯一标识
        NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
        // 存放到字典中
        timers_[name] = timer;
        dispatch_semaphore_signal(semaphore_);
        
        // 设置回调
        dispatch_source_set_event_handler(timer, ^{
            task();
            
            if (!repeats) { // 不重复的任务
                [self cancelTask:name];
            }
        });
        
        // 启动定时器
        dispatch_resume(timer);
        
        return name;
    }
    
    + (void)cancelTask:(NSString *)name {
        if (name.length == 0) return;
        
        dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
        
        dispatch_source_t timer = timers_[name];
        if (timer) {
            dispatch_source_cancel(timer);
            [timers_ removeObjectForKey:name];
        }
    
        dispatch_semaphore_signal(semaphore_);
    }
    

    介绍下内存的几大区域

    image.png
    代码段:编译之后的代码
    
    数据段
    字符串常量:比如NSString *str = @"123"
    已初始化数据:已初始化的全局变量、静态变量等
    未初始化数据:未初始化的全局变量、静态变量等
    
    栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
    
    堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
    
    int a = 10;
    int b;
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            static int c = 20;
            
            static int d;
            
            int e;
            int f = 20;
    
            NSString *str = @"123";
            
            NSObject *obj = [[NSObject alloc] init];
            
            NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
                  &a, &b, &c, &d, &e, &f, str, obj);
            
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    
    /*
     字符串常量
     str=0x10dfa0068
     
     已初始化的全局变量、静态变量
     &a =0x10dfa0db8
     &c =0x10dfa0dbc
     
     未初始化的全局变量、静态变量
     &d =0x10dfa0e80
     &b =0x10dfa0e84
     
     堆
     obj=0x608000012210
     
     栈
     &f =0x7ffee1c60fe0
     &e =0x7ffee1c60fe4
     */
    

    Tagged Pointer

    从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
    
    在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
    
    使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
    
    当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
    
    objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
    
    如何判断一个指针是否为Tagged Pointer?
    iOS平台,最高有效位是1(第64bit)
    Mac平台,最低有效位是1
    

    想一想下面这段代码会发生什么???
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
    self.name = [NSString stringWithFormat:@"abcdefghijk"];
    });
    }
    这里就不卖官子了,会carsh报错EXC_BAD_ACCESS

    讲一下你对 iOS 内存管理的理解

    在iOS中,使用引用计数来管理OC对象的内存
    
    一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
    
    调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
    
    内存管理的经验总结
    当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
    
    可以通过以下私有函数来查看自动释放池的情况
    extern void _objc_autoreleasePoolPrint(void);
    

    copy和mutableCopy

    拷贝的目的:产生一个副本对象,跟源对象互不影响
    修改了源对象,不会影响副本对象
    修改了副本对象,不会影响源对象

    iOS提供了2个拷贝方法
    1.copy,不可变拷贝,产生不可变副本
    2.mutableCopy,可变拷贝,产生可变副本

    深拷贝和浅拷贝
    1.深拷贝:内容拷贝,产生新的对象
    2.浅拷贝:指针拷贝,没有产生新的对象


    image.png

    在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中
    refcnts是一个存放着对象引用计数的散列表

    weak指针的实现原理
    将弱应用对象的属性存储到hash表中
    当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是
    1、调用objc_release
    2、因为对象的引用计数为0,所以执行dealloc
    3、在dealloc中,调用了_objc_rootDealloc函数
    4、在_objc_rootDealloc中,调用了object_dispose函数
    5、调用objc_destructInstance
    6、最后调用objc_clear_deallocating

    1、初始化时:runtime会调用objc_initWeak函数,objc_initWeak函数会初始化一个新的weak指针指向对象的地址。

    {
        NSObject *obj = [[NSObject alloc] init];
        id __weak obj1 = obj;
    }
    id objc_initWeak(id *location, id newObj) {
    // 查看对象实例是否有效
    // 无效对象直接导致指针释放
        if (!newObj) {
            *location = nil;
            return nil;
        }
        // 这里传递了三个 bool 数值
        // 使用 template 进行常量参数传递是为了优化性能
        return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
    }
    
    可以看出,这个函数仅仅是一个深层函数的调用入口,而一般的入口函数中,都会做一些简单的判断(例如 objc_msgSend 中的缓存判断),这里判断了其指针指向的类对象是否有效,无效直接释放,不再往深层调用函数。否则,object将被注册为一个指向value的__weak对象。而这事应该是objc_storeWeak函数干的。
    

    2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

    id objc_storeWeak(id *location, id value);
    
    // HaveOld:     true - 变量有值
    //             false - 需要被及时清理,当前值可能为 nil
    // HaveNew:     true - 需要被分配的新值,当前值可能为 nil
    //             false - 不需要分配新值
    // CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
    //             false - 用 nil 替代存储
    template bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
    static id storeWeak(id *location, objc_object *newObj) {
        // 该过程用来更新弱引用指针的指向
        // 初始化 previouslyInitializedClass 指针
        Class previouslyInitializedClass = nil;
        id oldObj;
        // 声明两个 SideTable
        // ① 新旧散列创建
        SideTable *oldTable;
        SideTable *newTable;
        // 获得新值和旧值的锁存位置(用地址作为唯一标示)
        // 通过地址来建立索引标志,防止桶重复
        // 下面指向的操作会改变旧值
    retry:
        if (HaveOld) {
            // 更改指针,获得以 oldObj 为索引所存储的值地址
            oldObj = *location;
            oldTable = &SideTables()[oldObj];
        } else {
            oldTable = nil;
        }
        if (HaveNew) {
            // 更改新值指针,获得以 newObj 为索引所存储的值地址
            newTable = &SideTables()[newObj];
        } else {
            newTable = nil;
        }
        // 加锁操作,防止多线程中竞争冲突
        SideTable::lockTwoHaveOld, HaveNew>(oldTable, newTable);
        // 避免线程冲突重处理
        // location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
        if (HaveOld  &&  *location != oldObj) {
            SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
            goto retry;
        }
        // 防止弱引用间死锁
        // 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
        if (HaveNew  &&  newObj) {
            // 获得新对象的 isa 指针
            Class cls = newObj->getIsa();
            // 判断 isa 非空且已经初始化
            if (cls != previouslyInitializedClass  &&
                !((objc_class *)cls)->isInitialized()) {
                // 解锁
                SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
                // 对其 isa 指针进行初始化
                _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
                // 如果该类已经完成执行 +initialize 方法是最理想情况
                // 如果该类 +initialize 在线程中
                // 例如 +initialize 正在调用 storeWeak 方法
                // 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记
                previouslyInitializedClass = cls;
                // 重新尝试
                goto retry;
            }
        }
        // ② 清除旧值
        if (HaveOld) {
            weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
        }
        // ③ 分配新值
        if (HaveNew) {
            newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
                                                          (id)newObj, location,
                                                          CrashIfDeallocating);
            // 如果弱引用被释放 weak_register_no_lock 方法返回 nil
            // 在引用计数表中设置若引用标记位
            if (newObj  &&  !newObj->isTaggedPointer()) {
                // 弱引用位初始化操作
                // 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
                newObj->setWeaklyReferenced_nolock();
            }
            // 之前不要设置 location 对象,这里需要更改指针指向
            *location = (id)newObj;
        }
        else {
            // 没有新值,则无需更改
        }
        SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
        return (id)newObj;
    }
    

    SideTable
    SideTable 这个结构体,我给他起名引用计数和弱引用依赖表,因为它主要用于管理对象的引用计数和 weak 表。在 NSObject.mm 中声明其数据结构:

    struct SideTable {
        spinlock_t slock;// 保证原子操作的自旋锁
        RefcountMap refcnts; // 引用计数的 hash 表
        weak_table_t weak_table;// weak 引用全局 hash 表
    }
    对于 slock 和 refcnts 两个成员不用多说,第一个是为了防止竞争选择的自旋锁,第二个是协助对象的 isa 指针的 extra_rc 共同引用计数的变量(对于对象结果,在今后的文中提到)。这里主要看 weak 全局 hash 表的结构与作用。
    

    weak表
    weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了某个对象相关的的所有的弱引用信息。其定义如下(具体定义在objc-weak.h中):

    struct weak_table_t {
        // 保存了所有指向指定对象的 weak 指针
        weak_entry_t *weak_entries;
        // 存储空间
        size_t    num_entries;
        // 参与判断引用计数辅助量
        uintptr_t mask;
        // hash key 最大偏移值
        uintptr_t max_hash_displacement;
    };
    
    这是一个全局弱引用hash表。使用不定类型对象的地址作为 key ,用 weak_entry_t 类型结构体对象作为 value 。其中的 weak_entries 成员,从字面意思上看,即为弱引用表入口。其实现也是这样的。
    
    其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下:
    
    typedef objc_object ** weak_referrer_t;
    struct weak_entry_t {
        DisguisedPtrobjc_object> referent;
        union {
            struct {
                weak_referrer_t *referrers;
                uintptr_t        out_of_line : 1;
                uintptr_t        num_refs : PTR_MINUS_1;
                uintptr_t        mask;
                uintptr_t        max_hash_displacement;
            };
            struct {
                // out_of_line=0 is LSB of one of these (don't care which)
                weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
            };
        }
    }
    
    在 weak_entry_t 的结构中,DisguisedPtr referent 是对泛型对象的指针做了一个封装,通过这个泛型类来解决内存泄漏的问题。从注释中写 out_of_line 成员为最低有效位,当其为0的时候, weak_referrer_t 成员将扩展为多行静态 hash table。其实其中的 weak_referrer_t 是二维 objc_object 的别名,通过一个二维指针地址偏移,用下标作为 hash 的 key,做成了一个弱引用散列。
    

    3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

    void *objc_destructInstance(id obj)  {
        if (obj) {
            Class isa = obj->getIsa();
            if (isa->hasCxxDtor()) {
                object_cxxDestruct(obj);//清除成员变量
            }
            if (isa->instancesHaveAssociatedObjects()) {
                _object_remove_assocations(obj);//删除关联对象
            }
            objc_clear_deallocating(obj);//将指向当前的弱指针置为nil
        }
        return obj;
    }
    
    inline void 
    objc_object::clearDeallocating()
    {
        if (slowpath(!isa.nonpointer)) {
            // Slow path for raw pointer isa.
            sidetable_clearDeallocating();
        }
        else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
            clearDeallocating_slow();
        }
        assert(!sidetable_present());
    }
    
    NEVER_INLINE void
    objc_object::clearDeallocating_slow()
    {
        assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    
        SideTable& table = SideTables()[this];
        table.lock();
        if (isa.weakly_referenced) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
        }
        table.unlock();
    }
    
    void 
    weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
    
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == nil) {
            /// XXX shouldn't happen, but does with mismatched CF/objc
            //printf("XXX no entry for clear deallocating %p\n", referent);
            return;
        }
    
        if (entry->out_of_line()) {
            referrers = entry->referrers;
            count = TABLE_SIZE(entry);
        } 
        else {
            referrers = entry->inline_referrers;
            count = WEAK_INLINE_COUNT;
        }
        
        for (size_t i = 0; i < count; ++i) {
            objc_object **referrer = referrers[i];
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable at %p holds %p instead of %p. "
                                 "This is probably incorrect use of "
                                 "objc_storeWeak() and objc_loadWeak(). "
                                 "Break on objc_weak_error to debug.\n", 
                                 referrer, (void*)*referrer, (void*)referent);
                    objc_weak_error();
                }
            }
        }
        weak_entry_remove(weak_table, entry);
    }
    
    //从引用计数表中删除废弃对象的地址为键值的记录
    static weak_entry_t *
    weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
    {
        assert(referent);
    
        weak_entry_t *weak_entries = weak_table->weak_entries;
    
        if (!weak_entries) return nil;
    
        size_t begin = hash_pointer(referent) & weak_table->mask;
        size_t index = begin;
        size_t hash_displacement = 0;
        while (weak_table->weak_entries[index].referent != referent) {
            index = (index+1) & weak_table->mask;
            if (index == begin) bad_weak_table(weak_table->weak_entries);
            hash_displacement++;
            if (hash_displacement > weak_table->max_hash_displacement) {
                return nil;
            }
        }
        
        return &weak_table->weak_entries[index];
    }
    
    

    autorelease对象在什么时机会被调用release
    在所处的runloop的休眠之前释放

    自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
    调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
    每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
    所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

    源码分析
    class AutoreleasePoolPage 
    {
        magic_t const magic;
        id *next;
        pthread_t const thread;
        AutoreleasePoolPage * const parent;
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
    }
    void *
    objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
    void
    objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    
    调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
    
    调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
    
    id *next指向了下一个能存放autorelease对象地址的区域  
    

    Runloop和Autorelease

    iOS在主线程的Runloop中注册了2个Observer
    第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
    第2个Observer
    监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
    监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

    相关文章

      网友评论

        本文标题:iOS底层原理-内存管理

        本文链接:https://www.haomeiwen.com/subject/xnwqvctx.html