面試驅(qū)動技術(shù)合集(初中級iOS開發(fā)),關(guān)注倉庫,及時獲取更新 Interview-series
I. Category
Category相關(guān)面試題
- Category實(shí)現(xiàn)原理?
- 實(shí)際開發(fā)中,你用Category做了哪些事?
- Category能否添加成員變量,如果可以,如何添加?
- load 、initialize方法的區(qū)別是什么,他們在category中的調(diào)用順序?以及出現(xiàn)繼承時他們之間的調(diào)用過程?
- Category 和 Class Extension的區(qū)別是什么?
- 為什么分類會“覆蓋”宿主類的方法?
1.Category的特點(diǎn)
- 運(yùn)行時決議
- 通過
runtime動態(tài)將分類的方法合并到類對象、元類對象中 - 實(shí)例方法合并到類對象中,類方法合并到元類對象中
- 通過
- 可以為系統(tǒng)類添加分類
2.分類中可以添加哪些內(nèi)容
- 實(shí)例方法
- 類方法
- 協(xié)議
- 屬性
分類中原理解析
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MNPerson+Test.m 函數(shù),生產(chǎn)一個cpp文件,窺探其底層結(jié)構(gòu)(編譯狀態(tài))
struct _category_t {
//宿主類名稱 - 這里的MNPerson
const char *name;
//宿主類對象,里面有isa
struct _class_t *cls;
//實(shí)例方法列表
const struct _method_list_t *instance_methods;
//類方法列表
const struct _method_list_t *class_methods;
//協(xié)議列表
const struct _protocol_list_t *protocols;
//屬性列表
const struct _prop_list_t *properties;
};
//_class_t 結(jié)構(gòu)
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
- 每個分類都是獨(dú)立的
- 每個分類的結(jié)構(gòu)都一致,都是
category_t
函數(shù)轉(zhuǎn)換
@implementation MNPerson (Test)
- (void)test{
NSLog(@"test - rua~");
}
@end
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
/* 二維數(shù)組( **mlists => 兩顆星星,一個)
[
[method_t,],
[method_t,method_t],
[method_t,method_t,method_t],
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;//宿主類,分類的總數(shù)
bool fromBundle = NO;
while (i--) {//倒序遍歷,最先訪問最后編譯的分類
// 獲取某一個分類
auto& entry = cats->list[i];
// 分類的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//最后編譯的分類,最先添加到分類數(shù)組中
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 核心:將所有分類的對象方法,附加到類對象的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
//realloc - 重新分配內(nèi)存 - 擴(kuò)容了
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//memmove,內(nèi)存挪動
//array()->lists 原來的方法列表
memmove(array()->lists + addedCount,
array()->lists,
oldCount * sizeof(array()->lists[0]));
//memcpy - 將分類的方法列表 copy 到原來的方法列表中
memcpy(array()->lists,
addedLists,
addedCount * sizeof(array()->lists[0]));
}
...
}
畫圖分析就是
3.實(shí)際開發(fā)中,你用Category做了哪些事?
- 聲明私有方法
- 分解體積龐大的類文件
- 把
Framework的私有方法公開
- 把
- 。。。
4.Category實(shí)現(xiàn)原理?
- Category編譯之后,底層結(jié)構(gòu)是category_t,里面存儲著分類的各種信息,包括 對象方法、類方法、屬性、協(xié)議信息
- 分類的在編譯后,方法并不會直接添加到類信息中,而是要在程序運(yùn)行的時候,通過
runtime, 講Category的數(shù)據(jù),
5.為什么分類會“覆蓋”宿主類的方法?
- 其實(shí)不是真正的“覆蓋”,宿主類的同名方法還是存在
- 分類將附加到類對象的方法列表中,整合的時候,分類的方法優(yōu)先放到前面
- OC的函數(shù)調(diào)用底層走的是msg_send() 函數(shù),它做的是方法查找,因?yàn)榉诸惖姆椒▋?yōu)先放在前面,所以通過選擇器查找到分類的方法之后直接調(diào)用,宿主類的方法看上去就像被“覆蓋”而沒有生效
6.Category 和 Class Extension的區(qū)別是什么?
Class Extension(擴(kuò)展)
- 聲明私有屬性
- 聲明私有方法
- 聲明私有成員變量
- 編譯時決議,Category 運(yùn)行時決議
- 不能為系統(tǒng)類添加擴(kuò)展
- 只能以聲明的形式存在,多數(shù)情況下,寄生于宿主類的.m文件中
II. load 、initialize
load實(shí)現(xiàn)原理
- 類第一次加載進(jìn)內(nèi)存的時候,會調(diào)用
+ load方法,無需導(dǎo)入,無需使用- 每個類、分類的
+ load在程序運(yùn)行過程中只會執(zhí)行一次+ load走的不是消息發(fā)送的objc_msgSend調(diào)用,而是找到+ load函數(shù)的地址,直接調(diào)用
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren’t any more
while (loadable_classes_used > 0) {
//先加載宿主類的load方法(按照編譯順序,調(diào)用load方法)
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
// 遞歸調(diào)用,先將父類添加到load方法列表中,再將自己加進(jìn)去
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
調(diào)用順序
- 先調(diào)用宿主類的
+ load函數(shù)- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
- 調(diào)用子類的+load之前會先調(diào)用父類的+load
- 再調(diào)用分類的的
+ load函數(shù)- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
實(shí)驗(yàn)證明:宿主類先調(diào)用,分類再調(diào)用
2019-02-27 17:28:00.519862+0800 load-Initialize-Demo[91107:2281575] MNPerson + load
2019-02-27 17:28:00.520032+0800 load-Initialize-Demo[91107:2281575] MNPerson (Play) + load
2019-02-27 17:28:00.520047+0800 load-Initialize-Demo[91107:2281575] MNPerson (Eat) + load
2019-02-27 17:39:10.354050+0800 load-Initialize-Demo[91308:2303030] MNDog + load (宿主類1)
2019-02-27 17:39:10.354237+0800 load-Initialize-Demo[91308:2303030] MNPerson + load (宿主類2)
2019-02-27 17:39:10.354252+0800 load-Initialize-Demo[91308:2303030] MNDog (Rua) + load (分類1)
2019-02-27 17:39:10.354263+0800 load-Initialize-Demo[91308:2303030] MNPerson (Play) + load(分類2)
2019-02-27 17:39:10.354274+0800 load-Initialize-Demo[91308:2303030] MNPerson (Eat) + load(分類3)
2019-02-27 17:39:10.354285+0800 load-Initialize-Demo[91308:2303030] MNDog (Run) + load(分類4)
Initialize實(shí)現(xiàn)原理
- 類第一次接收到消息的時候,會調(diào)用該方法,需導(dǎo)入,并使用
+ Initialize走的是消息發(fā)送的objc_msgSend調(diào)用
Initialize題目出現(xiàn)
/*父類*/
@interface MNPerson : NSObject
@end
@implementation MNPerson
+ (void)initialize{
NSLog(@"MNPerson + initialize");
}
@end
/*子類1*/
@interface MNTeacher : MNPerson
@end
@implementation MNTeacher
@end
/*子類2*/
@interface MNStudent : MNPerson
@end
@implementation MNStudent
@end
---------------------------------------------
問題出現(xiàn):以下會輸出什么結(jié)果
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MNTeacher alloc];
[MNStudent alloc];
}
return 0;
}
結(jié)果如下:
2019-02-27 17:57:33.305655+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
2019-02-27 17:57:33.305950+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
2019-02-27 17:57:33.306476+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
exo me? 為啥打印三次呢
原理分析:
-
initialize在類第一次接收消息的時候會調(diào)用,OC里面的[ xxx ]調(diào)用都可以看成objc_msgSend,所以這時候,[MNTeacher alloc]其實(shí)內(nèi)部會調(diào)用[MNTeacher initialize] -
initialize調(diào)用的時候,要先實(shí)現(xiàn)自己父類的initialize方法,第一次調(diào)用的時候,MNPerson沒被使用過,所以未被初始化,要先調(diào)用一下父類的[MNPerson initialize],輸出第一個MNPerson + initialize -
MNPerson調(diào)用了initialize之后,輪到MNTeacher類自己了,由于他內(nèi)部沒有實(shí)現(xiàn)initialize方法,所以調(diào)用父類的initialize, 輸出第二個MNPerson + initialize - 然后輪到
[MNStudent alloc],內(nèi)部也是調(diào)用[MNStudent initialize], 然后判斷得知 父類MNPerson類調(diào)用過initialize了,因此調(diào)用自身的就夠了,由于他和MNTeacher一樣,也沒實(shí)現(xiàn)initialize方法,所以同理調(diào)用父類的[MNPerson initialize],輸出第3個MNPerson + initialize
initialize 與 load 的區(qū)別
- load 是類第一次加載的時候調(diào)用,initialize 是類第一次接收到消息的時候調(diào)用,每個類只會initialize一次(父類的initialize方法可能被調(diào)用多次)
- load 和 initialize,加載or調(diào)用的時候,都會先調(diào)用父類對應(yīng)的
loadorinitialize方法,再調(diào)用自己本身的; - load 和 initialize 都是系統(tǒng)自動調(diào)用的話,都只會調(diào)用一次
- 調(diào)用方式也不一樣,load 是根據(jù)函數(shù)地址直接調(diào)用,initialize 是通過
objc_msgSend - 調(diào)用時刻,load是runtime加載類、分類的時候調(diào)用(只會調(diào)用一次)
- 調(diào)用順序:
- load:
- 先調(diào)用類的load
- 先編譯的類,優(yōu)先調(diào)用load
- 調(diào)用子類的load之前,會先調(diào)用父類的load
- 在調(diào)用分類的load
- 先調(diào)用類的load
- initialize:
- 先初始化父列
- 再初始化子類(可能最終調(diào)用的是父類的初始化方法)
- load:
/*父類*/
@interface MNPerson : NSObject
@end
@implementation MNPerson
+ (void)initialize{
NSLog(@"MNPerson + initialize");
}
+ (void)load{
NSLog(@"MNPerson + load");
}
/*子類1*/
@interface MNTeacher : MNPerson
@end
@implementation MNTeacher
+ (void)load{
NSLog(@"MNTeacher + load");
}
/*子類2*/
@interface MNStudent : MNPerson
@end
@implementation MNStudent
+ (void)load{
NSLog(@"MNStudent + load");
}
------------------------------------
問題出現(xiàn):以下會輸出什么結(jié)果?
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MNTeacher load];
}
return 0;
}
答案出現(xiàn)?。。?/h4>
2019-02-27 18:17:12.034392+0800 load-Initialize-Demo[92064:2370496] MNPerson + load
2019-02-27 18:17:12.034555+0800 load-Initialize-Demo[92064:2370496] MNStudent + load
2019-02-27 18:17:12.034569+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
2019-02-27 18:17:12.034627+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034645+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034658+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
2019-02-27 18:17:12.034392+0800 load-Initialize-Demo[92064:2370496] MNPerson + load
2019-02-27 18:17:12.034555+0800 load-Initialize-Demo[92064:2370496] MNStudent + load
2019-02-27 18:17:12.034569+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
2019-02-27 18:17:12.034627+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034645+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034658+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
exo me again!怎么這么多!連load 也有了?
解釋:
- 前三個load不多bb了吧,程序一運(yùn)行,runtime直接將全部的類加載到內(nèi)存中,肯定最先輸出;
- 第一個
MNPerson + initialize,因?yàn)槭?code>MNTeacher的調(diào)用,所以會先讓父類MNPerson調(diào)用一次initialize,輸出第一個MNPerson + initialize - 第二個
MNPerson + initialize,MNTeacher自身調(diào)用,由于他自己沒有實(shí)現(xiàn)initialize, 調(diào)用父類的initialize, 輸出第二個MNPerson + initialize - 最后一個
MNTeacher + load可能其實(shí)有點(diǎn)奇怪,不是說load只會加載一次嗎,而且他還不走objc_msgSend嗎,怎么還能調(diào)用這個方法?- 因?yàn)?!?dāng)類第一次加載進(jìn)內(nèi)存的時候,調(diào)用的
load方法是系統(tǒng)調(diào)的,這時候不走objc_msgSend - 但是,你現(xiàn)在是
[MNTeacher load]啊,這個就是objc_msgSend(MNTeacher,@selector(MNTeacher)),這就跑到MNTeacher + load里了! - 只是一般沒人手動調(diào)用
load函數(shù),但是,還是可以調(diào)用的!
- 因?yàn)?!?dāng)類第一次加載進(jìn)內(nèi)存的時候,調(diào)用的
III. 關(guān)聯(lián)對象AssociatedObject
Category能否添加成員變量,如果可以,如何添加?
這道題實(shí)際上考的就是關(guān)聯(lián)對象
如果是普通類聲明生命屬性的話
@interface MNPerson : NSObject
@property (nonatomic, copy)NSString *property;
@end
上述代碼系統(tǒng)內(nèi)部會自動三件事:
- 幫我們生成一個生成變量_property
- 生成一個
get方法- (NSString *)property - 生成一個
set方法- (void)setProperty:(NSString *)property
@implementation MNPerson{
NSString *_property;
}
- (void)setProperty:(NSString *)property{
_property = property;
}
- (NSString *)property{
return _property;
}
@end
分類也是可以添加屬性的 - 類結(jié)構(gòu)里面,有個properties 列表,里面就是
存放屬性的;
分類里面,生成屬性,只會生成方法的聲明,不會生成成員變量 && 方法實(shí)現(xiàn)!
人工智障翻譯:實(shí)例變量不能放在分類中
所以:
不能直接給category 添加成員變量,但是可以間接實(shí)現(xiàn)分類有成員變量的效果(效果上感覺像成員變量)
@interface MNPerson (Test)
@property (nonatomic, assign) NSInteger age;
@end
@implementation MNPerson (Test)
@end
person.age = 10等價于 [person setAge:10],所以證明了,給分類聲明屬性之后,并沒有添加其對應(yīng)的實(shí)現(xiàn)!
關(guān)聯(lián)對象
objc_setAssociatedObject Api
objc_setAssociatedObject( <#id _Nonnull object#>, (對象)
<#const void * _Nonnull key#>,(key)
<#id _Nullable value#>,(關(guān)聯(lián)的值)
<#objc_AssociationPolicy policy#>)(關(guān)聯(lián)策略)
關(guān)聯(lián)策略,等價于屬性聲明
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
比如這里的age屬性,默認(rèn)聲明是@property (nonatomic, assign) NSInteger age;,就是 assign,所以這里選擇OBJC_ASSOCIATION_ASSIGN
取值
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
面試題 - 以下代碼輸出的結(jié)果是啥
int main(int argc, const char * argv[]) {
@autoreleasepool {
MNPerson *person = [[MNPerson alloc]init];
{
MNPerson *test = [[MNPerson alloc]init];
objc_setAssociatedObject(person,
@"test",
test,
OBJC_ASSOCIATION_ASSIGN);
}
NSLog(@"%@",objc_getAssociatedObject(person, @"test"));
}
return 0;
}
原因,關(guān)聯(lián)的對象是person,關(guān)聯(lián)的value是 test,test變量 出了他們的
{}作用域之后,就會銷毀;
此時通過key 找到 對應(yīng)的對象,訪問對象內(nèi)部的value,因?yàn)閠est變量已經(jīng)銷毀了,所以程序崩潰了,這也說明了 => 內(nèi)部 test 對 value是強(qiáng)引用!
關(guān)聯(lián)對象的本質(zhì)
在分類中,因?yàn)轭惖膶?shí)例變量的布局已經(jīng)固定,使用 @property 已經(jīng)無法向固定的布局中添加新的實(shí)例變量(這樣做可能會覆蓋子類的實(shí)例變量),所以我們需要使用關(guān)聯(lián)對象以及兩個方法來模擬構(gòu)成屬性的三個要素。
引用自 關(guān)聯(lián)對象 AssociatedObject 完全解析
關(guān)聯(lián)對象的原理
實(shí)現(xiàn)關(guān)聯(lián)對象技術(shù)的核心對象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
class AssociationsManager {
static spinlock_t _lock;//自旋鎖,保證線程安全
static AssociationsHashMap *_map;
}
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap>
class ObjectAssociationMap : public std::map<void *, ObjcAssociation>
class ObjcAssociation {
uintptr_t _policy;
id _value;
}
以關(guān)聯(lián)對象代碼為例:
objc_setAssociatedObject(obj, @selector(key), @"hello world", OBJC_ASSOCIATION_COPY_NONATOMIC);
- 關(guān)聯(lián)對象并不是存儲在被關(guān)聯(lián)對象本身的內(nèi)存中的
- 關(guān)聯(lián)對象,存儲在全局的一個統(tǒng)一的
AssociationsManager中 - 關(guān)聯(lián)對象其實(shí)就是
ObjcAssociation對象,關(guān)聯(lián)的value就放在ObjcAssociation內(nèi) - 關(guān)聯(lián)對象由
AssociationsManager管理并在AssociationsHashMap存儲 - 對象的指針以及其對應(yīng)
ObjectAssociationMap以鍵值對的形式存儲在AssociationsHashMap中 -
ObjectAssociationMap則是用于存儲關(guān)聯(lián)對象的數(shù)據(jù)結(jié)構(gòu) - 每一個對象都有一個標(biāo)記位
has_assoc指示對象是否含有關(guān)聯(lián)對象 - 存儲在全局的一個統(tǒng)一的
AssociationsManager內(nèi)部有一持有一個_lock,他其實(shí)是一個spinlock_t(自旋鎖),用來保證AssociationsHashMap操作的時候,是線程安全的
Category 相關(guān)的問題一般初中級問的比較多,一般最深的就問到關(guān)聯(lián)對象,上面的問題以及解答已經(jīng)把比較常見的 Category 的問題都羅列解決了一下,如果還有其他常見的 Category 的試題歡迎補(bǔ)充~
傳言的互聯(lián)網(wǎng)寒冬貌似真的來臨了,在這種環(huán)境下,無法得知公司是否不裁員,還是讓自己??起來!19年的 銅三鐵四 從明天就要開始拉開帷幕了,也希望近期找工作的iOS們能找到一份滿意的工作,看下寒冬下,iOS開發(fā)是不是叕沒人要了~
本文基于 MJ老師 的基礎(chǔ)知識之上,結(jié)合了包括 draveness 在內(nèi)的一系列大神的文章總結(jié)的,如果不當(dāng)之處,歡迎討論~
友情演出:小馬哥MJ
參考資料: