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
3.1.3Infer
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)、静态库瘦身、编译器优化。
附录:学习资料