直接看看下面的面試題
- 介紹一下內(nèi)存的幾大區(qū)域
- 使用 CDDisplayLink、NSTimer 有什么注意點
- 講一下對 iOS 內(nèi)存管理的理解
- autorelease 什么時候釋放
- 方法里有局部變量,出了方法后會立即釋放嗎? 表現(xiàn)上是的
- ARC 都幫我們做了什么
- weak 指針的實現(xiàn)原理
CDDisplayLink、NSTimer 使用注意與處理
CDDisplayLink、NSTimer 會對 target 產(chǎn)生強引用,如果target 對它再產(chǎn)生強引用,那就會發(fā)生循環(huán)引用。
處理這個循環(huán)引用問題:
- 針對 NSTimer 可以使用 initWithBlock 的方式,直接在block 內(nèi)做事情。
- CDDisplayLink 沒有block 方法,可以從消息轉(zhuǎn)發(fā)的機制入手,設(shè)置一個代理 NSProxy 來轉(zhuǎn)發(fā)消息,斷開強引用
--------- VC --------------
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MyProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
--------- Proxy ------------
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy對象不需要調(diào)用init,因為它本來就沒有init方法
MyProxy *proxy = [MyProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
注意
應(yīng)該直接使用 NSProxy 的子類做代理對象。其本身是基類,內(nèi)部對于方法的處理,直接走的是消息轉(zhuǎn)發(fā)階段,不會在父類找方法實現(xiàn)和調(diào)用。
如果繼承 NSObject 子類做代理,會執(zhí)行方法查找和轉(zhuǎn)發(fā)的完整步驟,效率會低
GCD Timer
NSTimer 依賴于 Runloop,如果Runloop 的任務(wù)比較繁重,可能會導(dǎo)致 NSTimer 不準時
GCD 的 Timer 會更加準時,它是一種專門監(jiān)聽系統(tǒng)時間定時器
原理:
GCD 框架提供了一系列的接口來監(jiān)聽底層系統(tǒng)對象的活動
當監(jiān)聽被觸發(fā),會自動將 block 提交到指定的消息隊列來處理回調(diào)
系統(tǒng)底層對象包括:file descriptors, Mach ports, signals, VFS nodes, etc
基本使用如下: 完整代碼地址 GCD_Timer
+ (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();
// 創(chuàng)建定時器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設(shè)置時間
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_);
// 設(shè)置回調(diào)
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重復(fù)的任務(wù)
[self cancelTask:name];
}
});
// 啟動定時器
dispatch_resume(timer);
return name;
}
iOS 程序的內(nèi)存布局
程序裝載到內(nèi)存中之后,內(nèi)存分布如下。

Tagged Pointer
從 64 bit 開始, iOS 引入了 Tagged Pointer 技術(shù),用于優(yōu)化 NSNumber、NSDate、NSString 等小對象的存儲。
簡單來說:
- Tagged Pointer 是直接保存值的指針,非對象,無 isa 指針,不指向堆空間地址,無需對其進行內(nèi)存管理
- 對象值小于8個字節(jié)可以表示,則將其指針拆分為兩部分,一個部分標記,一部分存值。它是一個含值的指針,并非真正的對象
- 當對象值 8 個字節(jié)無法保存,則會創(chuàng)建真正的對象來保存該值
看以下代碼,存在什么問題,如何解決。
@property (nonatomic, strong) NSString *string;
dispatch_queue_t queue = dispatch_queue_create("memoryBeingFreedCase", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000; i++) {
dispatch_async(queue, ^{
self.string = [NSString stringWithFormat:@"The num is %d", i];
// self.string = [NSString stringWithFormat:@"%d", i];
});
}
出現(xiàn)問題: 代碼會崩潰。
因為: strong 類型的屬性,在set方法內(nèi)部會 release oldValue, 然后 retain newValue. 因為是多線程并發(fā),可能會對 oldValue 進行多次 release 造成壞內(nèi)存訪問,崩潰。
解決方法:
- 使用線程同步技術(shù),將并發(fā)任務(wù)改成串行
- 將屬性改為 atomic 屬性
- 改用 Tagged Point 技術(shù) 【上文中的注釋即可】
Copy 和 mutableCopy
- copy: 不可變拷貝,產(chǎn)生不可變副本
- mutableCopy: 可變拷貝,產(chǎn)生可變副本
拷貝分為深拷貝(內(nèi)容拷貝,產(chǎn)生新內(nèi)容) 和淺拷貝(指針拷貝)。
不可變對象 copy -> 指針拷貝 -> 結(jié)果為兩個指針指向同一個不可變對象
不可變對象 mutableCopy -> 內(nèi)容拷貝 -> 結(jié)果為兩個指針,一個可變對象,一個不可變對象
可變對象 copy -> 深拷貝 -> 結(jié)果兩個指針,一個可變對象,一個copy出來的不可變對象
可變對象 mutableCopy -> 深拷貝 -> 結(jié)果是兩個指針,兩個可變對象
總結(jié):

