面试驱动技术合集(初中级iOS开发),关注仓库,及时获取更新 Interview-series
isa详解
arm64架构之后,isa进行了优化,变成了一个共用体( union)结构,使用了位域的技术,可以存储更多的信息
#define MNWhiteMask (1<<0) //Ox0000 0001
#define MNRichMask (1<<1) //Ox0000 0010
#define MNBeautyMask (1<<2) //Ox0000 0100
@interface MNObject ()
{
//位域
struct {
char white : 1;
char rich : 1;
char beauty : 1;
}_whiteRichBeauty;
}
@end
番外篇 - 假设一个程序员都喜欢对象,有白富美三个属性,能否用一个字节去存储呢?
具体实现
@implementation MNGirl
- (void)setWhite:(BOOL)white{
_whiteRichBeauty.white = white;
}
- (void)setRich:(BOOL)rich{
_whiteRichBeauty.rich = rich;
}
- (void)setBeauty:(BOOL)beauty{
_whiteRichBeauty.beauty = beauty;
}
- (BOOL)white{
return _whiteRichBeauty.white;
}
- (BOOL)rich{
return _whiteRichBeauty.rich;
}
- (BOOL)beauty{
return _whiteRichBeauty.beauty;
}
@end
使用情况
MNGirl *girl = [MNGirl new];
[girl setWhite:NO];
[girl setRich:YES];
[girl setBeauty:NO];
NSLog(@"white = %d, rich = %d, beauty = %d", girl.white, girl.rich, girl.beauty);
------------------------------------------------------------------
输出:
white = 0, rich = -1, beauty = 0
white 和 beauty的值都是正确的,为啥rich 出错了?
涉及到符号位覆盖的问题!
(lldb) p/x girl->_whiteRichBeauty
((anonymous struct)) $1 = (white = 0x00, rich = 0x01, beauty = 0x00)
赋值的时候,其实还是正确的,rich = 1,取值的时候,怎么出问题了呢?
这里假设结构体 _whiteRichBeauty 占8位,假设是0x 0000 0000
rich = 1,占1位,就是 0x1
要把 0x1 赋值到 0x 0000 0000,就要涉及到拷贝
0x的第一位是1,所以将1拷贝过去,最后变成 0x 1111 1111
全是1,8个1 = 255 = 0xff
0xff 有符号 = 255,无符号 = -1,
所以这里输出-1 ==> 错误输出!
解决办法:
{
//位域
struct {
char white : 2;
char rich : 2;
char beauty : 2;
}_whiteRichBeauty;
}
一个占2位,三个属性合起来也才6位,1个字节还是可以放得下
然后现在 rich = 1,占2位,就是 0x01
rich的第一位是0,第二位才是1,所以将第一位0拷贝过去,最后变成 0x 0000 0001 = 结果!
苹果的做法 -> 共用体:
里面的成员共用一块内存,和结构体不一样,存储采取位运算,结构体只是增加可读性
@interface MNGirl ()
{
//共用体
union {
//存放所有数据
char bits;
//位域:增加可读性,可以省略
struct {
char white : 1;
char rich : 1;
char beauty : 1;
};
}_whiteRichBeauty;
}
@end
//共用体
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
ISA_BITFIELD = struct{
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
}
arm64架构下,isa不是直接指向class的地址,而且要&上一个MASK
shiftcls: class存放的地址,通过 & ISA_MASK ==> 取出表示 class地址的44位
这样其他位就还能存储数据了
释放更快的源码 - objc_destructInstance
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
//如果没有C++析构函数
bool cxx = obj->hasCxxDtor();
//没有设置过关联对象
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//移除操作
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
Class内部结构
class_rw_t
rw –> readwrite 可读可写,可以修改
struct class_rw_t {
method_array_t methods;//方法列表
property_array_t properties;//属性列表
protocol_array_t protocols;//协议列表
}
class_rw_t 中的 methods、properties、protocols 都是 二维数组 ,以methods为例
methods 里面是一组一组的 method_list_t,可以理解为一个一个的分类,每个method_list_t里面是该分类下的一个一个method_t 组成的方法列表
对于分类想了解得更深一点的,怎么拼接组装的,底层实现的,可以看下Category相关知识
class_ro_t
ro –> readonly,只读,不可修改,保留类最初始的属性
struct class_ro_t {
method_list_t * baseMethodList;//原方法列表
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;//成员变量列表
}
class_ro_t 里面baseMethodList 是一维数组,因为原始的类对象只有一个,就是宿主类,不包含分类信息,所以不需要二维数组,直接 baseMethodList 里面直接放 method_t就行了
struct method_t {
SEL name;//函数名
const char *types;//返回值类型&参数
MethodListIMP imp;//函数地址
}
Type Encodings
// "i24@0:8i16f20"
// 0id 8SEL 16int 20float == 24
- (int)test:(int)age height:(float)height
每一个方法都有两个默认参数self
和_cmd
我们可以查到id
类型为@
,SEL
类型为:
- 1、第一个参数
i
返回值 - 2、第二个参数
@
是id 类型的self
- 3、第三个参数
:
是SEL 类型的_cmd
- 4、第四个参数
i
是Int age
- 5、第五个参数
f
是float height
其中加载的数字其实是跟所占字节有关
- 1、
24
总共占有多少字节 - 2、
@0
是id 类型的self
的起始位置为0 - 3、
:8
是因为id 类型的self
占字节为8,所以SEL 类型的_cmd`的起始位置为8
这里的数字都可以省略 i24@0:8i16f20 ==> i@:if