OC-內(nèi)存管理

直接看看下面的面試題

  1. 介紹一下內(nèi)存的幾大區(qū)域
  2. 使用 CDDisplayLink、NSTimer 有什么注意點
  3. 講一下對 iOS 內(nèi)存管理的理解
  4. autorelease 什么時候釋放
  5. 方法里有局部變量,出了方法后會立即釋放嗎? 表現(xiàn)上是的
  6. ARC 都幫我們做了什么
  7. weak 指針的實現(xiàn)原理

CDDisplayLink、NSTimer 使用注意與處理

CDDisplayLink、NSTimer 會對 target 產(chǎn)生強引用,如果target 對它再產(chǎn)生強引用,那就會發(fā)生循環(huán)引用。

處理這個循環(huán)引用問題:

  1. 針對 NSTimer 可以使用 initWithBlock 的方式,直接在block 內(nèi)做事情。
  2. 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)存分布如下。

程序裝載之后內(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)存訪問,崩潰。

解決方法:

  1. 使用線程同步技術(shù),將并發(fā)任務(wù)改成串行
  2. 將屬性改為 atomic 屬性
  3. 改用 Tagged Point 技術(shù) 【上文中的注釋即可】

Copy 和 mutableCopy

  1. copy: 不可變拷貝,產(chǎn)生不可變副本
  2. mutableCopy: 可變拷貝,產(chǎn)生可變副本

拷貝分為深拷貝(內(nèi)容拷貝,產(chǎn)生新內(nèi)容) 和淺拷貝(指針拷貝)。

不可變對象 copy -> 指針拷貝 -> 結(jié)果為兩個指針指向同一個不可變對象
不可變對象 mutableCopy -> 內(nèi)容拷貝 -> 結(jié)果為兩個指針,一個可變對象,一個不可變對象

可變對象 copy -> 深拷貝 -> 結(jié)果兩個指針,一個可變對象,一個copy出來的不可變對象
可變對象 mutableCopy -> 深拷貝 -> 結(jié)果是兩個指針,兩個可變對象

總結(jié):


copy 總結(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é)

  1. 當調(diào)用 alloc/new/copy/mutableCopy 方法返回了一個對象,在不需要這個對象的時候,需要調(diào)用 release/autorelease 來釋放它
  2. 想擁有某個對象,就讓它的引用計數(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)邏輯

  1. 釋放池內(nèi)部結(jié)構(gòu)是棧,它依據(jù)線程存在
  2. 池(棧)中指針是自動釋放obj指針,或者 POOL_BOUNDARY 指針
  3. 池釋放時,以 POOL_BOUNDARY 為界,先釋放內(nèi)部的 obj
  4. 所有的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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容