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(@"\n str:%p \n str1:%p \n 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\n", 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.\n", 
                             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)、静态库瘦身、编译器优化。

附录:学习资料

 

posted @ 2019-06-12 14:13  LeeGof  阅读(...)  评论(...编辑  收藏