前言
相关文章:
iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)
相关代码:
前几篇文章对alloc方法进行了探究,有兴趣的小伙伴可以点击进入进行查看,这篇文章我们主要对isa进行分析:首相先上一张官方isa的走位图,在文章中会分析到如何实现的
要对isa进行分析,我们首先先看先isa的初始化代码
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
//isTaggedPointer这个涉及内存管理,后续在进行详细解释
assert(!isTaggedPointer());
if (!nonpointer) {
// isa对类的绑定
// 使用位域属性进行赋值
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
assert(cls->classArrayIndex() > 0);
// # define ISA_INDEX_MAGIC_VALUE 0x001C0001 直接使用bit进行赋值
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
// # define ISA_MAGIC_VALUE 0x000001a000000001ULL 直接使用bit进行赋值
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
// 使用位域进行赋值
newisa.has_cxx_dtor = hasCxxDtor;
// 这里就就标明右移三为就是类的结构
newisa.shiftcls = (uintptr_t)cls >> 3; //这个就是对类的绑定
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
根据newisa.shiftcls = (uintptr_t)cls >> 3; 我们可以看到isa右移3位就可以得到类的结构;
这里我们可以先看一个骚操作,怎么通过一个对象找到类的
我们可以通过isa指针的偏移可以得到类的地址这里我们先进行右移3位,左移3位(这两步操作主要是去除前三位影响数据),同样的先左移17位,再右移17位,是为了去除后17位操作我们可以得到两个地址:
0b0000000000000000000000000000000100000000000000000010011001011000 person地址
0b0000000000000000000000000000000100000000000000000010011001011000 isa偏移后地址
可以看出isa的偏移后地址和Person地址是一样的,
这里解释下为什么要进行偏移操作,我们来看下ISA的结构
# if __arm64__ //真机
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
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
//这里的总和为1+1+1+33+6+1+1+1+19=64位,即占用了8个字节
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__ //模拟器
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
因为我们使用的是模拟器,要取出shiftcls的值我们可以看出他的值为(3,44),从第三位开始往后取44位,所以我们需要偏移
如果要使用真机进行操作,取出shiftcls值(3,33),就需要右移3位,左移三位,左移28位,右移28位,才可以取出。当然这种取法主要是为了xx;
下面描述下另一种取法,通过对象找到类,我们OC中有方法object_getClass(),传入对象就可以直接获取类,下面看下源码
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
//一般情况都会进入
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
模拟器: # define ISA_MASK 0x00007ffffffffff8ULL
真机: # define ISA_MASK 0x0000000ffffffff8ULL
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
这里实际操作就是,拿出isa.bits 与上ISA_MASK这个值然后就能直接拿到类信息,这个就是传说中的面具取值发,实际上其实很简单就是进行了个与操作
例如我们要取出2-4位置所表示的值且不受其他位置影响,我们只需要找到3-4位都是1的值就行,及1100及12
例如给出值为: 110111我们要取出3-4位值
110111 & 1100得出值为0100(切记不是01,前面的0可以不要,后面的是需要补齐的),就可以拿出3-4位的值表示的值,前面补0
我们可以看到模拟器中0x00007ffffffffff8ULL这里的ULL其实就是unsigned long long ,所以我们取0x00007ffffffffff8,这个值其实就是从第3位开始到第44位都是1的值,与上这个值就可以得到3-44位所表示的值,
我们可以看出,使用这2种方式,都可以获取到isa中类的地址;
下面我们来看一个面试题,在一个Object-C工程中对象可以创建多个,类可以创建多个吗?
我们来用工程验证一下
//MARK: - 分析类对象内存存在个数
void xzTestClassNum(){
Class class1 = [XZPerson class];
Class class2 = [XZPerson alloc].class;
Class class3 = object_getClass([XZPerson alloc]);
Class class4 = [XZPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}
根据输出结果,我们可以看到类在内存中只创建一份,对象当然我们都知道可以创建多个,只是用不同对象中的isa指向同一个类,当然类中不仅仅只有isa,还有其他的,后面文章进行详细描述;
我们使用对象查看一下isa的走向:
这里我们就需要描述一下:类的isa指向的这个XZPerson 叫做元类:
我对这几个概念是这么理解的:
对象:根据类实例化出来 --->开发人员创建 --->运行时创建
类 :内存中只有一份,系统创建出来 --->编译器创建 --->编译时创建
元类:系统编译时发现有类,系统创建出来 --->编译器创建 --->编译时创建
这里插入一下,对象是运行由开发人员创建,没有问题,类和元类是有编译器在编译时进行创建,怎么来证明呢(可以在load,cxx,main函数打断点),为了方便在main函数打断点,在LLDB 中输出类,元类进行验证一下:
由此我们可以得出,类和元类是由编译器在编译时进行创建的!更进一步判断我们可以值进行编译使用编译出的macho文件用MachOView(百度云盘可下载链接:https://pan.baidu.com/s/1VwybGf87Fi1hQveLTPcExA 密码:h4kl)对二进制文件进行分析查看Section(objc_data)段可以直接看到生成的
我们继续对上面的进行继续分析:
我们也可以使用代码进行佐证:
void xzTestNSObject(){
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
}
输出打印结果:
我们可以看到根源类和根根元类地址是相同的,由此佐证苹果官方isa走向图。
这里将苹果官方图解释一下:这个图主要描述的是2个概念:
一个就是isa走向图是虚线:
这里就描述下坑点:XZTearch元类isa,是直接指向NSObject元类的,而不是XZPerson元类
一个是继承链为实线:
这里我们需要注意的是NSObject元类是继承于NSObject的,这里需要特别注意(在消息转发中这里会有坑点)。
这里我们来进行验证一下继承链:要验证这个,我们需要小大概了解一下类的结构体
struct objc_class : objc_object {
// Class ISA; //第一个指针为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() {
return bits.data();
}
...
}
对象的继承大家应该都没有什么问题,我们主要看一下元类的继承
使用这种方法,也可以看类继承,看完后发现,和官方图是一样的。
这里我们就对isa和类之间的关系进行解释了,顺便来看一下对象底层是怎么编译的,这里顺便介绍一下将.m文件编译成底层C++语言方式:clang
主要语句: clang -rewrite-objc main.m -o main.cpp 这种方式直接进行编译文件将main.m 输出为 main.cpp文件
clang -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m -o main.cpp 这种方式,一般导致UIKit报错编译文件,需要引入模拟器SDK资源其中(iPhoneSimulator.platform为模拟器资源,iphones为真机资源,)
或者使用xcode中自带工具xrun:
xcrun -sdk iphonesimulator clang -rewrite-objc main.m -o main4.cpp (iphonesimulator使用模拟器库资源)
xcrun -sdk iphones clang -rewrite-objc main.m -o main4.cpp (iphones使用真机库资源)
使用clang可以将OC高级语言转换为C++语言,例如我们将如下代码进行clang
#import <Foundation/Foundation.h>
@interface XZPerson : NSObject{
@end
@implementation XZPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XZPerson *p = [XZPerson alloc];
NSLog(@"hello world");
}
return 0;
}
编译完成:
我们可以看出,高级语言写了12行,编译为C++后111775行,太吓人了,不过我们不用把这些都看了,看其中部分代码,理解一下大概逻辑即可:
我们研究这个首先就是要找到我们知道的代码,必然是先找到main函数;
找到main函数后,我们刚好就看到了要研究的XZPerson实现确实为结构体,我们看一下struct NSObject_IMPL 结构
struct NSObject_IMPL 中只有一个从父类继承过来的isa;
我们给XZPerson添加一个属性进行查看一下:
#import <Foundation/Foundation.h>
//对象-->编译后-->结构体 父类会继承过来
@interface XZPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation XZPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XZPerson *p = [XZPerson alloc];
NSLog(@"hello world");
}
return 0;
}
这次我们编译完成后直接struct XZPerson_IMPL这个结构体
我们可以看到自己的属性,直接就添加进去了,而且有添加set,和get方法会自动添加,
那我们给Person其中添加一个成员变量会出现什么情况呢,
#import <Foundation/Foundation.h>
//对象-->编译后-->结构体 父类会继承过来
@interface XZPerson : NSObject{
// 成员变量
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;// 属性的区别,底层会生成setgetter方法
@end
@implementation XZPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XZPerson *p = [XZPerson alloc];
NSLog(@"hello world");
}
return 0;
}
这次我们编译完成后直接struct XZPerson_IMPL这个结构体
我们可以看到添加的属性,在属性中出现了nickName,但是并没有生成get,和set方法,
这样我们就对属性,成员变量有了更深刻的认知。
在编译后文件中我们也会看到一些_method_list_t,_ivar_list_t,_prop_list_t等等一系列东西,这个分析类的时候在进行详细分析。
总结:
这篇文章主要分析了isa和类的关系,其中包括,isa的走位分析,和类的继承关系,顺便说了下对象的底层编译;如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!
写给自己:
生活总有那么多的不如意,可还是要以乐观的心态去坚持,不要总是诉说着自己的悲哀,别人更多的只会当成笑话来听,要学会自己承受,坚持,未完待续。。。