前言:
最近看到大佬匯集的iOS面試題,個人感覺還不錯,打算試著探索一下這些問題的答案,也鞏固一下我自己基礎知識。這篇文章先總結一下基礎知識的答案吧。其中有些錯誤或不全的地方望指教。
-----------------------------------------持續更新中---------------------------------
iOS 基礎題
1:分類和擴展有什么區別?可以分別用來做什么?分類有哪些局限性?分類的結構體里面有哪些成員?
分類和擴展的作用
1:category的主要作用是為已經存在的類添加方法
下面也有其他作用可以了解下:
2:可以把類的實現分開在幾個不同的文件里面,
(可以減少單個文件的體積
可以把不同的功能組織到不同的category里
可以由多個開發者共同完成一個類
可以按需加載想要的category)
3:模擬多繼承
4:把framework的私有方法公開
擴展的作用:為一個類添加額外的原來沒有變量,方法和屬性
類別與類擴展的區別
1:extension在編譯期決定,它就是類的一部分,
在編譯期和頭文件里的@interface以及實現文件里的@implement一起形成一個完整的類,
它伴隨類的產生而產生,亦隨之一起消亡。
extension一般用來隱藏類的私有信息,
你必須有一個類的源碼才能為一個類添加extension,所以你無法為系統的類比如NSString添加extension
但是category則完全不一樣,它是在運行時候決定的.
類擴展是在編譯階段被添加到類中,而類別是在運行時添加到類中。
extension可以添加實例變量,而category是無法添加實例變量的
2:類擴展中聲明的方法沒被實現,編譯器會報警,但是類別中的方法沒被實現編譯器是不會有任何警告的。
分類局限性
(1)無法向類中添加新的實例變量。
(2)名稱沖突,即當類別中的方法與原始類方法名稱沖突時,類別具有更高的優先級。
(3)如果多個分類中都有和原有類中同名的方法, 那么調用該方法的時候執行誰由編譯器決定;編譯器會執行最后一個參與編譯的分類中的方法
在runtime層,category用結構體category_t
typedef 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;
} category_t;
從源碼中我們可以看出分類結構體成員:
1)類的名字(name)
2)類(cls)
3)category中所有給類添加的實例方法的列表(instanceMethods)
4)category中所有添加的類方法的列表(classMethods)
5)category實現的所有協議的列表(protocols)
6)category中添加的所有屬性(instanceProperties)
參考鏈接:
分類和擴展說明參考
美團關于分類的源碼解析說明
官方分類源碼地址
2:atomic的實現機制;為什么不能保證絕對的線程安全
這個問題我覺得看這個就夠了stackoverflow關于atomic和nonatomic的一個問題
當然也可以看別人根據stackoverflow這個問題總結好的中文說明
簡單來說:atomic 會加一個鎖來保障線程安全,也就是保證了讀寫操作是安全的,并且引用計數會 +1,來向調用者保證這個對象會一直存在.
但是不能保證線程安全,比如當線程A setter操作時,這時B線程的setter操作會等待。當A線程的setter結束后,B線程進行setter操作,
然后當A線程需要getter操作時,卻有可能獲得了在B線程中的值,這就破壞了線程安全
3:哪些場景可以觸發離屏渲染?
首先我們要知道什么是離屏渲染:
離屏渲染Off-Screen Rendering 指的是GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。
離屏渲染會先在屏幕外創建新緩沖區,離屏渲染結束后,再從離屏切到當前屏幕
還有另外一種屏幕渲染方式-當前屏幕渲染On-Screen Rendering ,
指的是GPU的渲染操作是在當前用于顯示的屏幕緩沖區中進行。
以下方式會觸發離屏幕渲染
1:使用系統提供的圓角效果也會觸發離屏渲染.(masksToBounds = true&&cornerRadius>0才會引發離屏渲染)
2:重寫drawRect
3:layer.shadow(Shawdow 可以通過指定路徑來取消離屏渲染)
4:layer.mask(Mask 效果無法取消離屏渲染,使用混合圖層的方法來模擬 mask 效果,性能各方面都是和無效果持平。)
5:layer.allowsGroupOpacity(GroupOpacity 是指 CALayer 的allowsGroupOpacity屬性,UIView 的alpha屬性等同于 CALayer opacity屬性,
開啟離屏渲染的條件是:layer.opacity != 1.0并且有子 layer 或者背景圖。)
layer.allowsEdgeAntialiasing(該屬性用于消除鋸齒,離屏渲染條件旋轉視圖并且設置layer.allowsEdgeAntialiasing = true)
6:layer.shouldRasterize(光柵化會觸發離屏渲染,開啟 Rasterization=true 后,GPU 只合成一次內容,然后復用合成的結果;合成的內容超過 100ms 沒有使用會從緩存里移除,在更新內容時還會產生更多的離屏渲染。對于內容不發生變化的視圖,原本拖后腿的離屏渲染就成為了助力)
參考:
離屏渲染優化詳解
Instruments性能優化-Core Animation
繪制像素到屏幕上
界面流暢性優化
4:被weak修飾的對象在被釋放的時候會發生什么?是如何實現的?知道sideTable么?里面的結構可以畫出來么?
釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取所有weak指針地址的數組,然后遍歷這個數組把其中的數據設為nil,最后把這個entry從weak表中刪除,最后清理對象的記錄.
objc_clear_deallocating該函數的動作如下:
1、從weak表中獲取廢棄對象的地址為鍵值的記錄
2、將包含在記錄中的所有附有 weak修飾符變量的地址,賦值為nil
3、將weak表中該記錄刪除
4、從引用計數表中刪除廢棄對象的地址為鍵值的記錄
SideTable 這個結構體主要用于管理對象的引用計數和 weak 表。在 NSObject.mm 中聲明其數據結構:
struct SideTable {
spinlock_t slock;//保證原子操作的自旋鎖
RefcountMap refcnts;//引用計數的 hash 表
weak_table_t weak_table;//weak 引用全局 hash 表
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
參考:
objc-weak.mm源碼
weak 弱引用的實現方式
iOS 底層解析weak的實現原理
5:KVO的底層實現?如何取消系統默認的KVO并手動觸發(給KVO的觸發設定條件:改變的值符合某個條件時再觸發KVO)?
當你觀察一個對象時,一個新的類會被動態創建。這個類繼承自該對象的原本的類,并重寫了被觀察屬性的 setter 方法。重寫的 setter 方法會負責在調用原 setter 方法之前和之后,通知所有觀察對象:值的更改。最后通過 isa 混寫(isa-swizzling) 把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統這個對象的類是什么 ) 指向這個新創建的子類,
對象就神奇的變成了新創建的子類的實例
關閉默認的KVO重寫方法
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}//如果返回NO,KVO無法自動運作,需手動觸發
鍵值觀察通知依賴于 NSObject 的兩個方法: willChangeValueForKey
: 和 didChangevlueForKey
。
在一個被觀察屬性發生改變之前, willChangeValueForKey:
一定會被調用,這就 會記錄舊的值。而當改變發生后,
observeValueForKey:ofObject:change:context:
會被調用,
并且 didChangeValueForKey:
也會被調用。如果可以手動實現這些調用,就可以實現手動觸發
.
參考:
如何自己動手實現 KVO
apple用什么方式實現對一個對象的KVO
6:一個int變量被__block修飾與否的區別?
Block不允許修改外部變量的值,這里所說的外部變量的值,指的是棧中指針的內存地址。
__block 所起到的作用就是只要觀察到該變量被 block 所持有。
__block 后,實際上成為了一個結構體,block內截獲了 該結構體的指針。
在block中使用自動變量時,使用的是 指針指向的結構體中的 自動變量。
ARC環境下,會被copy到堆上。(ARC環境下,一旦Block賦值就會觸發copy,__block就會copy到堆上,Block也是__NSMallocBlock。
ARC環境下也是存在__NSStackBlock的時候,這種情況下,__block就在棧上。)
MRC環境下,只有copy,__block才會被復制到堆上,否則,__block一直都在棧上。
測試,其實最好的方法是動手測試,這邊我只測試了ARC環境下的。我在.main.m
的測試代碼如下:
__block int a1 = 1;
int a2 = 1;
NSLog(@"__block定義前a1:%p", &a1);
NSLog(@"__block定義前a2:%p", &a2);;
void (^foo)(void) = ^{
a1 = 2;
NSLog(@"block內部a1:%p", &a1);
NSLog(@"block內部a2:%p", &a2);
};
NSLog(@"重新定義后a1:%p", &a1);
NSLog(@"重新定義后a2:%p", &a2);
NSLog(@"foo =%@",foo);
foo();
——---------------------- 輸出結果如下:-------------------------------
__block定義前a1:0x7fff53814128
__block定義前a2:0x7fff5381410c
重新定義后a1:0x60400003dd98
重新定義后a2:0x7fff5381410c
foo =<__NSMallocBlock__: 0x60c000244830>
block內部a1:0x60400003dd98
block內部a2:0x60400025dbd8
通知打印結果可以發現a1,a2blcok內部和定義前的地址字節數相差很大,堆地址要小于棧地址,又因為iOS中一個進程的棧區內存只有1M,Mac也只有8M,所以a1和a2在block內部都會被copy到堆上,只不過一個值的copy,一個是地址copy。
然后clang -rewrite-objc main.m
查看一下源碼,如果clang -rewrite-objc
報錯,可以像我一樣嘗試
xcrun -sdk iphonesimulator11.0 clang -rewrite-objc main.m
源碼如下:
//加上__block 后,實際上成為了一個結構體,block內截獲了 該結構體的指針
struct __Block_byref_a1_0 {
void *__isa;
__Block_byref_a1_0 *__forwarding;
int __flags;
int __size;
int a1;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a2;
////截獲的結構體指針
__Block_byref_a1_0 *a1; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a2, __Block_byref_a1_0 *_a1, int flags=0) : a2(_a2), a1(_a1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//指針引用
__Block_byref_a1_0 *a1 = __cself->a1; // bound by ref
//a2只是單純的值拷貝,。Block僅僅捕獲了a2的值,并沒有捕獲a2的內存地址。
int a2 = __cself->a2; // bound by copy
(a1->__forwarding->a1) = 2;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_2, &(a1->__forwarding->a1));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_3, &a2);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a1, (void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}
從源碼中可以看出:
帶有 __block的變量也被轉化成了一個結構體__Block_byref_i_0,很清楚看到了__block的引用過程。
而Block僅僅捕獲了a2的值,并沒有捕獲a2的內存地址。所以在__main_block_func_0這個函數中即使我們重寫這個自動變量a2的值,
也無法改變Block外面自動變量a2的值
參考:
iOS中__block 關鍵字的底層實現原理
深入研究Block捕獲外部變量和__block實現原理
7:為什么在block外部使用__weak修飾的同時需要在內部使用__strong修飾
_weak是為了解決循環引用問題,(如果block和對象相互持有就會形成循環引用)
而__strong在Block內部修飾的對象,會保證,在使用這個對象在block內,
這個對象都不會被釋放,strongSelf僅僅是個局部變量,存在棧中,會在block執行結束后回收,不會再造成循環引用。
__strong主要是用在多線程中,防止對象被提前釋放。
參考:
iOS __weak和__strong在Block中的使用
題外話:
有時候我們經常也會被問到block為什么 常使用copy關鍵字?
官方中有如下一段話:
總結別人的話來說:
block 使用
copy
是從 MRC遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對于 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。
如果不寫 copy ,該類的調用者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”
8:講一下對象,類對象,元類,跟元類結構體的組成以及他們是如何相關聯的?為什么對象方法沒有保存的對象結構體里,而是保存在類對象的結構體里.
對象isa指向類對象,類對象的isa指向元類。元類isa指向根元類。
根元類的isa指針指向自己,superclass指針指向NSObject類
實例對象結構體只有一個isa變量,指向實例對象所屬的類。
類對象有isa,superclass,方法,屬性,協議列表,以及成員變量的
描述。
所有的對象調用方法都是一樣的,沒有必要存在對象中,對象可以有
無數個,類對象就有一個所以只需存放在類對象中
可以從官方objc.h源碼里面找到實例定義
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
可以在runtime.h里面找到類對象的定義
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//向該類所繼承的父類對象
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
//成員變量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;//方法列表
//用于緩存調用過的方法
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
//協議鏈表用來存儲聲明遵守的正式協議
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
}
參考:
iOS開發·runtime原理與實踐: 基本知識篇
一個objc對象如何進行內存布局
9:iOS 中內省的幾個方法?class方法和objc_getClass方法有什么區別?
題外話:原諒我看了這道面試題,第一次聽說內省,才疏學淺,太菜了,只能好好搜索學習了一番。
內省是對象揭示自己作為一個運行時對象的詳細信息的一種能力。包括對象在繼承樹上的位置,對象是否遵循特定的協議,以及是否可以響應特定的消息。NSObject協議和類定義了很多內省方法,用于查詢運行時信息,以便根據對象的特征進行識別。
isKindOfClass:Class
檢查對象是否是那個類或者其繼承類實例化的對象
isMemberOfClass:Class
檢查對象是否是那個類但不包括繼承類而實例化的對象
respondToSelector:selector
檢查對象是否包含這個方法
conformsToProtocol:protocol
檢查對象是否符合協議,是否實現了協議中所有的必選方法。
object_getClass(obj)返回的是obj中的isa指針;
而[obj class]則分兩種情況:
一:當obj為實例對象時,
[obj class]中class是實例方法:- (Class)class,
返回的obj對象中的isa指針,返回的是類對象;
二:當obj為類對象(包括元類和根類以及根元類)時,調用的是類方法:+ (Class)class,返回的結果為其本身
可以在ViewController
通過簡單代碼驗證一下
//currentClass現在是類對象
Class currentClass = [self class];
//都指向實例對象isa指定的類對象
NSLog(@"currentClass = %p getClass=%p",currentClass ,object_getClass(self));
//class指向類對象本身 getClass指向類對象isa指向元類
NSLog(@"currentClass = %p getClass=%p",[currentClass class],object_getClass(currentClass));
const char *getClassName = object_getClassName(currentClass);
//實例對象指向類,類執行元類,元類指向根元類,根元類指向自己
for (int i = 1; i < 5; i++) {
NSLog(@"Following the isa pointer %d times gives %p %@---%s", i, currentClass,currentClass,getClassName);
currentClass = object_getClass(currentClass);
getClassName = object_getClassName(currentClass);
}
輸出結果如下:
currentClass = 0x10ab29198 getClass=0x10ab29198
currentClass = 0x10ab29198 getClass=0x10ab291c0
Following the isa pointer 1 times gives 0x10ab29198 ViewController---ViewController
Following the isa pointer 2 times gives 0x10ab291c0 ViewController---NSObject
Following the isa pointer 3 times gives 0x10b819e58 NSObject---NSObject
Following the isa pointer 4 times gives 0x10b819e58 NSObject---NSObject
參考
Objective-C的內省(Introspection)小結
10:RunLoop的作用是什么?它的內部工作機制了解么?(最好結合線程和內存管理來說)
這一塊平時用的比較少,了解不是很多。其有時間真的好好靜下心來看一下相關東西了。
字面意思是“消息循環、運行循環”,runloop內部實際上就是一個do-while循環,它在循環監聽著各種事件源、消息,對他們進行管理并分發給線程來執行。
線程和 RunLoop 之間是一一對應的。
運行機制從官方文檔說明
翻譯過來如下:
1.通知觀察者將要進入運行循環。
2.通知觀察者將要處理計時器。
3.通知觀察者任何非基于端口的輸入源即將觸發。
4.觸發任何準備觸發的基于非端口的輸入源。
5.如果基于端口的輸入源準備就緒并等待觸發,請立即處理該事件。轉到第9步。
6.通知觀察者線程即將睡眠。
7.將線程置于睡眠狀態,直到發生以下事件之一:
- 事件到達基于端口的輸入源。
- 計時器運行。
- 為運行循環設置的超時值到期。
- 運行循環被明確喚醒。
8.通知觀察者線程被喚醒。
9.處理待處理事件。
- 如果觸發了用戶定義的計時器,則處理計時器事件并重新啟動循環。轉到第2步。
- 如果輸入源被觸發,則傳遞事件。
- 如果運行循環被明確喚醒但尚未超時,請重新啟動循環。轉到第2步。
10.通知觀察者運行循環已退出。
這里借用一下這里的圖片
RunLoop_1.png
參考
深入理解RunLoop
關于Runloop的原理探究及基本使用
11:談談消息轉發機制實現
先會調用
objc_msgSend
方法,首先在Class中的緩存查找IMP,沒有緩存則初始化緩存。如果沒有找到,則向父類的Class查找。如果一直查找到根類仍舊沒有實現,則執行消息轉發。
1、調用resolveInstanceMethod:
方法。允許用戶在此時為該Class動態添加實現。如果有實現了,則調用并返回YES,重新開始objc_msgSend流程。這次對象會響應這個選擇器,一般是因為它已經調用過了class_addMethod。如果仍沒有實現,繼續下面的動作。
2、調用forwardingTargetForSelector:
方法,嘗試找到一個能響應該消息的對象。如果獲取到,則直接把消息轉發給它,返回非nil對象。否則返回nil,繼續下面的動作。注意這里不要返回self,否則會形成死循環。
3、調用methodSignatureForSelector:
方法,嘗試獲得一個方法簽名。如果獲取不到,則直接調用doesNotRecognizeSelector拋出異常。如果能獲取,則返回非nil;傳給一個NSInvocation并傳給forwardInvocation:
。
4、調用forwardInvocation:
方法,將第三步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這里面了,并返回非nil。
5、調用doesNotRecognizeSelector
:,默認的實現是拋出異常。如果第三步沒能獲得一個方法簽名,執行該步驟 。
參考:
Objective-C 消息發送與轉發機制原理
深入淺出理解消息的傳遞和轉發機制
-----------------------------------------未完待續-----------------------------------