copy 關(guān)鍵字
copy 關(guān)鍵字在設(shè)置屬性的時指定了其變量內(nèi)存管理的方式為 copy。
下面代碼會發(fā)生什么,為什么?
// .h 文件
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m 文件
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
// 會崩潰,-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
// 因為 屬性指定 copy 修飾符,直接在 setter 賦值的時候?qū)?[mutableArray copy] 成了不可變數(shù)組,雖然它的聲明依舊是 NSMutableArray
自己的類實現(xiàn) copy 關(guān)鍵字
若想令自己所寫的對象具有拷貝功能,則需實現(xiàn) NSCopying 協(xié)議。如果自定義的對象分為可變版本與不可變版本,那么就要同時實現(xiàn) NSCopying 與 NSMutableCopying 協(xié)議。
1.需聲明該類遵從 NSCopying 協(xié)議
2.實現(xiàn) NSCopying 協(xié)議。該協(xié)議只有一個方法:- (id)copyWithZone:(NSZone *)zone;
- (id)copyWithZone:(NSZone *)zone {
User *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
return copy;
}
如果是對象內(nèi)部有 array 這樣的屬性,需要考慮 array 內(nèi)部的拷貝賦值問題。
MRC 與 ARC
引用計數(shù)的存儲
在 iOS 中,使用引用計數(shù)來管理OC對象內(nèi)存,一個新創(chuàng)建的 OC 對象引用計數(shù)默認為 1,當引用計數(shù)減為 0 ,OC 對象就會被銷毀,釋放其占用的內(nèi)存空間。
在 MRC 中需要手動管理內(nèi)存,調(diào)用 retain 會讓 OC 對象引用計數(shù) +1,調(diào)用 release 會讓 OC 對象的引用計數(shù) -1
內(nèi)存管理的經(jīng)驗總結(jié)
- 當調(diào)用 alloc/new/copy/mutableCopy 方法返回了一個對象,在不需要這個對象的時候,需要調(diào)用 release/autorelease 來釋放它
- 想擁有某個對象,就讓它的引用計數(shù) +1,不再想擁有某個對象就讓它的計數(shù) -1
引用計數(shù)的保存在 isa 中,在 64 位機器上經(jīng)過優(yōu)化,如下。
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
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; // 當19位不夠保存,則在 sideTable 中保存。
uintptr_t extra_rc : 19; // 高19位為保存引用計數(shù)
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
#endif
[圖片上傳失敗...(image-314824-1595316835729)]
weak 實現(xiàn)原理
這個可以從 weak 的作用入手。weak 修飾的對象,被銷毀后其指針被賦值為 nil。所以可以從 runtime 源碼看看 dealloc 方法的實現(xiàn)
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // Tagged Pointer 直接結(jié)束
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{ // 優(yōu)化指針,弱引用,無關(guān)聯(lián)對象,無c++析構(gòu)函數(shù),無sidetable保存引用計數(shù)
// 直接釋放當前對象
free(this);
}
else {
// 進入內(nèi)部釋放
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
objc_destructInstance(obj);
free(obj);
return nil;
}
// dealloc方法的核心實現(xiàn),內(nèi)部會做判斷和析構(gòu)操作
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
// 判斷是否有OC或C++的析構(gòu)函數(shù)
bool cxx = obj->hasCxxDtor();
// 對象是否有相關(guān)聯(lián)的引用
bool assoc = obj->hasAssociatedObjects();
// This order is important.
// 對當前對象進行析構(gòu)
if (cxx) object_cxxDestruct(obj);
// 移除所有對象的關(guān)聯(lián),例如把weak指針置nil
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
ARC 幫我們做了什么?
ARC是 LLVM + runtime 協(xié)作的結(jié)果,LLVM 編譯器會自動幫我們生成 release/retain 等相關(guān)內(nèi)存管理代碼。runtime 在程序運行期間檢測對象引用計數(shù),釋放弱引用等,幫我們管理內(nèi)存。
autorelease 原理
autorelease 基于自動釋放池 AutoreleasePool
autoreleasePool 的主要底層數(shù)據(jù)結(jié)構(gòu)是: __AtAutoreleasePool(編譯器層)、AutoreleasePoolPage(runtime源碼)
調(diào)用了 autorelease 的對象最終都是通過 AutoreleasePoolPage 對象來管理的
通過 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 重寫@autoreleasepool 可以看到其被重寫為
// 自動釋放池變量C++實現(xiàn)
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 聲明釋放池變量
}
return 0;
}
// 實際自動釋放池的大括號執(zhí)行處被改寫為了
__AtAutoreleasePool()
// 用戶代碼
~__AtAutoreleasePool()
AutoreleasePoolPage 內(nèi)部結(jié)構(gòu)如下:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
AutoreleasePoolPage 實現(xiàn)邏輯
- 釋放池內(nèi)部結(jié)構(gòu)是棧,它依據(jù)線程存在
- 池(棧)中指針是自動釋放obj指針,或者 POOL_BOUNDARY 指針
- 池釋放時,以 POOL_BOUNDARY 為界,先釋放內(nèi)部的 obj
- 所有的pool,被組織成雙向鏈表,根據(jù)需要添加/移除 pages
5.線程本地存儲永遠指向 hotPage: 即新存儲自動釋放obj 的page
調(diào)用 push 方法會將一個 POOL_BOUNDARY 入棧,并返回其存放的內(nèi)存地址
調(diào)用 pop 方法時傳入一個 POOL_BOUNDARY 內(nèi)存地址,會從最后一個入棧的對象開始發(fā)送 release 消息,直到遇到這個 POOL_BOUNDARY
id *next 指向下一個能存放 Autorelease obj 的地址
具體看一下 runtime/NSObject.mm 源碼中。
對象調(diào)用 autorelease 知道后的釋放時機是什么?
從源碼可以看 [obj autorelease] 實際上是將 obj 放入到自動釋放池。autorelease 的釋放時機就是 AutoreleasePool 的釋放時機。
[obj autorelease];
....
最終調(diào)用的是
AutoreleasePoolPage::autorelease(obj);
obj 會被加入到自動釋放池
實際上 AutoreleasePool 的釋放時機是和 Runloop 相關(guān)的。Runloop 內(nèi)部會注冊兩個和 AutoreleasePool 相關(guān)的 observer,
// 監(jiān)聽 Runloop 進入,最先執(zhí)行 autoreleasePush()
"<CFRunLoopObserver 0x600000ca4320 [0x7fff8062d610]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x6000033d12c0 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb41780b038>\n)}}",
// 監(jiān)聽 Runloop 休眠和退出,執(zhí)行 autoreleasePop()
"<CFRunLoopObserver 0x600000ca43c0 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x6000033d12c0 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb41780b038>\n)}}"
可以發(fā)現(xiàn):
進入 Runloop 的時候最先執(zhí)行 autoreleasePush() 創(chuàng)建并進入 pool。
Runloop 進入休眠的時候,執(zhí)行 autoreleasePop() 釋放當前 pool 內(nèi)的對象。
Runloop 退出的時候,執(zhí)行 autoreleasePop() 釋放當前 pool 內(nèi)的對象。
談?wù)剬?iOS 內(nèi)存管理的理解
思路:
MRC - 自己寫 retain/release 時代
ARC - 編譯器自動生成 RR 代碼,簡化開發(fā)
核心概念 AutoreleasePool 的實現(xiàn)原理。
- end