面試驅動技術合集(初中級iOS開發),關注倉庫,及時獲取更新 Interview-seriesgit
Category 相關的問題通常初中級問的比較多,通常最深的就問到關聯對象,本文把比較常見的 Category 的問題都羅列解決了一下,若是還有其餘常見的 Category 的試題歡迎補充~github
runtime
動態將分類的方法合併到類對象、元類對象中使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MNPerson+Test.m
函數,生產一個cpp文件,窺探其底層結構(編譯狀態)面試
struct _category_t {
//宿主類名稱 - 這裏的MNPerson
const char *name;
//宿主類對象,裏面有isa
struct _class_t *cls;
//實例方法列表
const struct _method_list_t *instance_methods;
//類方法列表
const struct _method_list_t *class_methods;
//協議列表
const struct _protocol_list_t *protocols;
//屬性列表
const struct _prop_list_t *properties;
};
//_class_t 結構
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
複製代碼
category_t
@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
/* 二維數組( **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;//宿主類,分類的總數
bool fromBundle = NO;
while (i--) {//倒序遍歷,最早訪問最後編譯的分類
// 獲取某一個分類
auto& entry = cats->list[i];
// 分類的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//最後編譯的分類,最早添加到分類數組中
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 - 從新分配內存 - 擴容了
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//memmove,內存挪動
//array()->lists 原來的方法列表
memmove(array()->lists + addedCount,
array()->lists,
oldCount * sizeof(array()->lists[0]));
//memcpy - 將分類的方法列表 copy 到原來的方法列表中
memcpy(array()->lists,
addedLists,
addedCount * sizeof(array()->lists[0]));
}
...
}
複製代碼
畫圖分析就是數組
Framework
的私有方法公開runtime
, 講Category的數據,
- 類第一次加載進內存的時候,會調用
+ load
方法,無需導入,無需使用- 每一個類、分類的
+ load
在程序運行過程當中只會執行一次+ load
走的不是消息發送的objc_msgSend
調用,而是找到+ load
函數的地址,直接調用
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方法(按照編譯順序,調用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
// 遞歸調用,先將父類添加到load方法列表中,再將本身加進去
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
複製代碼
+ load
函數
+ load
函數
實驗證實:宿主類先調用,分類再調用安全
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
走的是消息發送的objc_msgSend
調用
/*父類*/
@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
---------------------------------------------
問題出現:如下會輸出什麼結果
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MNTeacher alloc];
[MNStudent alloc];
}
return 0;
}
複製代碼
結果以下:bash
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? 爲啥打印三次呢數據結構
原理分析:iphone
initialize
在類第一次接收消息的時候會調用,OC裏面的 [ xxx ]
調用均可以當作 objc_msgSend
,因此這時候,[MNTeacher alloc]
其實內部會調用 [MNTeacher initialize]
initialize
調用的時候,要先實現本身父類的 initialize
方法,第一次調用的時候,MNPerson
沒被使用過,因此未被初始化,要先調用一下父類的 [MNPerson initialize]
,輸出第一個MNPerson + initialize
MNPerson
調用了 initialize
以後,輪到MNTeacher
類本身了,因爲他內部沒有實現 initialize
方法,因此調用父類的initialize
, 輸出第二個MNPerson + initialize
[MNStudent alloc]
,內部也是調用 [MNStudent initialize]
, 而後判斷得知 父類MNPerson
類調用過initialize
了,所以調用自身的就夠了,因爲他和MNTeacher
同樣,也沒實現initialize
方法,因此同理調用父類的[MNPerson initialize]
,輸出第3個MNPerson + initialize
load
or initialize
方法,再調用本身自己的;objc_msgSend
/*父類*/
@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");
}
------------------------------------
問題出現:如下會輸出什麼結果?
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MNTeacher load];
}
return 0;
}
複製代碼
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 也有了?函數
解釋:佈局
MNPerson + initialize
,由於是MNTeacher
的調用,因此會先讓父類MNPerson
調用一次initialize
,輸出第一個 MNPerson + initialize
MNPerson + initialize
, MNTeacher
自身調用,因爲他本身沒有實現 initialize
, 調用父類的initialize
, 輸出第二個 MNPerson + initialize
MNTeacher + load
可能其實有點奇怪,不是說 load
只會加載一次嗎,並且他還不走 objc_msgSend
嗎,怎麼還能調用這個方法?
load
方法是系統調的,這時候不走 objc_msgSend
[MNTeacher load]
啊,這個就是objc_msgSend(MNTeacher,@selector(MNTeacher)),這就跑到MNTeacher + load
裏了!load
函數,可是,仍是能夠調用的!這道題實際上考的就是關聯對象
若是是普通類聲明生命屬性的話
@interface MNPerson : NSObject
@property (nonatomic, copy)NSString *property;
@end
複製代碼
上述代碼系統內部會自動三件事:
get
方法 - (NSString *)property
set
方法 - (void)setProperty:(NSString *)property
@implementation MNPerson{
NSString *_property;
}
- (void)setProperty:(NSString *)property{
_property = property;
}
- (NSString *)property{
return _property;
}
@end
複製代碼
分類也是能夠添加屬性的 - 類結構裏面,有個properties
列表,裏面就是 存放屬性的;
分類裏面,生成屬性,只會生成方法的聲明,不會生成成員變量 && 方法實現!
人工智障翻譯:實例變量不能放在分類中
因此:
不能直接給category 添加成員變量,可是能夠間接實現分類有成員變量的效果(效果上感受像成員變量)
@interface MNPerson (Test)
@property (nonatomic, assign) NSInteger age;
@end
@implementation MNPerson (Test)
@end
複製代碼
person.age = 10
等價於 [person setAge:10]
,因此證實了,給分類聲明屬性以後,並無添加其對應的實現!
objc_setAssociatedObject Api
objc_setAssociatedObject( <#id _Nonnull object#>, (對象)
<#const void * _Nonnull key#>,(key)
<#id _Nullable value#>,(關聯的值)
<#objc_AssociationPolicy policy#>)(關聯策略)
複製代碼
關聯策略,等價於屬性聲明
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屬性,默認聲明是@property (nonatomic, assign) NSInteger age;
,就是 assign,因此這裏選擇OBJC_ASSOCIATION_ASSIGN
取值
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
複製代碼
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;
}
複製代碼
緣由,關聯的對象是person,關聯的value是 test,test變量 出了他們的
{}
做用域以後,就會銷燬; 此時經過key 找到 對應的對象,訪問對象內部的value,由於test變量已經銷燬了,因此程序崩潰了,這也說明了 => 內部 test 對 value是強引用!
在分類中,由於類的實例變量的佈局已經固定,使用 @property 已經沒法向固定的佈局中添加新的實例變量(這樣作可能會覆蓋子類的實例變量),因此咱們須要使用關聯對象以及兩個方法來模擬構成屬性的三個要素。
引用自 關聯對象 AssociatedObject 徹底解析
實現關聯對象技術的核心對象有
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;
}
複製代碼
以關聯對象代碼爲例:
objc_setAssociatedObject(obj, @selector(key), @"hello world", OBJC_ASSOCIATION_COPY_NONATOMIC);
複製代碼
AssociationsManager
中ObjcAssociation
對象,關聯的 value
就放在 ObjcAssociation
內AssociationsManager
管理並在 AssociationsHashMap
存儲ObjectAssociationMap
以鍵值對的形式存儲在 AssociationsHashMap
中ObjectAssociationMap
則是用於存儲關聯對象的數據結構has_assoc
指示對象是否含有關聯對象AssociationsManager
內部有一持有一個_lock
,他實際上是一個spinlock_t(自旋鎖),用來保證AssociationsHashMap
操做的時候,是線程安全的Category
相關的問題通常初中級問的比較多,通常最深的就問到關聯對象
,上面的問題以及解答已經把比較常見的 Category
的問題都羅列解決了一下,若是還有其餘常見的 Category
的試題歡迎補充~
傳言的互聯網寒冬貌似真的來臨了,在這種環境下,沒法得知公司是否不裁人,仍是讓本身💪起來!19年的 銅三鐵四 從明天就要開始拉開帷幕了,也但願近期找工做的iOS們能找到一份滿意的工做,看下寒冬下,iOS開發是否是叕沒人要了~
本文基於 MJ老師 的基礎知識之上,結合了包括 draveness 在內的一系列大神的文章總結的,若是不當之處,歡迎討論~
友情演出:小馬哥MJ
參考資料: