面試 (五) : 原理篇-1

weixin_34189116發表於2017-06-09

runtime怎麼新增屬性、方法等
• ivar表示成員變數
• class_addIvar
• class_addMethod
• class_addProperty
• class_addProtocol
• class_replaceProperty

是否可以把比較耗時的操作放在NSNotificationCenter中
• 首先必須明確通知在哪個執行緒中發出,那麼處理接受到通知的方法也在這個執行緒中呼叫
• 如果在非同步執行緒發的通知,那麼可以執行比較耗時的操作;
• 如果在主執行緒發的通知,那麼就不可以執行比較耗時的操作

runtime 如何實現 weak 屬性
• 首先要搞清楚weak屬性的特點

weak策略表明該屬性定義了一種“非擁有關係” (nonowning relationship)。
為這種屬性設定新值時,設定方法既不保留新值,也不釋放舊值。此特質同assign類似;
然而在屬性所指的物件遭到摧毀時,屬性值也會清空(nil out)
• 那麼runtime如何實現weak變數的自動置nil?

runtime對註冊的類,會進行佈局,會將 weak 物件放入一個hash表中。
用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為0的時候會呼叫物件的 dealloc 方法,
假設weak 指向的物件記憶體地址是a,那麼就會以a為key,在這個 weak hash表中搜尋,找到所有以a為key的 weak 物件,從而設定為 nil。
weak屬性需要在dealloc中置nil麼
• 在ARC環境無論是強指標還是弱指標都無需在 dealloc 設定為 nil , ARC 會自動幫我們處理
• 即便是編譯器不幫我們做這些,weak也不需要在dealloc中置nil
• 在屬性所指的物件遭到摧毀時,屬性值也會清空

模擬下weak的setter方法,大致如下

  • (void)setObject:(NSObject *)object
    {
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    [object cyl_runAtDealloc:^{
    _object = nil;
    }];
    }

一個Objective-C物件如何進行記憶體佈局?(考慮有父類的情況)
• 所有父類的成員變數和自己的成員變數都會存放在該物件所對應的儲存空間中
• 父類的方法和自己的方法都會快取在類物件的方法快取中,類方法是快取在元類物件中
• 每一個物件內部都有一個isa指標,指向他的類物件,類物件中存放著本物件的如下資訊
○ 物件方法列表
○ 成員變數的列表
○ 屬性列表

每個Objective-C 物件都有相同的結構,如下圖所示
Objective-C 物件的結構圖

ISA指標

根類(NSObject)的例項變數

倒數第二層父類的例項變數

...

父類的例項變數

類的例項變數

• 根類物件就是NSObject,它的super class指標指向nil
類物件既然稱為物件,那它也是一個例項。類物件中也有一個isa指標指向它的元類(meta class),即類物件是元類的例項。元類內部存放的是類方法列表,根元類的isa指標指向自己,superclass指標指向NSObject類

[圖片上傳中。。。(1)]

一個objc物件的isa的指標指向什麼?有什麼作用?
• 每一個物件內部都有一個isa指標,這個指標是指向它的真實型別
• 根據這個指標就能知道將來呼叫哪個類的方法
下面的程式碼輸出什麼?

