關於iOS底層原理的若干解析
作者丨FindCrt
https://www.jianshu.com/p/d2e0dc7bf57f
問題
1.如果讓你實現屬性的weak,如何實現的?
如果讓你來實現屬性的atomic,如何實現?
KVO為什麼要建立一個子類來實現?
類結構體的組成,isa指標指向了什麼?(這裡應該將元類和根元類也說一下)
RunLoop有幾種事件源?有幾種模式?
方法列表的資料結構是什麼?
分類是如何實現的?它為什麼會覆蓋掉原來的方法?
1. weak原理
weak 弱引用的實現方式
https://www.desgard.com/weak/
這篇文章我覺得寫得很好,我用自己的話簡單總結下:
weak是啥?在一個物件被釋放後,指向它的所有weak指標都跟著被設為nil,所以關鍵就是怎麼從這個物件找到所有指向它的weak指標。
系統使用一張表,用物件的地址做key,值是物件的引用計數和weak指標表。
在類似__weak SomeClass *obj = otherObj這種的時候,呼叫storeWeak方法把新指標obj和物件otherObj關聯起來,實際乾的就是:
使用指標獲取舊的物件,在使用舊物件獲取舊物件的weak表,把指標從就舊物件的weak表裡移除
使用新物件獲取新物件的weak表,把指標加入到weak表裡
2.實現atomic
stackoverflow的這個問題很好
https://stackoverflow.com/questions/588866/whats-the-difference-between-the-atomic-and-nonatomic-attributes/589348#589348
簡單說,在屬性的getter/setter實現裡,先加鎖然後再對變數進行訪問
- (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_;
}
}
發散下
首先這樣做就會加大開銷,因為開鎖解鎖
然後這樣做,實際很多時候並不能保證執行緒同步的作用,除了上面的stackoverflow問題裡的第一個答案
https://stackoverflow.com/questions/588866/whats-the-difference-between-the-atomic-and-nonatomic-attributes/589392#589392
提到的firstname+secondname的例子,我可以舉一個:比如倉庫裡有5袋米,然後10個人去拿,每個人就相當於每個執行緒,每個執行緒先要check是否還有米,然後決定去拿use。atomic只能保證你check的時候是獨立的,use的時候也是獨立的,這樣可能出現什麼?5人check完,第一個人還沒有use,那麼第6個人check的時候,他以為還有5袋米,然後他也去拿,最後結果就是米的數量變成了負數。
簡單說,就是check和use要正整體加鎖:
lock->check->use->unlock
而atomic是在屬性內部實現的加鎖,即相當於:
lock->check->unlock->可能其他執行緒插入進來...->lock->use->unlock。
然後提到@synchronized
簡單說:
@synchronized(obj) {
// do work
}
也是用一張雜湊表,在進入這個程式碼塊的時候,使用obj這個物件獲取對應的遞迴鎖,然後加鎖,在出程式碼塊的時候解鎖。所以這是以obj的地址為唯一性的鎖。
3. KVO的原理
原理參考
https://www.jianshu.com/p/829864680648
實現一個自己的KVO參考
https://tech.glowing.com/cn/implement-kvo/
在你給物件a設定觀察者之後,假設a的型別為ClassA,那麼會從ClassA臨時建一個子類subClassA,然後重寫你觀察的那個屬性的方法,把物件a型別改成這個子類subClassA。
修改子類的方法使用了runtime裡的isa指標的作用
回到問題,為什麼要實現一個子類?
重寫屬性,是怎麼重寫的?比如setName會變成:
void setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
也就是通過willChangeValueForKey和didChangeValueForKey來通知外界的,所以你必須要重寫原本的setter方法,否則外界不會收到訊息
那麼重寫就有兩種選擇:改本類和改子類。如果改了本類,就會汙染本類的所有其他的物件的方法
本來我還想到重寫的方法會被反覆重寫,導致willChangeValueForKey反覆巢狀,但想這個是可以通過設定表示來避免的,比如在類裡建個表儲存KVO重寫的方法
其實這裡是一個很好的思路,我見過使用method swizzling導致類的其他地方被汙染的,可以像KVO裡一樣,自動建立一個子類,然後就你當前的物件方法被修改了,這樣你就不用擔心其他地方會因為方法篡改而導致位置bug
4. isa指標的問題
看這個圖就好了:
好,下一題!-_-
5. RunLoop
深入理解RunLoop,看這篇就好了
https://blog.ibireme.com/2015/05/18/runloop/
mode有幾種:公開的有kCFRunLoopDefaultMode和UITrackingRunLoopMode後一種在scrollView滾動的時候會切換到。這裡會牽扯到一個經典考題:滾動導致NSTimer不起作用的問題。上面的文章裡有說明白。
事件有:source、timer和observer
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如@"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
6. 方法列表的結構
先看類的結構:
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這個就是方法列表了,首先這裡有個不易發現的知識點:為什麼methodLists是指標的指標而不是指標?
這個問題裡的答案說了一些,
https://stackoverflow.com/questions/8847146/whats-is-methodlists-attribute-of-the-structure-objc-class-for
簡單說:
objc_method_list *代表一個方法鏈,按理說對於類來說,這個結構就足夠了,objc_method_list **這個代表n條方法鏈,其實是因為Category才會這樣。
在合併Category和類的時候,就可以把Category的方法直接放進來,而不用修改原來的方法鏈。
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);
個人認為這樣是為了:
保持各個方法表的獨立,比如category定義了和類本身同樣的方法,可以共存
修改起來方便些,如果只有一個表,就得增加和刪除一大堆的節點,而且還得維護那些節點是category的,哪些是類的。
然後是objc_method_list的結構:
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的開原始碼
https://opensource.apple.com/source/objc4/objc4-208/runtime/objc-class.m.auto.html
(這幾個問題其實都是對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;
}
上面這個函式是從裡objc_method_list找到對應的Method,可以看出方法儲存在method_list裡面。沒看程式碼前,我以為是objc_method_list實際是連結串列的一個節點,每個method_list只儲存一個方法,然後用obsolete連線下一個方法。
7. Category的原理
參考這篇
https://tech.meituan.com/DiveIntoCategory.html
把category的方法、屬性和協議都和原有類合併;
對於屬性和協議,把連結串列銜接起來就好了
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 **)裡,然後把類原本的方法列表放進來
// Copy old methods to the method list array
for (i = 0; i < oldCount; i++) {
newLists[newCount++] = oldLists[i];
}
所以為什麼會覆蓋的問題就得到了解決:並不是覆蓋,而是在類本身的方法列表放到了後面,從而被滯後隱藏了。其實也可以猜得到,不可能把原本類的方法去掉,否則原本方法就丟了,而現在這樣,在category移除後,原本類的方法又可以暴露出來了。
關於category,有個在靜態庫的載入問題,這篇回答講得非常好。https://stackoverflow.com/questions/2567498/objective-c-categories-in-static-library/22264650#22264650
簡單說就是category不是編譯器用來確認載入的標識
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全部載入。
其他的一些相關問題
自動釋放池的原理:
http://blog.leichunfeng.com/blog/2015/05/31/objective-c-autorelease-pool-implementation-principle/#jtss-tsina
在開始的時候,建立一個AutoreleasePoolPage型別的雙向連結串列,它會儲存所有使用__autoreleasing標記的物件(MRC時直接呼叫autoRelease方法),實際就是呼叫了下面的方法,建立一個新節點加進去
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結束後,對每個物件release。
Associated Objects的原理,同樣使用雜湊表。
http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/
對於一些除錯,用lldb可以達到特殊效果,參考這篇
https://blog.csdn.net/baihuaxiu123/article/details/51316510
推薦↓↓↓
長
按
關
注
涵蓋:程式設計師大咖、原始碼共讀、程式設計師共讀、資料結構與演算法、黑客技術和網路安全、大資料科技、程式設計前端、Java、Python、Web程式設計開發、Android、iOS開發、Linux、資料庫研發、幽默程式設計師等。