今天看到了這位兄弟的面試題總結(jié)文章:先是程序員,然后才是iOS程序員 — 寫給廣大非科班iOS開發(fā)者的一篇面試總結(jié),里面的問題確實(shí)不錯,所以就查資料學(xué)習(xí)了下,在這給個答案(鏈接-_-),以及一些其他的原理和發(fā)散。
問題
- 如果讓你實(shí)現(xiàn)屬性的weak,如何實(shí)現(xiàn)的?
- 如果讓你來實(shí)現(xiàn)屬性的atomic,如何實(shí)現(xiàn)?
- KVO為什么要創(chuàng)建一個子類來實(shí)現(xiàn)?
- 類結(jié)構(gòu)體的組成,isa指針指向了什么?(這里應(yīng)該將元類和根元類也說一下)
- RunLoop有幾種事件源?有幾種模式?
- 方法列表的數(shù)據(jù)結(jié)構(gòu)是什么?
- 分類是如何實(shí)現(xiàn)的?它為什么會覆蓋掉原來的方法?
1. weak原理
這篇文章我覺得寫得很好,我用自己的話簡單總結(jié)下:
weak是啥?在一個對象被釋放后,指向它的所有weak指針都跟著被設(shè)為nil,所以關(guān)鍵就是怎么從這個對象找到所有指向它的weak指針。
系統(tǒng)使用一張表,用對象的地址做key,值是對象的引用計數(shù)和weak指針表。
在類似__weak SomeClass *obj = otherObj這種的時候,調(diào)用storeWeak方法把新指針obj和對象otherObj關(guān)聯(lián)起來,實(shí)際干的就是:
- 使用指針獲取舊的對象,在使用舊對象獲取舊對象的weak表,把指針從就舊對象的weak表里移除
- 使用新對象獲取新對象的weak表,把指針加入到weak表里
2.實(shí)現(xiàn)atomic
簡單說,在屬性的getter/setter實(shí)現(xiàn)里,先加鎖然后再對變量進(jìn)行訪問
- (UITextField *) userName {
UITextField *retval = nil;
@synchronized(self) {
retval = [[userName retain] autorelease];
}
return retval;
}
- (void) setUserName:(UITextField *)userName_ {
@synchronized(self) {
[userName_ retain];
[userName release];
userName = userName_;
}
}
發(fā)散下
- 首先這樣做就會加大開銷,因為開鎖解鎖
- 然后這樣做,實(shí)際很多時候并不能保證線程同步的作用,除了上面的stackoverflow問題里的第一個答案提到的
firstname+secondname的例子,我可以舉一個:比如倉庫里有5袋米,然后10個人去拿,每個人就相當(dāng)于每個線程,每個線程先要check是否還有米,然后決定去拿use。atomic只能保證你check的時候是獨(dú)立的,use的時候也是獨(dú)立的,這樣可能出現(xiàn)什么?5人check完,第一個人還沒有use,那么第6個人check的時候,他以為還有5袋米,然后他也去拿,最后結(jié)果就是米的數(shù)量變成了負(fù)數(shù)。
簡單說,就是check和use要正整體加鎖:
lock->check->use->unlock
而atomic是在屬性內(nèi)部實(shí)現(xiàn)的加鎖,即相當(dāng)于:
lock->check->unlock->可能其他線程插入進(jìn)來...->lock->use->unlock。
- 然后提到
@synchronized,就也說下它的原理,參考這里。
簡單說:
@synchronized(obj) {
// do work
}
也是用一張哈希表,在進(jìn)入這個代碼塊的時候,使用obj這個對象獲取對應(yīng)的遞歸鎖,然后加鎖,在出代碼塊的時候解鎖。所以這是以obj的地址為唯一性的鎖。
3. KVO的原理
- 在你給對象a設(shè)置觀察者之后,假設(shè)a的類型為ClassA,那么會從ClassA臨時建一個子類subClassA,然后重寫你觀察的那個屬性的方法,把對象a類型改成這個子類subClassA。
- 修改子類的方法使用了runtime里的isa指針的作用
- 回到問題,為什么要實(shí)現(xiàn)一個子類?
- 重寫屬性,是怎么重寫的?比如setName會變成:
void setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
也就是通過willChangeValueForKey和didChangeValueForKey來通知外界的,所以你必須要重寫原本的setter方法,否則外界不會收到消息
- 那么重寫就有兩種選擇:改本類和改子類。如果改了本類,就會污染本類的所有其他的對象的方法
- 本來我還想到重寫的方法會被反復(fù)重寫,導(dǎo)致
willChangeValueForKey反復(fù)嵌套,但想這個是可以通過設(shè)置表示來避免的,比如在類里建個表存儲KVO重寫的方法 - 其實(shí)這里是一個很好的思路,我見過使用method swizzling導(dǎo)致類的其他地方被污染的,可以像KVO里一樣,自動創(chuàng)建一個子類,然后就你當(dāng)前的對象方法被修改了,這樣你就不用擔(dān)心其他地方會因為方法篡改而導(dǎo)致位置bug
4. isa指針的問題
看這個圖就好了:

