離職找工作中,刷一刷網上的面試題。原文連結
23. block的實質是什麼?一共有幾種block?都是什麼情況下生成的?
block是什麼呢?
block是能夠截獲自動變數(區域性變數)的匿名函式。寫法如下:
^int (int count) {
return count + 1;
}
^{
printf("Blocks\n");
}
複製程式碼
block的本質是什麼呢?
定義一個簡單的block
int main(int argc, const char * argv[]) {
void (^block)() = ^{
printf("Hello World!");
};
block();
return 0;
}
複製程式碼
使用Clang
命令
clang -rewrite-objc main.m
複製程式碼
得到結果
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello World!");}
複製程式碼
你定義完block之後,其實是建立了一個函式,在建立結構體的時候把函式的指標一起傳給了block,所以之後可以拿出來呼叫。
其實,Block 是轉化為 Block 結構體型別的自動變數,型別定義如下。通過void *isa;,可以知道 Block 也是一個 OC 的物件,這就是 Block 的本質。
一共有幾種block?
根據isa指標,block一共有3種型別的block
* _NSConcreteGlobalBlock 全域性靜態,儲存在資料.data 區域
* _NSConcreteStackBlock 儲存在棧中,出函式作用域就銷燬
* _NSConcreteMallocBlock 儲存在堆中,retainCount == 0銷燬
複製程式碼
這幾種block是什麼情況下生成的
遇到一個Block,我們怎麼判斷這個Block的儲存位置呢?
- 定義在函式外面的block,或者block不訪問外界變數(包括棧中和堆中的變數)為全域性block。例如
typedef int (^blk_t)(int);
blk_t gBlk = ^(int count) {return count;};
int test(){
for(int i=0;i<10;i++){
blk_t blk = ^(int count) {return count;};
}
}
複製程式碼
-
Block訪問外界變數
- MRC 環境下:訪問外界變數的 Block 預設儲存棧中。
- ARC 環境下:訪問外界變數的 Block 預設儲存在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。
引用於這篇文章
ibireme大神寫的關於block的這篇文章
關於block的詳解這篇文章也寫得很好。
24. 為什麼在預設情況下無法修改被block捕獲的變數? __block都做了什麼?
因為變數被拷貝的到block結構體中了。所以不能修改。對於用 __block
修飾的外部變數引用,block 是複製其引用地址來實現訪問的。block可以修改__block
修飾的外部變數的值。
clang
程式碼
__block int val = 10;
轉換成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
複製程式碼
會發現一個區域性變數加上__block
修飾符後竟然跟block一樣變成了一個__Block_byref_val_0
結構體型別的自動變數例項!
23. 模擬一下迴圈引用的一個情況?block實現介面反向傳值如何實現?
迴圈引用
self.someBlock = ^{
[self dosomething];
};
複製程式碼
block實現介面反向傳值如何實現
這個沒明白什麼意思。難道是這樣?
@interface OneCell : UITableViewCell
@property (noaotomic,copy)^someAction(OneCell *cell , Action type);
@end
複製程式碼
24. objc在向一個物件傳送訊息時,發生了什麼?
簡單的說大概是:
- 判斷該物件是否為nil,為nil則返回nil
- 不為nil,就從該物件的isa指標找到該物件的類
- 從類的cache裡面查詢方法,如果命中則呼叫該方法
- 未命中則從類的方法列表裡面查詢
- 依然未命中,則從父類,父類的父類...方法列表裡面查詢
- 還是未命中則走訊息轉發機制
- 呼叫
+(BOOL)resolveInstanceMethord:(SEL)sel;
- 呼叫
+(BOOL)forwardingTargetForSelector:(SEL)sel
- 嗲用
+(void)methodSignatureForSelector:(SEL)sel
- 呼叫
+(void)forwardInvocation:(NSInvocation *)invocation
- 呼叫
具體的可以看這篇文章,他從底層程式碼分析了Objective-C 訊息傳送與轉發機制原理
25. 什麼時候會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
當一個類未實現呼叫的方法的時候就會報unrecognized selector
。要解決這個問題可以從上一題的訊息轉發機制入手。
26. 能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
不能向編譯後的類增加例項變數。但是可以用class_addIvar
向執行時建立的類新增例項變數。引用蘋果官方的話說:
This function may only be called after
objc_allocateClassPair
and beforeobjc_registerClassPair
. Adding an instance variable to an existing class is not
給一個類新增變數會更改該類的例項的記憶體佈局。所以,不能動態的修改類的例項變數。
27. runtime如何實現weak變數的自動置nil?
網上有很多種方法。我想得比較簡單。用一個NSTableMap來裝屬性變數。
@interface ViewController(addWeak)
@property(nonatomic,readonly)NSMapTable *propertyMap;
@property(nonatomic,weak)NSObject *someObj;
@end
@implementation ViewController(addWeak)
static NSString *mapKey = @"mapKey";
-(NSMapTable *)propertyMap{
NSMapTable *table = objc_getAssociatedObject(self, &mapKey);
if (table==nil) {
table = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
}
return table;
}
-(NSObject *)someObj{
return [self.propertyMap objectForKey:@"someObj"];
}
-(void)setSomeObj:(NSObject *)someObj{
if (someObj == nil) {
[self.propertyMap removeObjectForKey:@"someObj"];
}else{
[self.propertyMap setObject:someObj forKey:@"someObj"];
}
}
@end
複製程式碼
28. 給類新增一個屬性後,在類結構體裡哪些元素會發生變化?
bestswifter大神的這篇關於Catgory的文章第二段有詳細說明OC2.0中Class的結構。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
};
複製程式碼
其中class_rw_t
結構體如下:
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
}
複製程式碼
在新加一個property
的時候,objc_object
的bit
欄位會新增成員變數,data
欄位中methors
會增加響應的get
,set
方法,properties
陣列會新增元素。
29. runloop是來做什麼的?runloop和執行緒有什麼關係?主執行緒預設開啟了runloop麼?子執行緒呢?
runloop是來做什麼的
runploop是為了讓執行緒一直接受任務不退出的。
runloop和執行緒有什麼關係
Runloop
和執行緒是一一對應的。1個執行緒只有1個Runloop
。他們的關係是放在一個全域性的Dictionary
裡面的。執行緒剛建立時沒有 RunLoop
,如果你不主動獲取,那它一直都不會有。蘋果不允許直接建立 RunLoop
,它只提供了兩個自動獲取的函式:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。RunLoop
的建立是發生在第一次獲取時,RunLoop 的銷燬是發生線上程結束時。 RunLoop
只能在一個執行緒的內部獲取。
主執行緒預設開啟了runloop麼?子執行緒呢?
主執行緒預設開啟了runloop。子執行緒沒有。
30. runloop的mode是用來做什麼的?有幾種mode?
在ibireme大神的深入理解RunLoop有詳細的介紹runloop。一下文字也是引用自這篇文章。
CFRunLoopMode 和 CFRunLoop 的結構大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
複製程式碼
這裡有個概念叫 “CommonModes”:一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 新增到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裡的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode裡。
應用場景舉例:主執行緒的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回撥,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回撥,並且也不會影響到滑動操作。
有時你需要一個 Timer,在兩個 Mode 中都能得到回撥,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 裡去。
CFRunLoop對外暴露的管理 Mode 介面只有下面2個:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製程式碼
ode 暴露的管理 mode item 的介面有下面幾個:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製程式碼
你只能通過 mode name 來操作內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時,RunLoop會自動幫你建立對應的 CFRunLoopModeRef。對於一個 RunLoop 來說,其內部的 mode 只能增加不能刪除。
蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用這兩個 Mode Name 來操作其對應的 Mode。
同時蘋果還提供了一個操作 Common 標記的字串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用這個字串來操作 Common Items,或標記一個 Mode 為 “Common”。使用時注意區分這個字串和其他 mode name。
31. 為什麼把NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈以後,滑動scrollview的時候NSTimer卻不動了?
因為當我們滑動ScrollView的時候,當前runloop的model為UITrackingRunLoopMode
。所以在NSDefaultRunLoopMode
下的NSTimer就不動了。
要解決這個問題。可以用以下幾種方式:
- 把timer的model新增
NSRunLoopCommonModes
中。 - 把timer新增到子執行緒的runloop中。
32. 蘋果是如何實現Autorelease Pool的?
關於Autorelease Pool
sunny大神的的黑幕背後的Autorelease介紹得很好。以下回答也是從這篇文章找的。
AutoreleasePoolPage
AutoreleasePool
並沒有單獨的結構,而是由若干個AutoreleasePoolPage
以雙向連結串列的形式組合而成(分別對應結構中的parent指標和child指標)AutoreleasePool
是按執行緒一一對應的(結構中的thread
指標指向當前執行緒)AutoreleasePoolPage
每個物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址- 上面的
id *next
指標作為遊標指向棧頂最新add
進來的autorelease
物件的下一個位置 - 一個
AutoreleasePoolPage
的空間被佔滿時,會新建一個AutoreleasePoolPage
物件,連線連結串列,後來的autorelease
物件在新的page
加入
釋放時機
在沒有手加Autorelease Pool
的情況下,Autorelease
物件是在當前的runloop
迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop
迭代中都加入了自動釋放池Push
和Pop
.
每當進行一次objc_autoreleasePoolPush呼叫時,runtime向當前的AutoreleasePoolPage中add進一個哨兵物件,值為0(也就是個nil),那麼這一個page就變成了下面的樣子:
objc_autoreleasePoolPush的返回值正是這個哨兵物件的地址,被objc_autoreleasePoolPop(哨兵物件)作為入參,於是:根據傳入的哨兵物件地址找到哨兵物件所處的page 在當前page中,將晚於哨兵物件插入的所有autorelease物件都傳送一次- release訊息,並向回移動next指標到正確位置 補充2:從最新加入的物件一直向前清理,可以向前跨越若干個page,直到哨兵所在的page 剛才的objc_autoreleasePoolPop執行後,最終變成了下面的樣子:
33. isa指標?(物件的isa,類物件的isa,元類的isa都要說)
物件的isa
指向的是類,類的isa
指向的元類,元類的isa
指向的NSObject
34. 類方法和例項方法有什麼區別?
不是很明白這題的意思。
- 類方法用類呼叫,例項方法需要例項呼叫
- 類方法存在於元類的方法列表裡,例項方法存在於類的方法列表裡
35. 介紹一下分類,能用分類做什麼?內部是如何實現的?它為什麼會覆蓋掉原來的方法?
分類的作用
- 可以把類的實現分開在幾個不同的檔案裡面。
- 可以減少單個檔案的體積
- 可以把不同的功能組織到不同的category裡
- 可以由多個開發者共同完成一個類
- 可以按需載入想要的category 等等。
- 宣告私有方法
分類的結構
struct category_t {
const char *name; // 類名
classref_t cls; // 分類所屬的類
struct method_list_t *instanceMethods; // 例項方法列表
struct method_list_t *classMethods; // 類方法列表
struct protocol_list_t *protocols; // 遵循的協議列表
struct property_list_t *instanceProperties; // 屬性列表
};
複製程式碼
分類中的方法是新增到原類方法列表的前面的,並不會替換掉原來的方法。所以就算新增了分類以後依然可以呼叫原方法
Class currentClass = [MyClass class];
MyClass *my = [[MyClass alloc] init];
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
if ([@"printName" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL);
if (lastImp != NULL) {
fn f = (fn)lastImp;
f(my,lastSel);
}
free(methodList);
}
複製程式碼
關於分類美團的這篇文章寫得很詳細
36. 執行時能增加成員變數麼?能增加屬性麼?如果能,如何增加?如果不能,為什麼?
這題和第26題有些重複吧。執行時不能動態增加成員變數,能動態增加屬性。動態增加屬性的時候需要用objc_setAssociatedObject
和objc_getAssociatedObject
來關聯屬性所對應的變數。
37. objc中向一個nil物件傳送訊息將會發生什麼?(返回值是物件,是標量,結構體)
什麼也不會發生。返回值是物件的話,就返回nil。返回標量的就是預設值,比如CGRect就為(0,0,0,0)。返回值是結構體的話,就返回結構體體的初始值。