1. 程式人生 > >關於iOS底層原理的若干解析

關於iOS底層原理的若干解析

640?wx_fmt=gif

640?wx_fmt=jpeg

Linux程式設計點選右側關注,免費入門到精通!640?wx_fmt=jpeg

作者丨FindCrt
https://www.jianshu.com/p/d2e0dc7bf57f

640?wx_fmt=gif

問題

1.如果讓你實現屬性的weak,如何實現的?

  • 如果讓你來實現屬性的atomic,如何實現?

  • KVO為什麼要建立一個子類來實現?

  • 類結構體的組成,isa指標指向了什麼?(這裡應該將元類和根元類也說一下)

  • RunLoop有幾種事件源?有幾種模式?

  • 方法列表的資料結構是什麼?

  • 分類是如何實現的?它為什麼會覆蓋掉原來的方法?

640?wx_fmt=gif1. weak原理

weak 弱引用的實現方式

https://www.desgard.com/weak/

這篇文章我覺得寫得很好,我用自己的話簡單總結下:


weak是啥?在一個物件被釋放後,指向它的所有weak指標都跟著被設為nil,所以關鍵就是怎麼從這個物件找到所有指向它的weak指標。

系統使用一張表,用物件的地址做key,值是物件的引用計數和weak指標表。


在類似__weak SomeClass *obj = otherObj這種的時候,呼叫storeWeak方法把新指標obj和物件otherObj關聯起來,實際乾的就是:

使用指標獲取舊的物件,在使用舊物件獲取舊物件的weak表,把指標從就舊物件的weak表裡移除

使用新物件獲取新物件的weak表,把指標加入到weak表裡

640?wx_fmt=gif2.實現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的地址為唯一性的鎖。

640?wx_fmt=gif3. 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

640?wx_fmt=gif4. isa指標的問題

看這個圖就好了:

640?wx_fmt=other

好,下一題!-_-

640?wx_fmt=gif5. 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
...
};

640?wx_fmt=gif6. 方法列表的結構

先看類的結構:

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連線下一個方法。

640?wx_fmt=gif7. 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

 推薦↓↓↓ 

640?wx_fmt=png

涵蓋:程式設計師大咖、原始碼共讀、程式設計師共讀、資料結構與演算法、黑客技術和網路安全、大資料科技、程式設計前端、Java、Python、Web程式設計開發、Android、iOS開發、Linux、資料庫研發、幽默程式設計師等。