@implementation Son : Father

  • (id)init
    {
    self = [super init];
    if (self) {
    NSLog(@"%@", NSStringFromClass([self class]));
    NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
    }
    @end
    • 答案:都輸出Son
    • 這個題目主要是考察關於objc中對 self 和 super 的理解:
    ○ self 是類的隱藏引數,指向當前呼叫方法的這個類的例項。而 super 本質是一個編譯器標示符,和 self 是指向的同一個訊息接受者
    ○ 當使用 self 呼叫方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;
    ○ 而當使用 super時,則從父類的方法列表中開始找。然後呼叫父類的這個方法
    ○ 呼叫[self class] 時,會轉化成 objc_msgSend函式

id objc_msgSend(id self, SEL op, ...)
○ 呼叫 [super class]時,會轉化成 objc_msgSendSuper函式

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
○ 第一個引數是 objc_super 這樣一個結構體,其定義如下

struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
○ 第一個成員是receiver, 類似於上面的 objc_msgSend函式第一個引數self
○ 第二個成員是記錄當前類的父類是什麼,告訴程式從父類中開始找方法,找到方法後,最後內部是使用 objc_msgSend(objc_super->receiver, @selector(class))去呼叫, 此時已經和[self class]呼叫相同了,故上述輸出結果仍然返回 Son
○ objc Runtime開原始碼對- (Class)class方法的實現

-(Class)class {
return object_getClass(self);
}
runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法)
• 每一個類物件中都一個物件方法列表(物件方法快取)
• 類方法列表是存放在類物件中isa指標指向的元類物件中(類方法快取)
• 方法列表中每個方法結構體中記錄著方法的名稱,方法實現,以及引數型別,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現.
• 當我們傳送一個訊息給一個NSObject物件時,這條訊息會在物件的類物件方法列表裡查詢
• 當我們傳送一個訊息給一個類時,這條訊息會在類的Meta Class物件的方法列表裡查詢
objc中的類方法和例項方法有什麼本質區別和聯絡
• 類方法:
○ 類方法是屬於類物件的
○ 類方法只能通過類物件呼叫
○ 類方法中的self是類物件
○ 類方法可以呼叫其他的類方法
○ 類方法中不能訪問成員變數
○ 類方法中不能直接呼叫物件方法
○ 類方法是儲存在元類物件的方法快取中
• 例項方法:
○ 例項方法是屬於例項物件的
○ 例項方法只能通過例項物件呼叫
○ 例項方法中的self是例項物件
○ 例項方法中可以訪問成員變數
○ 例項方法中直接呼叫例項方法
○ 例項方法中可以呼叫類方法(通過類名)
○ 例項方法是存放在類物件的方法快取中
使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?
• 無論在MRC下還是ARC下均不需要
• 被關聯的物件在生命週期內要比物件本身釋放的晚很多,它們會在被 NSObject -dealloc 呼叫的object_dispose()方法中釋放
• 補充:物件的記憶體銷燬時間表,分四個步驟

1.呼叫-release :引用計數變為零

  • 物件正在被銷燬,生命週期即將結束.
  • 不能再有新的 __weak 弱引用,否則將指向 nil.
  • 呼叫 [self dealloc]
  1. 父類呼叫-dealloc
  • 繼承關係中最直接繼承的父類再呼叫 -dealloc
  • 如果是MRC 程式碼 則會手動釋放例項變數們(iVars)
  • 繼承關係中每一層的父類 都再呼叫 -dealloc
  1. NSObject 調 -dealloc
  • 只做一件事:呼叫Objective-C runtime 中的 object_dispose() 方法
  1. 呼叫object_dispose()
  • 為 C++ 的例項變數們(iVars)呼叫destructors
  • 為 ARC 狀態下的 例項變數們(iVars) 呼叫 -release
  • 解除所有使用runtime Associate方法關聯的物件
  • 解除所有 __weak 引用
  • 呼叫free()
    _objc_msgForward函式是做什麼的?直接呼叫它將會發生什麼?
    • _objc_msgForward是IMP型別,用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward會嘗試做訊息轉發
    • 直接呼叫_objc_msgForward是非常危險的事,這是把雙刃刀,如果用不好會直接導致程式Crash,但是如果用得好,能做很多非常酷的事
    • JSPatch就是直接呼叫_objc_msgForward來實現其核心功能的
    • 詳細解說參見這裡的第一個問題解答
    能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
    • 不能向編譯後得到的類中增加例項變數;
    • 能向執行時建立的類中新增例項變數;
    • 分析如下:
    ○ 因為編譯後的類已經註冊在runtime中,類結構體中的objc_ivar_list 例項變數的連結串列和instance_size例項變數的記憶體大小已經確定,同時runtime 會呼叫class_setIvarLayout 或 class_setWeakIvarLayout來處理strong weak引用,所以不能向存在的類中新增例項變數
    ○ 執行時建立的類是可以新增例項變數,呼叫 class_addIvar函式,但是得在呼叫objc_allocateClassPair之後,objc_registerClassPair之前,原因同上。
    runloop和執行緒有什麼關係?
    • 每條執行緒都有唯一的一個RunLoop物件與之對應的
    • 主執行緒的RunLoop是自動建立並啟動
    • 子執行緒的RunLoop需要手動建立
    • 子執行緒的RunLoop建立步驟如下:
    ○ 在子執行緒中呼叫[NSRunLoop currentRunLoop]建立RunLoop物件(懶載入,只建立一次)
    ○ 獲得RunLoop物件後要呼叫run方法來啟動一個執行迴圈

