zoukankan      html  css  js  c++  java
  • iOS性能优化

    0.前言

    关于性能优化的官方文档介绍,可以在https://developer.apple.com/library/archive/navigation/ 中搜索 "Performance Overview"。

    1.内存管理

    1.1内存5大区

    栈区

    特点:由编译器自动完成分配和释放,不需要程序员手动管理。主要存储了函数的参数和局部变量值等。

    int i = 5;  //4个字节
    int j = 10;  //4个字节
    NSObject *obj = [[NSObject alloc] init];  //8个字节
    int k = 15;  //4个字节
    NSLog(@"%p", &i);  //0x7ffee5a99edc
    NSLog(@"%p", &j);  //0x7ffee5a99ed8
    NSLog(@"%p", &obj);  //0x7ffee5a99ed0  &是取地址的
    NSLog(@"%p", &k);  //0x7ffee5a99ecc

    从上面的代码可以看出:

    • 栈区地址分配是从高到低的;
    • 栈区地址分配是连续的。

    堆区

    特点:需要程序员手动开辟和管理内存(OC中使用ARC,OC对象通常是不需要程序员考虑释放问题)。例如OC中通过new、alloc方法创建的对象;C中通过malloc创建的对象等。

    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"%p", &obj);  //0x7ffee261eed8
    NSObject *obj1 = [[NSObject alloc] init];
    NSObject *obj2 = [[NSObject alloc] init];
    NSObject *obj3 = [[NSObject alloc] init];
    NSLog(@"%p", obj1);  //0x600002c480e0
    NSLog(@"%p", obj2);  //0x600002c480f0
    NSLog(@"%p", obj3);  //0x600002c48100

    从上面代码可以看出:

    • 堆区的地址比栈区要低;
    • 堆区地址分配是不连续的,无规则。

    BSS段(全局区、静态区)

    特点:程序运行过程内存的数据一直存在,程序结束后由系统释放。例如未初始化的全局变量和静态变量等。

    常量区(数据段)

    特点:专门用于存储常量,程序结束后由系统释放。例如已初始化的全局变量、静态变量、常量等。

    关于BSS端和常量区,看一下这段代码:

    // BSS段
    int g1;
    static int s1;
    
    // 数据段
    int g2 = 0;
    static int s2 = 0;
    
    int main(int argc, char * argv[]) {
        // BSS段
        NSLog(@"%p", &g1);  //0x108d2ae4c
        NSLog(@"%p", &s1);  //0x108d2ae50
            
        // 数据段
        NSLog(@"%p", &g2);  //0x108d2ae48
        NSLog(@"%p", &s2);  //0x108d2ae54
    }

    从结果可以看到,这两个区域并没有很明显的界限。

    程序代码区

    特点:用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区。也就是程序代码(被编译成二进制的代码)。

    总结

    • 栈区和堆区是运行时分配的内存,其他区是编译时分配的;
    • 栈区的地址是连续的,并且是由高到低分配的;
    • 堆区的地址是不连续的,堆区的访问速度没有栈区快。

    对象的存储示例:

    1.2引用计数

    1.2.1内存管理方案

    引用计数是怎么存储的?

    • 如果对象使用了TaggedPointer,苹果会直接将其指针值作为引用计数返回;
    • 引用计数可以直接存储在优化过的isa指针中;
    • 如果isa指针存储不下,引用计数就会把一部分存储在一个散列表中。

    TaggedPointer

    • TaggedPointer专门用来存储小的对象,例如NSNumber和NSDate;
    • TaggedPointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量。所以,它的内存并不存储在堆中,也不需要malloc和free;
    • 在内存读取速度快。

    下面我们看一个示例:

    /* 打印结果
    0xea81e6603491df06
    0xea81e6603491df16
    0xea81e6603491df26
    0xea81e6603491df36
    0xea81e6603491df46
    0xea81e6603491df56
    0xea81e6603491df66
    0xea81e6603491df76
    0xea81e6603491df86
    0xea81e6603491df96
    */
    for (int i = 0; i < 10; i++) {
        //Tag + Data
        NSNumber *num = @(i);
        NSLog(@"%p", num);
    }

    从上面可以看到,小对象的存储和普通对象的存储是不同的,它的前后是Tag位,中间是值,采取的是Tag + Data的存储方式。

    这里,我们对上面的代码做一个小改动,再看一下打印结果:

    /*
    0x94a757483dbe2e96
    0x9458a8b7c241d166
    0x9558a8b7c241d176
    0x9658a8b7c241d146
    0x9758a8b7c241d156
    0x9058a8b7c241d126
    0x9158a8b7c241d136
    0x9258a8b7c241d106
    0x9358a8b7c241d116
    0x600001641ec0
    */
    for (int i = 0; i < 10; i++) {
        NSNumber *num = @(i * 0xFFFFFFFFFFFFF);
        NSLog(@"%p", num);
    }

    可以明显的看到,最后一个大对象是存储在堆里面的。

    接下来我们看一下NSString的存储处理:

    /*
     str:0x10d94d290 常量区
     str1:0x95ac12668c8ee515 栈地址
     str2:0x60000206ca40 堆地址
    */
    NSString *str = @"Gof";  //
    NSString *str1 = [NSString stringWithFormat:@"Gof"];  
    NSString *str2 = [NSString stringWithFormat:@"Communication occurs between NSPort objects"];
    NSLog(@"
     str:%p 
     str1:%p 
     str2:%p", str, str1, str2);

    isa_t

    union isa_t {  //objc-private.h 61行
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    #if defined(ISA_BITFIELD)
        struct {
            ISA_BITFIELD;  // defined in isa.h
        };
    #endif
    };

    其中ISA_BITFIELD的定义如下:

    #   define ISA_BITFIELD                                                      
          uintptr_t nonpointer        : 1;                                       
          uintptr_t has_assoc         : 1;                                       
          uintptr_t has_cxx_dtor      : 1;                                       
          uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ 
          uintptr_t magic             : 6;                                       
          uintptr_t weakly_referenced : 1;                                       
          uintptr_t deallocating      : 1;                                       
          uintptr_t has_sidetable_rc  : 1;                                       
          uintptr_t extra_rc          : 19

    对于上面的各位,解释如下:

    • nonpointer:0表示普通的isa指针;1表示使用优化,存储引用计数;
    • has_assoc:表示该对象是否包含associated object,如果没有,则析构时会更快;
    • has_cxx_dtor:表示该对象是否有 C++ 或 ARC的析构函数,如果没有,则析构时会更快;
    • shiftcls:类的指针;
    • magic:固定值为0xd2,用于在调试时分辨对象是否未完成初始化;
    • weakly_referenced:表示该对象是否有过weak对象,如果没有,则析构时会更快;
    • deallocating:表示该对象是否正在析构;
    • has_sidetable_rc:表示该对象的引用计数值是否过大无法存储在 isa 指针;
    • extra_rc:存储引用计数值减一后的结果。

    1.2.2相关方法实现原理

    retainCount

    - (NSUInteger)retainCount {  //NSObject.mm 2291行
        return ((id)self)->rootRetainCount();
    }
    
    inline uintptr_t 
    objc_object::rootRetainCount()  //objc-object.h 713行
    {
        if (isTaggedPointer()) return (uintptr_t)this;
    
        sidetable_lock();
        isa_t bits = LoadExclusive(&isa.bits);
        ClearExclusive(&isa.bits);
        if (bits.nonpointer) {  //如果是优化过的isa指针
            uintptr_t rc = 1 + bits.extra_rc;  //读取 extra_rc中的引用计数值并加1
            if (bits.has_sidetable_rc) {  //如果散列表中存有引用计数
                rc += sidetable_getExtraRC_nolock();
            }
            sidetable_unlock();
            return rc;
        }
    
        sidetable_unlock();
        return sidetable_retainCount();
    }
    
    objc_object::sidetable_getExtraRC_nolock()  //NSObject.mm 1395行
    {
        assert(isa.nonpointer);
        SideTable& table = SideTables()[this];
        RefcountMap::iterator it = table.refcnts.find(this);
        if (it == table.refcnts.end()) return 0;
        //first表示key, second表示value 右移两位(一位是弱引用标识,一位是是否析构标识)
        else return it->second >> SIDE_TABLE_RC_SHIFT;
    }

    retain

    - (id)retain {  //NSObject.mm 2232行
        return ((id)self)->rootRetain();
    }
    
    objc_object::rootRetain()  //objc-object.h 459行
    {
        return rootRetain(false, false);
    }
    
    objc_object::rootRetain(bool tryRetain, bool handleOverflow)  //objc-object.h 471行
    {
        if (isTaggedPointer()) return (id)this;
    
        bool sideTableLocked = false;
        bool transcribeToSideTable = false;
    
        isa_t oldisa;
        isa_t newisa;
    
        do {
            transcribeToSideTable = false;
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            //slowpath表示不执行的概率大
            if (slowpath(!newisa.nonpointer)) {
                ClearExclusive(&isa.bits);
                if (!tryRetain && sideTableLocked) sidetable_unlock();
                if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
                else return sidetable_retain();
            }
            // don't check newisa.fast_rr; we already called any RR overrides
            if (slowpath(tryRetain && newisa.deallocating)) {
                ClearExclusive(&isa.bits);
                if (!tryRetain && sideTableLocked) sidetable_unlock();
                return nil;
            }
            uintptr_t carry;
            newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
            //溢出时,存储一半引用计数到散列表中
            if (slowpath(carry)) {
                // newisa.extra_rc++ overflowed
                if (!handleOverflow) {
                    ClearExclusive(&isa.bits);
                    return rootRetain_overflow(tryRetain);
                }
                // Leave half of the retain counts inline and 
                // prepare to copy the other half to the side table.
                if (!tryRetain && !sideTableLocked) sidetable_lock();
                sideTableLocked = true;
                transcribeToSideTable = true;
                newisa.extra_rc = RC_HALF;  //存一半在extra_rc
                newisa.has_sidetable_rc = true;  //设置引用计数值过大无法全部存储在isa的标识
            }
        } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }
    
        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
        return (id)this;
    }

    release

    - (oneway void)release {  //NSObject.mm 2274行
        ((id)self)->rootRelease();
    }
    
    objc_object::rootRelease()  //objc-object.h 553行
    {
        return rootRelease(true, false);
    }
    
    objc_object::rootRelease(bool performDealloc, bool handleUnderflow)  //objc-object.h 565行
    {
        if (isTaggedPointer()) return false;
    
        bool sideTableLocked = false;
    
        isa_t oldisa;
        isa_t newisa;
    
     retry:
        do {
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            if (slowpath(!newisa.nonpointer)) {
                ClearExclusive(&isa.bits);
                if (sideTableLocked) sidetable_unlock();
                return sidetable_release(performDealloc);
            }
            // don't check newisa.fast_rr; we already called any RR overrides
            uintptr_t carry;
            newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
            //下溢出
            if (slowpath(carry)) {
                // don't ClearExclusive()
                goto underflow;
            }
        } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                                 oldisa.bits, newisa.bits)));
    
        if (slowpath(sideTableLocked)) sidetable_unlock();
        return false;
    
     underflow:
        // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    
        // abandon newisa to undo the decrement
        newisa = oldisa;
        //散列表中有值
        if (slowpath(newisa.has_sidetable_rc)) {
            if (!handleUnderflow) {
                ClearExclusive(&isa.bits);
                return rootRelease_underflow(performDealloc);
            }
    
            // Transfer retain count from side table to inline storage.
    
            if (!sideTableLocked) {
                ClearExclusive(&isa.bits);
                sidetable_lock();
                sideTableLocked = true;
                // Need to start over to avoid a race against 
                // the nonpointer -> raw pointer transition.
                goto retry;
            }
    
            // Try to remove some retain counts from the side table.        
            size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);  //从散列表中溢出引用计数
    
            // To avoid races, has_sidetable_rc must remain set 
            // even if the side table count is now zero.
            //重新赋值给isa指针
            if (borrowed > 0) {
                // Side table retain count decreased.
                // Try to add them to the inline count.
                newisa.extra_rc = borrowed - 1;  // redo the original decrement too
                bool stored = StoreReleaseExclusive(&isa.bits, 
                                                    oldisa.bits, newisa.bits);
                if (!stored) {
                    // Inline update failed. 
                    // Try it again right now. This prevents livelock on LL/SC 
                    // architectures where the side table access itself may have 
                    // dropped the reservation.
                    isa_t oldisa2 = LoadExclusive(&isa.bits);
                    isa_t newisa2 = oldisa2;
                    if (newisa2.nonpointer) {
                        uintptr_t overflow;
                        newisa2.bits = 
                            addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                        if (!overflow) {
                            stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                           newisa2.bits);
                        }
                    }
                }
    
                if (!stored) {
                    // Inline update failed.
                    // Put the retains back in the side table.
                    sidetable_addExtraRC_nolock(borrowed);
                    goto retry;
                }
    
                // Decrement successful after borrowing from side table.
                // This decrement cannot be the deallocating decrement - the side 
                // table lock and has_sidetable_rc bit ensure that if everyone 
                // else tried to -release while we worked, the last one would block.
                sidetable_unlock();
                return false;
            }
            else {
                // Side table is empty after all. Fall-through to the dealloc path.
            }
        }
    
        // Really deallocate.
        //执行析构
        if (slowpath(newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return overrelease_error();
            // does not actually return
        }
        newisa.deallocating = true;
        if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
    
        if (slowpath(sideTableLocked)) sidetable_unlock();
    
        __sync_synchronize();
        if (performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return true;
    }

    weak

    首先我们看一下weak的实现原理:

    • weak底层也是使用的哈希存储,对象的内存地址作为key,指向该对象的所有弱引用的指针作为值;
    • 释放时,就是以对象的内存地址作为key,去存储弱引用对象的哈希表里,找到所有的弱引用对象,然后设置为nil,最后移除这个弱引用的散列表。

    对象在调用dealloc方法(NSObject.mm 2319行)时,最终会到如下代码中,来处理所有的weak对象:

    void 
    weak_clear_no_lock(weak_table_t *weak_table, id referent_id)   //objc-weak.mm 462行
    {
        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
    ", referent);
            return;
        }
    
        // zero out references
        weak_referrer_t *referrers;
        size_t count;
        
        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;  //置为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.
    ", 
                                 referrer, (void*)*referrer, (void*)referent);
                    objc_weak_error();
                }
            }
        }
        //从弱引用表中移除
        weak_entry_remove(weak_table, entry);
    }

    1.3自动释放池 

    我们先看一下下面简单的代码,经过Clang之后的效果:

    int main(int argc, char * argv[]) {
        @autoreleasepool {
          
        }
        return 0;
    }

    使用下面指令编译:

    clang -rewrite-objc main.m

    会生成一个main.app,结果如下:

    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;   //声明时会调用结构体里的构造函数,离开作用域时会调用析构函数
    
        }
        return 0;
    }

    上面的 __AtAutoreleasePool 是一个结构体:

    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };

    总结一下:

    • 自动释放池的主要结构体和类是:__AtAutoreleasePool 和 AutoreleasePoolPage;
    • 调用了 autorelease 的对象最终都是通过 AutoreleasePoolPage 对象来管理的;
    • AutoreleasePoolPage 的大小是4096个字节;
    • 自动释放池是 AutoreleasePoolPage,它是以双向链表的形式连接起来的。

    我们先看下面这段代码:

    extern void _objc_autoreleasePoolPrint(void);  //通过这个函数来查看自动释放池的情况
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            //自动释放池大小为4096个字节,自身具有56个字节,每个对象占8个字节,因此自动释放池能放 (4096 - 56) / 8 = 505个对象,减去1个哨兵对象,还能放504个对象
            //for循环分别设置为503、504、505,可以看到结果是这样的:
            /*
             503:
             objc[14037]: AUTORELEASE POOLS for thread 0x1000a95c0
             objc[14037]: 504 releases pending.
             objc[14037]: [0x102806000]  ................  PAGE  (hot) (cold)
             objc[14037]: [0x102806038]  ################  POOL 0x102806038
             objc[14037]: [0x102806040]       0x100771b00  NSObject
             ...
             objc[14037]: ##############
             
             504:
             objc[14059]: AUTORELEASE POOLS for thread 0x1000a95c0
             objc[14059]: 505 releases pending.
             objc[14059]: [0x103802000]  ................  PAGE (full) (hot) (cold)
             objc[14059]: [0x103802038]  ################  POOL 0x103802038
             objc[14059]: [0x103802040]       0x1030818e0  NSObject
             ...
             objc[14037]: ##############
             
             505:
             objc[14075]: AUTORELEASE POOLS for thread 0x1000a95c0
             objc[14075]: 506 releases pending.
             objc[14075]: [0x102003000]  ................  PAGE (full)  (cold)
             objc[14075]: [0x102003038]  ################  POOL 0x102003038
             objc[14075]: [0x102003040]       0x10065d730  NSObject
             ...
             objc[14075]: [0x102007000]  ................  PAGE  (hot)
             objc[14075]: [0x102007038]       0x100659ba0  NSObject
             objc[14075]: ##############
             */
            for (int i = 0; i < 505; i++) {
                NSObject *obj = [[[NSObject alloc] init] autorelease];
            }
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }

    如果是嵌套的自动释放池,会怎么样呢?

    extern void _objc_autoreleasePoolPrint(void);  //通过这个函数来查看自动释放池的情况
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            /*
             打印结果:
             objc[14155]: AUTORELEASE POOLS for thread 0x1000a95c0
             objc[14155]: 507 releases pending.  (有两个哨兵对象)
             objc[14155]: [0x103803000]  ................  PAGE (full)  (cold) (第一页)
             objc[14155]: [0x103803038]  ################  POOL 0x103803038 (哨兵对象)
             objc[14155]: [0x103803040]       0x1031205e0  NSObject
             ....
             objc[14155]: [0x103803ff8]  ################  POOL 0x103803ff8 (哨兵对象)
             objc[14155]: [0x103806000]  ................  PAGE  (hot)  (第二页)
             objc[14155]: [0x103806038]       0x10311cc00  NSObject
             objc[14155]: [0x103806040]       0x10311cc10  NSObject
             objc[14155]: ##############
             */
            for (int i = 0; i < 503; i++) {
                NSObject *obj = [[[NSObject alloc] init] autorelease];
            }
            @autoreleasepool {
                for (int i = 0; i < 2; i++) {
                    NSObject *obj = [[[NSObject alloc] init] autorelease];
                }
                _objc_autoreleasePoolPrint();
            }
        }
        return 0;
    }

    从上面的打印结果可以看到,嵌套的自动释放池,只是多添加了一个哨兵对象。

    objc_autoreleasePoolPush(入栈)

    void *
    objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
        static inline void *push() 
        {
            id *dest;
            if (DebugPoolAllocation) {  //Debug状态
                // Each autorelease pool starts on a new pool page.
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {
                dest = autoreleaseFast(POOL_BOUNDARY);
            }
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    
        id *autoreleaseNewPage(id obj)
        {
            AutoreleasePoolPage *page = hotPage();  //获取当前操作的page
            if (page) return autoreleaseFullPage(obj, page);
            else return autoreleaseNoPage(obj);
        }
    
        id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
        {
            // The hot page is full. 
            // Step to the next non-full page, adding a new page if necessary.
            // Then add the object to that page.
            assert(page == hotPage());
            assert(page->full()  ||  DebugPoolAllocation);  //如果是满的或者Debug状态
    
            do {
                if (page->child) page = page->child;  //获取下一个page
                else page = new AutoreleasePoolPage(page);  //创建一个新的page
            } while (page->full());
    
            setHotPage(page);  //设置成活跃page
            return page->add(obj);  //添加对象到自动释放池
        }
    
        static inline id *autoreleaseFast(id obj)
        {
            AutoreleasePoolPage *page = hotPage();  //获取当前活跃的page
            if (page && !page->full()) {  //page存在且没有满,直接添加对象
                return page->add(obj);
            } else if (page) {
                return autoreleaseFullPage(obj, page);  //如果满了,就新创建一个自动释放池
            } else {
                return autoreleaseNoPage(obj);
            }
        }

    _objc_autoreleasePoolPop(出栈)

    _objc_autoreleasePoolPop(void *ctxt)
    {
        objc_autoreleasePoolPop(ctxt);
    }
    
    void
    objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    
        static inline void pop(void *token) 
        {
            AutoreleasePoolPage *page;
            id *stop;
    
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
                // Popping the top-level placeholder pool.
                if (hotPage()) {
                    // Pool was used. Pop its contents normally.
                    // Pool pages remain allocated for re-use as usual.
                    pop(coldPage()->begin());
                } else {
                    // Pool was never used. Clear the placeholder.
                    setHotPage(nil);
                }
                return;
            }
    
            page = pageForPointer(token);  //获取当前哨兵所在的页
            stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    // Start of coldest page may correctly not be POOL_BOUNDARY:
                    // 1. top-level pool is popped, leaving the cold page in place
                    // 2. an object is autoreleased with no pool
                } else {
                    // Error. For bincompat purposes this is not 
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
    
            if (PrintPoolHiwat) printHiwat();
    
            page->releaseUntil(stop);  //不断的释放自动释放池里的
    
            // memory: delete empty children
            if (DebugPoolAllocation  &&  page->empty()) {
                // special case: delete everything during page-per-pool debugging
                AutoreleasePoolPage *parent = page->parent;
                page->kill();
                setHotPage(parent);
            } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
                // special case: delete everything for pop(top) 
                // when debugging missing autorelease pools
                page->kill();
                setHotPage(nil);
            } 
            else if (page->child) {
                // hysteresis: keep one empty child if page is more than half full
                if (page->lessThanHalfFull()) {
                    page->child->kill();
                }
                else if (page->child->child) {
                    page->child->child->kill();
                }
            }
        }

    从上面的源码可以看到:

    • 当对象调用autorelease方法时,会将延迟释放的对象添加到AutoreleasePoolPage中;
    • 调用pop方法时,会向栈中的对象发送release消息。

    2.引起内存泄漏的原因

    2.1循环引用

     先看一份代码:

    self.block = ^{
        NSLog(@"%@", self.str);
    };

    很显然,上面的代码会产生循环引用,那么怎么解决呢?

    __weak __typeof(self)weakSelf = self;
    self.block = ^{
        NSLog(@"%@", weakSelf.str);
    };

    如果在block中执行一个耗时任务,这时VC退出了,会导致weakSelf为空,这个时候我们一般使用下面的方式:

    __weak __typeof(self)weakSelf = self;
    self.block = ^{
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongSelf.str);
        });
    };
    self.block();

    2.2强引用

    先看下面的代码:

    //Runloop -> timer -> self
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];

    上面的代码中,timer强引用self,导致self无法正常释放:

    怎么解决呢?

    方式一:打断 timer -> self的强引用

    //方式一:打断 timer -> self的强引用
    - (void)didMoveToParentViewController:(UIViewController *)parent {
        if (nil == parent) {
            [self.timer invalidate];
            self.timer = nil;
        }
    }

    方式二:通过一个中间对象来弱持有self:

    @interface GofTimer ()
    
    @property (nonatomic, weak) id target;  //!<target
    @property (nonatomic, assign) SEL sel;  //!<selector
    @property (nonatomic, strong) NSTimer *timer;  //!<定时器
    
    @end
    
    @implementation GofTimer
    
    - (id)initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
        if (self = [super init]) {
            self.target = aTarget;
            self.sel = aSelector;
            self.timer = [NSTimer timerWithTimeInterval:ti target:self selector:@selector(fire) userInfo:userInfo repeats:yesOrNo];
            [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
        }
        return self;
    }
    
    - (void)stop {
        [self.timer invalidate];
        self.timer = nil;
    }

    调用的时候:

    self.timer = [[GofTimer alloc] initWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];

    在dealloc方法中,调用一下GofTimer对象的stop方法,就可以释放Runloop对Timer的强持有。

    方式三:和方式二比较类似,添加一个中间对象,self和timer都持有中间对象:Runloop -> timer -> target <- self

    //方式三:Runloop -> timer -> target <- self
    self.target = [[NSObject alloc] init];
    class_addMethod([self.target class], @selector(targetFire), (IMP)targetFire, "v@:");
    self.timer = [NSTimer timerWithTimeInterval:1 target:self.target selector:@selector(targetFire) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

    2.3非OC对象,没有手动释放

    对于非OC对象,手动进行内存的释放操作。示例:

    CFHostRef hostRef = NULL;
    NSArray *addresses;
    hostRef = CFHostCreateWithName(kCFAllocatorDefault, (__bridge CFStringRef)hostname);
    if (hostRef != NULL) {
        result = CFHostStartInfoResolution(hostRef, kCFHostAddresses, NULL);
        if (result == TRUE) {
            addresses = (__bridge NSArray*)CFHostGetAddressing(hostRef, &result);
        }
            
        CFRelease(hostRef);
    }

    2.4小结

    内存泄漏在我们的日常开发中,比较常见。一次内存泄露的危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。因此我们需要对程序中的内存泄漏问题认真对待,多检查自己的代码,避免出现泄漏情况出现。

    参考资料:iOS八种内存泄漏问题

    3.内存问题检测方法

    3.1静态检测方法

    3.1.1Clang Static Analyzer

    用于针对C,C++和Objective-C的程序进行分析,Clang默认的配置主要是空指针检测,类型转换检测,空判断检测,内存泄漏检测这种等问题。如果需要更多的配置,可以使用开源的Clang项目,然后集成到自己的CI上。

    手动检测

    自动检测

    静态检测方法,并不能检测出循环引用的的问题。

    3.1.2OCLint

    一个强大的静态代码分析工具,可以用来提高代码质量,查找潜在的bug,主要针对 C、C++和Objective-C的静态分析。功能非常强大,而且是出自国人之手。OCLint基于 Clang 输出的抽象语法树对代码进行静态分析,支持与现有的CI集成,部署之后基本不需要维护,简单方便。
    参考资料:https://www.jianshu.com/p/87b48da8ab32

    3.1.3Infer

    facebook开源的静态分析工具,Infer可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题。Infer效率高,规模大,几分钟能扫描数千行代码。

    3.1.4小结

    静态内存泄漏分析可以发现大部分问题,但只是静态分析,并不准确。它只是针对有可能发生内存泄漏的地方,一些动态内存分配的情形并没有分析。

    3.2动态监测方法(Instruments或MLeaksFinder等)

    Instruments

    Instruments是采样检测,不一定检测出所有的内存问题,只是一个辅助作用。

    示例代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        GofObject *objA = [[GofObject alloc] init];
        GofObject *objB = [[GofObject alloc] init];
        //相互引用造成内存泄露
        objA.obj = objB;
        objB.obj = objA;
    }

    通过Instruments进行检测,可以看到:

    MLeaksFinder

    具有局限性,仅检测视图和VC。

    MTHawkeye

    iOS 下的调试优化辅助工具集,旨在帮助 iOS 开发者提升开发效率、辅助优化性能体验。

    DoraemonKit

    一款功能齐全的客户端( iOS 、Android )研发助手。

    3.3dealloc

    这个很简单,就是通过在dealloc方法打点的方式,查看是否释放。

    4.优化建议

    4.1启动优化

    可参考:

    总结起来就是:

    • main函数之前:减少动态库,合并动态库,减少OC类、分类的数量,减少selector数量;
    • main函数至启动完成:不要添加耗时任务。

    关于动态库对启动时间的影响,可参看:iOS Dynamic Framework 对App启动时间影响实测

    4.2界面优化

    关于界面的卡顿原因,可参看:iOS应用卡顿分析

    • 耗时操作,尽量不要放在主线程。如果需要放在主线程的话,可以考虑使用Runloop的方式,进行优化;
    • 合理使用CPU和GPU。

    从上面的那篇文章可以看到:

    • CPU:主要用来计算显示内容,包括视图创建、布局计算、图片解码、文本绘制等;
    • GPU:把CPU计算好的数据进行渲染。 

     好用的对卡顿优化的第三方框架:

    4.3能耗优化

    • CPU:高CPU使用量会迅速消耗掉用户的电池电量,我们App做的每件事几乎都需要用CPU。因此使用CPU时,要精打细算,真正有需要时通过分批、定时、有序地执行任务。

    • 网络操作:网络通信时,蜂窝数据和Wi-Fi等元器件开始工作会消耗电能。分批传输、减少传输、压缩数据、恰当地处理错误,这样可以为App节省电能。

    • 图像、动画、视频:App内容每次更新到屏幕上都需要消耗电能处理像素信息,动画和视频格外耗电。不经意的或者不必要的内容更新同样会消耗电能,所以UI不可见时,应该避免更新其内容。

    • 位置:App为了记录用户的活动或者提供基于位置的服务会进行定位。定位精度越高,定位时间越长,消耗电量也就越多。App可以从这几方面来考虑节能:降低定位精度、缩短定位时间、不需要位置信息之后立即停止定位。

    • 动作传感器:长时间用不上加速度计、陀螺仪、磁力计等设备的动作数据时,应该停止更新数据,不然也会浪费电能。应按需获取,用完即停。
    • 蓝牙:蓝牙活动频度太高会消耗电能,应该尽量分批、减少数据轮询等操作。

    4.4网络优化

    • 资源优化:尽可能的缩小传输数据的大小;
    • ProtocolBuffer:可以考虑使用ProtocolBuffer代替Json进行数据传输。Protocolbuffer是由Google推出的一种数据交换格式,它独立于语言,独立于平台,序列号与反序列化也很简单。在实际项目中,当数据变小的时候会显著提高传输速度。

    4.5安装包瘦身

    安装包瘦身可参考:

    总结一下,安装包瘦身主要从以下两方面着手:

    • 资源优化:包括对资源的压缩(图片、音频、视频)、删除无用资源等;
    • 可执行文件瘦身:删除无用代码(AppCode)、静态库瘦身、编译器优化。

    附录:学习资料

  • 相关阅读:
    [AWS] Lab
    [AWS] Lab
    [AWS] Launch the VPC Wizard
    [AWS] EC2 Dashboard
    mysql .net下载
    币乎咕噜DeFi踩雷记
    量化分析师关于有限差分方法(FDM)最好的5本书
    QuantStart量化交易文集
    Exploring Market Making Strategy for High Frequency Trading: An Agent-Based Approach
    什么是信息比率?信息比率与夏普比率的差别?
  • 原文地址:https://www.cnblogs.com/LeeGof/p/10991107.html
Copyright © 2011-2022 走看看