回答-阿里、字节:一套高效的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的结构.png2、需要在调用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_t
和 class_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
区别?
category
与 extension
的主要区别是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、IMP
、SEL
、Method
的区别和使用场景?
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、load
、initialize
方法的区别什么?在继承关系中他们有什么区别
调用方式不同:
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方法)