回答-阿里、字节:一套高效的iOS面试题①(结构模型)

最近工作比较闲,想巩固一下自己的iOS开发基础知识,就回答一下阿里、字节:一套高效的iOS面试题,欢迎各位同行批评斧正!

runtime是iOS开发最核心的知识了,如果下面的问题都解决了,那么对 objc-runtime 的理解已经很深了。
runtime已经开源了,这有一份别人调试好可运行的源码objc-runtime,也可以去官网找objc4,以下回答用到的源码版本是objc4-756.2。

结构模型

1、介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

isa :

1、所有继承于NSObject类的对象,在内存布局中,第一个变量都是isa。

2、在arm64之前,实例对象的isa指向类对象,类对象的isa指向元类对象。

3、在arm64之后,isa经过了优化,采取了共用体的结构,将一个64位的内存数据分开存储了很多的信息,其中的33位才是存储类对象、元类对象的地址值的,可以通过一个位运算取出instance的isa包含的class的地址,取出class的isa包含的meta-class的地址。

union isa_t {
    Class cls;
#if defined(ISA_BITFIELD)
    struct {
      uintptr_t nonpointer        : 1; //0,代表普通的指针,存储着Class、Meta-Class对象的内存地址,1,代表优化过,使用位域存储更多的信息                                    
      uintptr_t has_assoc         : 1; //是否有设置过关联对象,如果没有,释放时会更快
      uintptr_t has_cxx_dtor      : 1; //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
      uintptr_t shiftcls          : 33; //存储着Class、Meta-Class对象的内存地址信息
      uintptr_t magic             : 6;  //用于在调试时分辨对象是否未完成初始化
      uintptr_t weakly_referenced : 1;  //是否有被弱引用指向过,如果没有,释放时会更快
      uintptr_t deallocating      : 1;  //对象是否正在释放
      uintptr_t has_sidetable_rc  : 1;  //里面存储的值是引用计数器减1
      uintptr_t extra_rc          : 19  //引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
    };
#endif
};

对象:

@interface OffcnPerson : NSObject {
    @public
    int _age;
    int _no;
}

  OffcnPerson *person = [[OffcnPerson alloc] init];
  person->_age = 25;
  person->_no = 77123;

以上面的person对象为例,它的内存结构包含它父类的变量isa指针,以及person对象自身的变量_age_no

实际分配的大小应该是所有变量加起来,内存对齐后的大小,对象的底层是使用结构体存储变量的。

结构体的大小必须是结构体中占用内存最大的成员变量的倍数 ,上面的person对象占用内存变量是isa指针,占用8个字节,所以person内存最小得是16个字节。如果再增加一个int _height,变量的总大小是20个字节,实际上会分配24个字节。

类:

class对象在内存中存储的信息主要包括

isa指针、superclass指针、类的属性信息(@property)、类的对象方法信息( instance method)、类的协议信息(Protocol)、类的成员变量信息 (ivar)。

metaclass:

meta-class对象和class对象的内存结构是一样的,在内存中存储的信息主要包括

isa指针、superclass指针、类的属性信息(@property)、类的对象方法信息( instance method)。

2、为什么要设计metaclass

1、如果对象方法、类方法都储存在类对象的结构体中,需要在objc_class的结构中增加一个数组存储类对象方法列表。

窥探struct objc_class的结构.png

2、需要在调用objc_msgSend的时候就需要额外追加一个参数去分辨该次调用的是对象方法还是类方法,而我们现在的objc_msgSend()只接收了(id self, SEL _cmd, ...)这三种参数,第一个self就是消息的接收者,第二个就是方法,后续的...就是各式各样的参数。

如果不加参数,对象方法和类方法同名时就不知道调用的是哪一个了。

如果在objc_msgSend中再添加一个参数标识是对象方法还是类方法,就需要在消息发送机制中进行对象类型和方法类型判断,影响消息发送的效率。

3、把对象方法和类方法耦合在一起不符合设计模式中的单一职责原则,通过增加一个和类对象具有相同结构的metaclass后,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表。

instance的isa指向class,当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用,class的isa指向meta-class,当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用。

3、class_copyIvarList&class_copyPropertyList区别

class_copyPropertyList返回的仅仅是对象类的属性(@property声明的属性),而class_copyIvarList返回类的所有属性(等同于getter+setter+变量)和变量(包括在@interface大括号中声明的变量)。

@interface OffcnStudent ()
{
    int _age;
    int _no;
}

@property (nonatomic,   copy) NSString *name;
@property (nonatomic,   copy) NSString *department;

