關(guān)于iOS底層原理的若干解析

今天看到了這位兄弟的面試題總結(jié)文章:先是程序員,然后才是iOS程序員 — 寫給廣大非科班iOS開發(fā)者的一篇面試總結(jié),里面的問題確實(shí)不錯,所以就查資料學(xué)習(xí)了下,在這給個答案(鏈接-_-),以及一些其他的原理和發(fā)散。

問題

  1. 如果讓你實(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原理

weak 弱引用的實(shí)現(xiàn)方式

這篇文章我覺得寫得很好,我用自己的話簡單總結(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

stackoverflow的這個問題很好

簡單說,在屬性的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的原理

原理參考
實(shí)現(xiàn)一個自己的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"];
}

也就是通過willChangeValueForKeydidChangeValueForKey來通知外界的,所以你必須要重寫原本的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指針的問題

看這個圖就好了:


isa指針圖

好,下一題!-_-

5. RunLoop

深入理解RunLoop,看這篇就好了
mode有幾種:公開的有kCFRunLoopDefaultModeUITrackingRunLoopMode后一種在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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,106評論 0 9
  • 本文載自: http://blog.csdn.net/a316212802/article/details/49...
    MrLuckyluke閱讀 2,594評論 1 7
  • “你為我做了這么多衣服,紹珍,這輩子辛苦你了,我要怎么報答你?” “我不要你報答,我要攢著,攢到下輩子還要你穿” ...
    西渡橋閱讀 509評論 0 2
  • 每一天,忙碌而碌碌無為, 日子在不知不覺中, 過成了無止盡的忙碌, 生活原來的模樣早已暗淡模糊, 我想我是真的累了...
    大神策劃閱讀 914評論 0 0
  • 唐鶴升 看到自己孤立于鐵皮墻內(nèi) 我變得越來越不安 怎么辦 屬于我的時間不多了 時光在我的記憶里 深深地烙下許多曾經(jīng)...
    唐鶴升閱讀 424評論 0 1

友情鏈接更多精彩內(nèi)容