好,下一題!-_-
5. RunLoop
深入理解RunLoop,看這篇就好了
mode有幾種:公開的有kCFRunLoopDefaultMode和UITrackingRunLoopMode后一種在scrollView滾動的時候會切換到。這里會牽扯到一個經(jīng)典考題:滾動導(dǎo)致NSTimer不起作用的問題。上面的文章里有說明白。
事件有:source、timer和observer
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
6. 方法列表的結(jié)構(gòu)
先看類的結(jié)構(gòu):
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists這個就是方法列表了,首先這里有個不易發(fā)現(xiàn)的知識點(diǎn):為什么methodLists是指針的指針而不是指針?
這個問題里的答案說了一些,簡單說:
objc_method_list *代表一個方法鏈,按理說對于類來說,這個結(jié)構(gòu)就足夠了,objc_method_list **這個代表n條方法鏈,其實(shí)是因為Category才會這樣。
在合并Category和類的時候,就可以把Category的方法直接放進(jìn)來,而不用修改原來的方法鏈。
while (i--) {
//取出category的方法列表
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
//直接放到列的方法列表的列表里,而不修改類本身的方法列表
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);
個人認(rèn)為這樣是為了:
- 保持各個方法表的獨(dú)立,比如category定義了和類本身同樣的方法,可以共存
- 修改起來方便些,如果只有一個表,就得增加和刪除一大堆的節(jié)點(diǎn),而且還得維護(hù)那些節(jié)點(diǎn)是category的,哪些是類的。
然后是objc_method_list的結(jié)構(gòu):
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
這里沒有借鑒,只有自己翻一下runtime的開源代碼(這幾個問題其實(shí)都是對runtime源碼的解析吧)
/* These next three functions are the heart of ObjC method lookup. */
static inline Method _findMethodInList(struct objc_method_list * mlist, SEL sel) {
int i;
if (!mlist) return NULL;
for (i = 0; i < mlist->method_count; i++) {
Method m = &mlist->method_list[i];
if (m->method_name == sel) {
return m;
}
}
return NULL;
}
上面這個函數(shù)是從里objc_method_list找到對應(yīng)的Method,可以看出方法存儲在method_list里面。沒看代碼前,我以為是objc_method_list實(shí)際是鏈表的一個節(jié)點(diǎn),每個method_list只存儲一個方法,然后用obsolete連接下一個方法。
7. Category的原理
- 把category的方法、屬性和協(xié)議都和原有類合并;
- 對于屬性和協(xié)議,把鏈表銜接起來就好了
newproperties = buildPropertyList(NULL, cats, isMeta);
if (newproperties) {
newproperties->next = cls->data()->properties;
cls->data()->properties = newproperties;
}
newprotos = buildProtocolList(cats, NULL, cls->data()->protocols);
if (cls->data()->protocols && cls->data()->protocols != newprotos) {
_free_internal(cls->data()->protocols);
}
cls->data()->protocols = newprotos;
- 對于方法,先把所有category的方法列表都存在列表的列表(method_list_t **)里,然后把類原本的方法列表放進(jìn)來
// Copy old methods to the method list array
for (i = 0; i < oldCount; i++) {
newLists[newCount++] = oldLists[i];
}
所以為什么會覆蓋的問題就得到了解決:并不是覆蓋,而是在類本身的方法列表放到了后面,從而被滯后隱藏了。其實(shí)也可以猜得到,不可能把原本類的方法去掉,否則原本方法就丟了,而現(xiàn)在這樣,在category移除后,原本類的方法又可以暴露出來了。
關(guān)于category,有個在靜態(tài)庫的加載問題,這篇回答講得非常好。簡單說就是category不是編譯器用來確認(rèn)加載的標(biāo)識
Categories are a runtime-only feature, categories aren't symbols like classes or functions and that also means a linker cannot determine if a category is in use or not.
解決方案就是在Other Linker Flags里添加-Objc,-force_load或-all_load來加載,-Objc是所有OC代碼的文件都加載,-force_load指定文件加載,-all_load全部加載。
其他的一些相關(guān)問題
- 自動釋放池的原理:
參考這篇
在開始的時候,創(chuàng)建一個AutoreleasePoolPage類型的雙向鏈表,它會保存所有使用__autoreleasing標(biāo)記的對象(MRC時直接調(diào)用autoRelease方法),實(shí)際就是調(diào)用了下面的方法,創(chuàng)建一個新節(jié)點(diǎn)加進(jìn)去
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
在pool結(jié)束后,對每個對象release。
- Associated Objects的原理,同樣使用哈希表。
- 對于一些調(diào)試,用lldb可以達(dá)到特殊效果,參考這篇