@end

  
- (void)invokeClass_copyPropertyList {
    unsigned int count =0;
    objc_property_t *properties = class_copyPropertyList(OffcnStudent.class, &count);
    for (int i =0; i<count;i++) {
        objc_property_t property = properties[i];
        //获取属性的名字
        NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        NSLog(@"propertyName:%@",propertyName);
    }
}
//打印
/*
propertyName:name
propertyName:department
*/

- (void)invokeClass_copyIvarList {
    unsigned int count =0;
    Ivar *ivars = class_copyIvarList(OffcnStudent.class, &count);
    for (int i =0; i<count;i++) {
        //获取属性的名字
        NSString *ivarName = [[NSString alloc] initWithCString:ivar_getName(ivars[i]) encoding:NSUTF8StringEncoding];
        NSLog(@"ivarName:%@",ivarName);
    }
}
//打印
/*
ivarName:_age
ivarName:_no
ivarName:_name
ivarName:_department
*/

4、class_rw_tclass_ro_t 的区别

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

class_rw_t* data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
//由此可以看出bits是采用位域的方式存储数据的,其中的某一些内存空间存放的是class_rw_t

class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定。

调用realizeClassWithoutSwift方法时,如果当前的class没有实现初始化,会对rw进行内存分配、将class_ro_t的内容拷贝过去,把rw设置给class的data、设置nextSiblingClass和 firstSubclass属性、然后再将当前类的分类的方法拷贝到rw的methods方法列表中。

当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    class_rw_t *rw;
    Class supercls;
    Class metacls;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;

     //初始化rw,把ro设置给rw
    // Normal class. Allocate writeable class data.
    rw = objc::zalloc<class_rw_t>();
    rw->set_ro(ro);
    rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
    cls->setData(rw); //把rw设置给class的data
    
    // Realize superclass and metaclass, if they aren't already.
    // This needs to be done after RW_REALIZED is set above, for root classes.
    // This needs to be done after class index is chosen, for root metaclasses.
    supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);


    // Update superclass and metaclass in case of remapping
    cls->superclass = supercls;
    cls->initClassIsa(metacls);

    // Reconcile instance variable offsets / layout.
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Set fastInstanceSize if it wasn't set already.
    cls->setInstanceSize(ro->instanceSize);
    
    // Connect this class to its superclass's subclass lists
    if (supercls) {
        addSubclass(supercls, cls); //设置nextSiblingClass和 firstSubclass属性
    } else {
        addRootClass(cls);
    }

    // Attach categories 追加分类
    methodizeClass(cls, previously);

    return cls;
}

5、category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

实现思路:

1、每个分类中有多个方法,根据分类中方法的总大小 + 原来类对象中方法列表的大小,realloc重新分配数组内存空间

2、往后挪动原来类对象方法(类方法)列表的数据,挪动的大小为分类中方法的总大小

3、根据编译顺序把分类中的方法倒序添加的数组中,依次加入到新分配数组的前面

4、根据objc_msgsend去调用方法,先找类对象的方法列表,再通过superclass找到父类的方法列表进行调用

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    bool isMeta = cls->isMetaClass();

    // 为方法列表的二维数组分配内存,大小为分类方法的总大小
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int i = cats->count;
    while (i--) {
        //从分类方法的末尾取出方法
        auto& entry = cats->list[i];
        //根据当前cls是类对象还是元类对象,对应取出对象方法和类方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist; //依次把分类中的方法加入方法列表中
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
}

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount; //分类方法与类中的方法总和
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        //把类中的方法向后挪(分类的方法总数)个位置
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        //把分类中的方法列表拷贝到方法数组的前面
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
}

1、category如何被加载的?

从上面的两段代码可以看出,分类中的方法会被添加到方法列表的前面。

分类中的方法是按照倒序的方法添加到方法的列表前面的。

2、两个category的load方法的加载顺序?

1、首先要搞清楚load方法什么时候调用?load方法是在程序启动runtime加载类、分类的时候就会调用。

2、先调用类的load方法,再调用分类的load方法,load方法的调用不是通过消息发送机制,而是找到函数地址直接调用。

3、分类中的load方法是按照编译的先后顺序加载调用的,具体可以看下面call_category_loads中的实现。

4、先调用父类,再调用子类,先编译先调用,具体可以看下面schedule_class_load的实现。

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more(重复的调用类的+load方法)
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE(然后调用分类的+load)
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

static void call_class_loads(void)
{
    int i;
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
      //找到load方法的地址直接调用
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.(按照正序遍历分类中的load方法,然后调用)
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }
}

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

3、两个category的同名方法的加载顺序?