啟動RunLoop
[[NSRunLoop currentRunLoop] run];
○ RunLoop的其他啟動方法

第一個引數:指定執行模式
第二個引數:指定RunLoop的過期時間,即:到了這個時間後RunLoop就失效了
[[NSRunLoop currentRunLoop] runMode:kCFRunLoopDefaultMode beforeDate:[NSDate distantFuture]];
runloop的mode作用是什麼?
• 用來控制一些特殊操作只能在指定模式下執行,一般可以通過指定操作的執行mode來控制執行時機,以提高使用者體驗
• 系統預設註冊了5個Mode
○ kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行,對應OC中的:NSDefaultRunLoopMode
○ UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他Mode影響
○ kCFRunLoopCommonModes:這是一個標記Mode,不是一種真正的Mode,事件可以執行在所有標有common modes標記的模式中,對應OC中的NSRunLoopCommonModes,帶有common modes標記的模式有:UITrackingRunLoopMode和kCFRunLoopDefaultMode
○ UIInitializationRunLoopMode:在啟動 App時進入的第一個 Mode,啟動完成後就不再使用
○ GSEventReceiveRunLoopMode:接受系統事件的內部Mode,通常用不到
以+scheduledTimerWithTimeInterval...的方式觸發的timer,在滑動頁面上的列表時,timer會暫定回撥,為什麼?如何解決?
• 這裡強調一點:在主執行緒中以+scheduledTimerWithTimeInterval...的方式觸發的timer預設是執行在NSDefaultRunLoopMode模式下的,當滑動頁面上的列表時,進入了UITrackingRunLoopMode模式,這時候timer就會停止
• 可以修改timer的執行模式為NSRunLoopCommonModes,這樣定時器就可以一直執行了
• 以下是我的筆記補充:
○ 在子執行緒中通過scheduledTimerWithTimeInterval:...方法來構建NSTimer
§ 方法內部已經建立NSTimer物件,並加入到RunLoop中,執行模式為NSDefaultRunLoopMode
§ 由於Mode有timer物件,所以RunLoop就開始監聽定時器事件了,從而開始進入執行迴圈
§ 這個方法僅僅是建立RunLoop物件,並不會主動啟動RunLoop,需要再呼叫run方法來啟動
○ 如果在主執行緒中通過scheduledTimerWithTimeInterval:...方法來構建NSTimer,就不需要主動啟動RunLoop物件,因為主執行緒的RunLoop物件在程式執行起來就已經被啟動了

userInfo引數:用來給NSTimer的userInfo屬性賦值,userInfo是隻讀的,只能在構建NSTimer物件時賦值
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run:) userInfo:@"ya了個hoo" repeats:YES];

scheduledTimer...方法建立出來NSTimer雖然已經指定了預設模式,但是【允許你修改模式】
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

【僅在子執行緒】需要手動啟動RunLoop物件,進入執行迴圈
[[NSRunLoop currentRunLoop] run];

猜想runloop內部是如何實現的?
• 從字面意思看:執行迴圈、跑圈;
• 本質:內部就是do-while迴圈,在這個迴圈內部不斷地處理各種事件(任務),比如:Source、Timer、Observer;
• 每條執行緒都有唯一一個RunLoop物件與之對應,主執行緒的RunLoop預設已經啟動,子執行緒的RunLoop需要手動啟動;
• 每次RunLoop啟動時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode,如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入,這樣做主要是為了隔離不同Mode中的Source、Timer、Observer,讓其互不影響;
• 附上RunLoop的執行圖

[圖片上傳中。。。(2)]

不手動指定autoreleasepool的前提下,一個autorealese物件在什麼時刻釋放?(比如在一個vc的viewDidLoad中建立)
• 分兩種情況:手動干預釋放時機、系統自動去釋放
○ 手動干預釋放時機:指定autoreleasepool就是所謂的:當前作用域大括號結束時就立即釋放
○ 系統自動去釋放:不手動指定autoreleasepool,Autorelease物件會在當前的 runloop 迭代結束時釋放,下面詳細說明釋放時機
§ RunLoop中的三個狀態會處理自動釋放池,通過列印程式碼發現有兩個Observer監聽到狀態值為:1和160(32+128)
□ kCFRunLoopEntry(1) 第一次進入會建立一個自動釋放池
□ kCFRunLoopBeforeWaiting(32) 進入休眠狀態前先銷燬自動釋放池,再建立一個新的自動釋放池
□ kCFRunLoopExit(128) 退出RunLoop時銷燬最後一次建立的自動釋放池
• 如果在一個vc的viewDidLoad中建立一個Autorelease物件,那麼該物件會在 viewDidAppear 方法執行前就被銷燬了(是這樣的嗎???)

蘋果是如何實現autoreleasepool的?
• autoreleasepool以一個佇列陣列的形式實現,主要通過下列三個函式完成.

objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_aurorelease
• 看函式名就可以知道,對autorelease分別執行push,和pop操作。銷燬物件時執行release操作
GCD的佇列(dispatch_queue_t)分哪兩種型別?背後的執行緒模型是什麼樣的?
• 序列佇列
• 並行佇列
• dispatch_global_queue();是全域性併發佇列
• dispatch_main_queue();是一種特殊序列佇列
• 背後的執行緒模型:自定義佇列 dispatch_queue_t queue; 可以自定義是並行:DISPATCH_QUEUE_CONCURRENT 或者 序列DISPATCH_QUEUE_SERIAL

蘋果為什麼要廢棄dispatch_get_current_queue?
• 容易誤用造成死鎖
如何用GCD同步若干個非同步呼叫?(如根據若干個url非同步載入多張圖片,然後在都下載完成後合成一張整圖)
• 必須是併發佇列才起作用
• 需求分析
○ 首先,分別非同步執行2個耗時的操作
○ 其次,等2個非同步操作都執行完畢後,再回到主執行緒執行一些操作
• 使用佇列組實現上面的需求

建立佇列組
dispatch_group_t group = dispatch_group_create();
獲取全域性併發佇列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
往佇列組中新增耗時操作
dispatch_group_async(group, queue, ^{
執行耗時的非同步操作1
});
往佇列組中新增耗時操作
dispatch_group_async(group, queue, ^{
執行耗時的非同步操作2
});
當併發佇列組中的任務執行完畢後才會執行這裡的程式碼
dispatch_group_notify(group, queue, ^{
如果這裡還有基於上面兩個任務的結果繼續執行一些程式碼,建議還是放到子執行緒中,等程式碼執行完畢後在回到主執行緒
回到主執行緒
dispatch_async(group, dispatch_get_main_queue(), ^{
執行相關程式碼...
});
});

dispatch_barrier_async的作用是什麼?

• 函式定義

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
• 必須是併發佇列,要是序列佇列,這個函式就沒啥意義了
• 注意:這個函式的第一個引數queue不能是全域性的併發佇列
• 作用:在它前面的任務執行結束後它才執行,在它後面的任務等它執行完成後才會執
• 示例程式碼

-(void)barrier
{
dispatch_queue_t queue = dispatch_queue_create("12342234", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"----1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----2-----%@", [NSThread currentThread]);
});
在它前面的任務執行結束後它才執行,在它後面的任務等它執行完成後才會執行
dispatch_barrier_async(queue, ^{
NSLog(@"----barrier-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----3-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"----4-----%@", [NSThread currentThread]);
});
}

以下程式碼執行結果如何?

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2");
    });
    NSLog(@"3");
    }
    • 答案:主執行緒死鎖

相關文章