RunTime應(yīng)用實(shí)例:MustOverride

一、常用做法

親,我的簡(jiǎn)書(shū)已不再維護(hù)和更新了,所有文章都遷移到了我的個(gè)人博客:https://mikefighting.github.io/,歡迎交流。

在IOS開(kāi)發(fā)中,我們的基類往往會(huì)寫(xiě)一些空方法,然后讓子類去實(shí)現(xiàn),基類控制主要流程(這其實(shí)就是模板方法模式),這時(shí)我們往往這樣寫(xiě):

- (void)mustBeOverriddenMethod {

[NSException raise:@"Method did not be overridden" format:@"you must override this method in the subclass"];

}

這樣該方法如果直接被父類調(diào)用就會(huì)報(bào)異常,并且提示一定要被子類所覆蓋。但是該方法存在如下弊端:

  1. 該方法一定要被調(diào)用才可以報(bào)異常,如果子類沒(méi)有調(diào)用該方法,也沒(méi)有覆蓋該方法,父類在某些特定的情況下才調(diào)用該方法,那么就會(huì)出錯(cuò)。
  2. 不可以在該方法內(nèi)部做一個(gè)基本的實(shí)現(xiàn),然后被子類繼承并且調(diào)用[super mustBeOverriddenMethod]
  3. 如果項(xiàng)目中存在一個(gè)子類,但是暫時(shí)沒(méi)有用到,并且其沒(méi)有覆寫(xiě)這個(gè)方法,那么沒(méi)有提示。以后其他人用這個(gè)類,很可能就會(huì)出錯(cuò)。

二、優(yōu)雅的做法及疑問(wèn)

以上這些問(wèn)題都可以通過(guò)MustOverride框架來(lái)實(shí)現(xiàn)。
先來(lái)看下其用法,然后我們逐步分析其實(shí)現(xiàn)方式。
只要在父類需要被實(shí)現(xiàn)的方法內(nèi)容添加一個(gè)宏:SUBCLASS_MUST_OVERRIDE即可:

   - (void)someMethod {
 
    SUBCLASS_MUST_OVERRIDE;
}  

這樣就可以了,并且更加神奇的是:

  1. 沒(méi)有類調(diào)用該方法也可以報(bào)異常。

  2. 就算子類沒(méi)有被用到也會(huì)報(bào)異常。

  3. 父類中可以做簡(jiǎn)單的實(shí)現(xiàn),子類可以調(diào)用super來(lái)擴(kuò)展該實(shí)現(xiàn)。
    這時(shí)你可能產(chǎn)生如下疑問(wèn):

  4. 這個(gè)類沒(méi)有用到為啥可以報(bào)異常?

  5. 它是怎樣找到這個(gè)類的被標(biāo)記了SUBCLASS_MUST_OVERRIDE的方法的?

三、對(duì)問(wèn)題的剖析

一切都要從這個(gè)宏說(shuō)起,進(jìn)入宏的定義可以發(fā)現(xiàn):

     #define SUBCLASS_MUST_OVERRIDE __attribute__((used, section("__DATA,MustOverride" \
))) static const char *__must_override_entry__ = __func__

是不是感覺(jué)有些長(zhǎng)?我們可以將該宏拆分:

  #define SUBCLASS_MUST_OVERRIDE static const char *__must_override_entry__ = __func__
  
   __attribute__((used, section("__DATA, MustOverride" )))   

首先定義了一個(gè)靜態(tài)常量指針__must_override_entry__,這個(gè)指針指向__func__,也就是該宏所在方法的方法名。然后利用__attribute__(編譯器指令,可以在聲明時(shí)做一些錯(cuò)誤檢查,或者一些優(yōu)化),將其放入指定的section中(關(guān)于section的定義會(huì)在后續(xù)章節(jié)中加以說(shuō)明),我們可以在loader.h中看到section是這樣一個(gè)結(jié)構(gòu)體:

