RunTime应用实例:MustOverride

一、常用做法

亲,我的简书已不再维护和更新了,所有文章都迁移到了我的个人博客:https://mikefighting.github.io/,欢迎交流。

在IOS开发中,我们的基类往往会写一些空方法,然后让子类去实现,基类控制主要流程(这其实就是模板方法模式),这时我们往往这样写:

- (void)mustBeOverriddenMethod {

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

}

这样该方法如果直接被父类调用就会报异常,并且提示一定要被子类所覆盖。但是该方法存在如下弊端:

  1. 该方法一定要被调用才可以报异常,如果子类没有调用该方法,也没有覆盖该方法,父类在某些特定的情况下才调用该方法,那么就会出错。
  2. 不可以在该方法内部做一个基本的实现,然后被子类继承并且调用[super mustBeOverriddenMethod]
  3. 如果项目中存在一个子类,但是暂时没有用到,并且其没有覆写这个方法,那么没有提示。以后其他人用这个类,很可能就会出错。

二、优雅的做法及疑问

以上这些问题都可以通过MustOverride框架来实现。
先来看下其用法,然后我们逐步分析其实现方式。
只要在父类需要被实现的方法内容添加一个宏:SUBCLASS_MUST_OVERRIDE即可:

   - (void)someMethod {
 
    SUBCLASS_MUST_OVERRIDE;
}  

这样就可以了,并且更加神奇的是:

  1. 没有类调用该方法也可以报异常。

  2. 就算子类没有被用到也会报异常。

  3. 父类中可以做简单的实现,子类可以调用super来扩展该实现。
    这时你可能产生如下疑问:

  4. 这个类没有用到为啥可以报异常?

  5. 它是怎样找到这个类的被标记了SUBCLASS_MUST_OVERRIDE的方法的?

三、对问题的剖析

一切都要从这个宏说起,进入宏的定义可以发现:

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

是不是感觉有些长?我们可以将该宏拆分:

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

首先定义了一个静态常量指针__must_override_entry__,这个指针指向__func__,也就是该宏所在方法的方法名。然后利用__attribute__(编译器指令,可以在声明时做一些错误检查,或者一些优化),将其放入指定的section中(关于section的定义会在后续章节中加以说明),我们可以在loader.h中看到section是这样一个结构体:

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) */
};

关于used的用法我们要到ARM的指令说明中查询

ARM中关于used的说明

从上面可以看出,used的意思是告诉编译器该静态变量要在该对象文件中被保留(尽管该变量是没有被引用的)。被标注的静态变量将会按照声明的顺序,放到指定的一个section中。使用__attribute__((section("name")))可以指明该section.
那么放到section中的静态变量是怎样被使用的呢?
我们可以看到在load方法中,其调用了CheckOverrides函数,也就是在该类加载到Runtime中的时候就被调用,不论其是否被使用。

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_infodladdr我们要从Linux指令集中去查找

Linux中关于dladdr的说明

从其中的解释可以看出来,dladdr可以用来确定addr指明的地址是否存在于公用的对象中,这些对象是被调用程序所加载的。如果存在那么dladdr会返回公用对象及重叠addr的表示。该信息被封装到了Dl_info结构体中。取出Dl_info结构体中的dli_fbase,然后调用getsectbynamefromheader_64,就可以获取之前存储数据的section。然后遍历该section以找到所有被标识的方法。接下来利用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); // 获取项目中所有用到的类
    });
    
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;
 }

判断某个类是否覆盖了方法:

  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;
}

如果没有覆盖则报异常。
小结:MustOverrid在编译期利用__attribute__((used, section("__DATA, MustOverride" )))来将方法名放到section中,然后在文件加载到runtime的时候找到这个section,进而找到对应地方法,找到所有的子类,利用runtime判断其是否覆盖了父类的方法。

附:

关于load方法的几点说明:
在类或者分类被加载到Runtime的时候,会触发load方法;并且只会在第一次被加载的时候被调用,所以只会调用一次。
load方法的调用顺序:

  1. 父类先调用+load方法,然后子类再调用。
  2. 分类调用+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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9
  • 前言 2000年,伊利诺伊大学厄巴纳-香槟分校(University of Illinois at Urbana-...
    星光社的戴铭阅读 15,859评论 8 180
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,172评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,544评论 33 466
  • 一、概述 Objective-C语言是一门动态语言,它将很多静态语言在编译和链接期所做的事推迟到运行时处理。这种动...
    Fly晴天里Fly阅读 1,196评论 0 6