由于分类的方法是倒序添加到方法列表的前面的,所以后编译的先调用。

在Build Phases 的Compile Sources中可以调整编译的顺序。

6、category & extension区别,能给NSObject添加Extension吗,结果如何?

1、category & extension区别?

categoryextension的主要区别是extension相当于把变量属性的访问权限改为私有了,编译后就己经合并到底层的C++代码中了,category是在运行时才合并到类的方法列表中。

2、能给NSObject添加Extension吗,结果如何?

不能给NSObject添加Extension。Extension里面的变量和属性都是私有的,需要在.m文件中添加,现在拿不到NSObject的源文件,所以无法给NSObject添加Extension。

7、OC的消息转发机制和其他语言的消息机制优劣对比?

源代码 -> 编译链接 -> 运行。

对于C语言来说,编译完之后生成的二进制文件就是当初源代码那个样子。C语言就是当初写的是什么,编译的就是什么,运行的结果也就是什么。运行结果和当初的编译时保持一致的。

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。OC能够在程序的运行中修改之前编译好的一些东西,可以在运行的过程中添加一些方法。

OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数

平时编写的OC代码,底层都是转换成了Runtime API进行调用。

8、在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)。

1、方法查询(消息发送)

1、在方法查询之前会判断receiver(方法调用者)是否为nil,如果为nil就直接退出。

2、receiver通过isa指针找到receiverClass,从receiverClass的cache方法列表中查找selector方法名,找到方法后调用,结束查找。

3、在cache方法列表没找到方法,就去receiverClass的class_rw_t中查找方法,找到方法后调用,结束查找。并将该方法缓存到receiverClass的cache方法列表中。

4、在receiverClass的class_rw_t没找到方法,就从receiverClass的superClass的cache方法列表查找,找到方法后调用,结束查找。并将该方法缓存到receiverClass的cache方法列表中。

5、在receiverClass的superClass的cache中没找到方法,就去receiverClass的superClass的class_rw_t中查找,找到方法后调用,结束查找。并将该方法缓存到receiverClass的cache方法列表中。

6、找不到就去receiverClass的superClass的superClass中重复以上过程,如果上层已没有superClass就进入下一阶段,动态方法解析。

2、动态方法解析

1、判断是否曾经有动态解析,如果有动态解析,直接走第3步中的消息转发

2、如果没有动态方法解析过,可以通过+resolveInstanceMethod:+resolveClassMethod:来动态添加方法实现。

3、添加过方法后,标记为已经动态解析,然后重新走消息发送的流程,“从receiverClass的cache中查找方法”这一步开始执行。

void other(id self,SEL _cmd) {
    
}

+(BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(test)) {
       // 动态添加test方法的实现
        class_addMethod(self, sel, (IMP)other, "v@:");
      // 返回YES代表有动态添加方法
      return YES;
    }
    return [super resolveClassMethod:sel];
}

3、消息转发

1、调用forwardingTargetForSelector:方法,返回值不为nil,调用objc_msgSend

2、返回值为nil,调用methodSignatureForSelector:方法,返回值不为nil,调用forwardInvocation:方法

3、返回值为nil,调用doesNotRecognizeSelector:方法

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        // objc_msgSend([[MJCat alloc] init], aSelector)
        return [[MJCat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

 //方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    anInvocation.target = [[MJCat alloc] init];
    [anInvocation invoke];

    [anInvocation invokeWithTarget:[[MJCat alloc] init]];
}

9、IMPSELMethod的区别和使用场景?

1、IMP代表函数的具体实现

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

2、SEL代表函数名,一般叫做选择器,底层结构跟char *类似

可以通过@selector()和sel_registerName()获得

可以通过sel_getName()和NSStringFromSelector()转成字符串

不同类中相同名字的方法,所对应的方法选择器是相同的

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

method_t是对函数的封装。

typedef struct method_t *Method;
struct method_t {
    SEL name;     //函数名
    const char *types;  //编码 (返回值类型、参数类型)
    MethodListIMP imp; //指向函数的指针 (函数地址)
};

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码,具体可以查看Type Encoding

10、loadinitialize方法的区别什么?在继承关系中他们有什么区别

调用方式不同:

load是根据函数地址直接调用,initialize是通过objc_msgSend调用。

调用时机不同:

load是runtime加载类、分类的时候调用(只会调用1次)

initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)。

load、initialize的调用顺序?

load

1> 先调用类的load
a) 先编译的类,优先调用load
b) 调用子类的load之前,会先调用父类的load

2> 再调用分类的load
a) 先编译的分类,优先调用load

initialize

1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize方法)

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

推荐阅读更多精彩内容