struct section { /* for 32-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint32_t    addr;       /* memory address of this section */
    uint32_t    size;       /* size in bytes of this section */
    uint32_t    offset;     /* file offset of this section */
    uint32_t    align;      /* section alignment (power of 2) */
    uint32_t    reloff;     /* file offset of relocation entries */
    uint32_t    nreloc;     /* number of relocation entries */
    uint32_t    flags;      /* flags (section type and attributes)*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
};

關(guān)于used的用法我們要到ARM的指令說(shuō)明中查詢

ARM中關(guān)于used的說(shuō)明

從上面可以看出,used的意思是告訴編譯器該靜態(tài)變量要在該對(duì)象文件中被保留(盡管該變量是沒(méi)有被引用的)。被標(biāo)注的靜態(tài)變量將會(huì)按照聲明的順序,放到指定的一個(gè)section中。使用__attribute__((section("name")))可以指明該section.
那么放到section中的靜態(tài)變量是怎樣被使用的呢?
我們可以看到在load方法中,其調(diào)用了CheckOverrides函數(shù),也就是在該類加載到Runtime中的時(shí)候就被調(diào)用,不論其是否被使用。

Dl_info info;
dladdr((const void *)&CheckOverrides, &info);

const MustOverrideValue mach_header = (MustOverrideValue)info.dli_fbase;
const MustOverrideSection *section = GetSectByNameFromHeader((void *)mach_header, "__DATA", "MustOverride");
if (section == NULL) return;

NSMutableArray *failures = [NSMutableArray array];
for (MustOverrideValue addr = section->offset; addr < section->offset + section->size; addr += sizeof(const char **))
{
    NSString *entry = @(*(const char **)(mach_header + addr));
    NSArray *parts = [[entry substringWithRange:NSMakeRange(2, entry.length - 3)] componentsSeparatedByString:@" "];
    NSString *className = parts[0];
    NSRange categoryRange = [className rangeOfString:@"("];
    if (categoryRange.length)
    {
        className = [className substringToIndex:categoryRange.location];
    }

    BOOL isClassMethod = [entry characterAtIndex:0] == '+';
    Class cls = NSClassFromString(className);
    SEL selector = NSSelectorFromString(parts[1]);

    for (Class subclass in SubclassesOfClass(cls))
    {
        if (!ClassOverridesMethod(isClassMethod ? object_getClass(subclass) : subclass, selector))
        {
            [failures addObject:[NSString stringWithFormat:@"%@ does not implement method %c%@ required by %@",
                                 subclass, isClassMethod ? '+' : '-', parts[1], className]];
        }
    }
}

從中可以看到其從Dl_info中獲取了section,
什么是Dl_info,dladdr?我們要從Linux指令集中去查找,

Linux中關(guān)于dladdr的說(shuō)明

從其中的解釋可以看出來(lái),dladdr可以用來(lái)確定addr指明的地址是否存在于公用的對(duì)象中,這些對(duì)象是被調(diào)用程序所加載的。如果存在那么dladdr會(huì)返回公用對(duì)象及重疊addr的表示。該信息被封裝到了Dl_info結(jié)構(gòu)體中。取出Dl_info結(jié)構(gòu)體中的dli_fbase,然后調(diào)用getsectbynamefromheader_64,就可以獲取之前存儲(chǔ)數(shù)據(jù)的section。然后遍歷該section以找到所有被標(biāo)識(shí)的方法。接下來(lái)利用RunTime找到所有的子類:

  static NSArray *SubclassesOfClass(Class baseClass)
{
    static Class *classes;
    static unsigned int classCount;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      classes = objc_copyClassList(&classCount); // 獲取項(xiàng)目中所有用到的類
    });
    
NSMutableArray *subclasses = [NSMutableArray array];
for (unsigned int i = 0; i < classCount; i++)
{
    Class cls = classes[i];
    Class superclass = cls;
    while (superclass)
    {
        if (superclass == baseClass)
        {
            [subclasses addObject:cls];
            break;
        }
        superclass = class_getSuperclass(superclass);
    }
}
return subclasses;
 }

判斷某個(gè)類是否覆蓋了方法:

  static BOOL ClassOverridesMethod(Class cls, SEL selector)
{
    unsigned int numberOfMethods;
    Method *methods = class_copyMethodList(cls, &numberOfMethods);
    for (unsigned int i = 0; i < numberOfMethods; i++)
    {
        if (method_getName(methods[i]) == selector)
        {
            free(methods);
            return YES;
        }
    }
    free(methods);
    return NO;
}

如果沒(méi)有覆蓋則報(bào)異常。
小結(jié):MustOverrid在編譯期利用__attribute__((used, section("__DATA, MustOverride" )))來(lái)將方法名放到section中,然后在文件加載到runtime的時(shí)候找到這個(gè)section,進(jìn)而找到對(duì)應(yīng)地方法,找到所有的子類,利用runtime判斷其是否覆蓋了父類的方法。

附:

關(guān)于load方法的幾點(diǎn)說(shuō)明:
在類或者分類被加載到Runtime的時(shí)候,會(huì)觸發(fā)load方法;并且只會(huì)在第一次被加載的時(shí)候被調(diào)用,所以只會(huì)調(diào)用一次。
load方法的調(diào)用順序:

  1. 父類先調(diào)用+load方法,然后子類再調(diào)用。
  2. 分類調(diào)用+load方法要晚于原類。

延伸閱讀

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0474e/BABHIIEF.html
http://tech.meituan.com/DiveIntoCategory.html
http://man7.org/linux/man-pages/man3/dladdr.3.html
https://www.bignerdranch.com/blog/inside-the-bracket-part-5-runtime-api/
http://nshipster.com/attribute/

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

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,042評(píng)論 0 9
  • 前言 2000年,伊利諾伊大學(xué)厄巴納-香檳分校(University of Illinois at Urbana-...
    星光社的戴銘閱讀 16,274評(píng)論 8 180
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,331評(píng)論 0 7
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,885評(píng)論 33 466
  • 一、概述 Objective-C語(yǔ)言是一門動(dòng)態(tài)語(yǔ)言,它將很多靜態(tài)語(yǔ)言在編譯和鏈接期所做的事推遲到運(yùn)行時(shí)處理。這種動(dòng)...
    Fly晴天里Fly閱讀 1,292評(píng)論 0 6

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