iOSiOS-Runtime

iOS 底层 -- alloc与init

2019-10-05  本文已影响0人  Engandend

[TOC]

1、 什么是runtime

runtime 是C、C++、汇编实现的一套API,目的是为 OC增加运行时功能

2、 关于alloc与init到底在底层做了什么

看以下打印信息

JEObject *obj = [JEObject alloc];
JEObject *obj1 = [obj init];
JEObject *obj2 = [obj init];
NSLog(@"\n obj - %p\n obj1 - %p\n obj2 - -%p",obj,obj1,obj2);

打印结果为:

 obj - 0x600003218320
 obj1 - 0x600003218320
 obj2 - -0x600003218320

结果很奇怪,三个对象的地址都完全一样。既然很奇怪,那就 来看看源码,直接在xcode中是看不到源码的,我们需要在opensource中去下载涉及到的源码 ,在MacOS中的最新版本(当前是10.14.5)

opensource_macos.png
点开之后 搜索objc 得到2个搜索结果 下载objc4-xxxx(后面的代表版本号,会随着版本的更新而变化) objc4.png

这里有一个问题,在opensource中,有如此多的源码,如何知道 要下载objc的这份源码呢,这里提供2种方式

方法一、汇编:

将断点打在 JEObject *obj = [JEObject alloc];这一行
菜单栏 -> Debug -> Debug workflow -> Always show disassembly 勾选这个选项(也就是开启汇编模式)
程序运行起来,进入断点的时候出现这样的代码:(不同版本的Xcode,内容会稍有区别)

汇编模式(xcode 10.3)
汇编模式 Xcode 11.0

在这里我们大致可以猜一下 明显的双引号中间的红色文字,应该表示的意思就是方法名,当然在这里,这些可以不去理会。

这里需要重点关注callq (更老版本的Xcode 这里是 bl
callq 表示的是调用了方法 。在当前断点下面最近的那个callq那一行打上断点 (也就是 objc_msgSend 或者 objc_alloc),继续走,来到断点时, 按住control键, 注意看框起来的部分 框起来的那个地方发生了变化

进入objc_msgSend内部 点击 框起来的那个按钮,(control 不松开),进去之后 会出现很精简的汇编内容 control 进入之后

继续点击 step into 按钮 就会出现我们需要的内容


最终结果

这样说就拿到了

方法二: 符号断点法

添加 符号断点(可能很多人只添加过全局断点,还不知道符号断点是啥😂)

添加符号断点

步骤一:在需要探究的方法上打断点


在需要的地方打断点

步骤二:当程序运行到此断点的时候 添加符号断点

写上当前调用的方法名(关键词) 比如 ‘alloc’ ‘init’ ‘setimage:’等 为符号断点写上方法名
符号断点添加成功之后(可能会小卡一下)让断点直接走(下一曲图标的那个)就出现了我们需要的内容 需要的结果

有了这个结果,就可以去opensource搜索源码了。

方法三、偶然发现,不知道是否正确

进入API的系统声明,就是按住command ->jump to definition,
箭头所指即为所需要的结果,拿这个信息去opensource搜索

偶然发现

⚠️:这个地方如果是文件夹,是有源码可找的,如果是framework 那么就是没有源码可看的

2.1 alloc的底层实现

打开源码文件,搜索 alloc { (系统的方法 一般都是在方法最后加一个空格,然后才跟上大括号)进去之后 查看具体的实现,其经过了2次调用 alloc -> _objc_rootAlloc -> callAlloc 在callAlloc中得到了返回实际的对象。

alloc _objc_rootAllc callAlloc

2.2 init的底层实现

同样的方法, 搜索 init {,进去之后 只调用了一个方法 init -> _objc_rootInit 在 rootInit方法中 只做了一件事情, 就是 retrun obj。

init objc_rootInit

到了这里 就知道了 为何上面 alloc之后 2次init 打印的地址都是一样的了。

2.3 new的底层实现

既然都已经进来了,那我们就来验证另外一个问题 new,我们经常说 new方法 其实就相当于是 alloc init,那么我们进入源码看看他们之间究竟是否有差别,
同样的方法, 搜索 new { 进入之后 查看实现 里面只有一次方法的实现

new 方法的实现

在new方法里面就是调用alloc的实现(callAlloc)后 进行了init操作,由此可见,[Class new] 完全等价于 [[Class alloc] init]

问题来了

上面说,init没有任何操作, 由此引出另外一个问题:既然init没有做任何操作,为何还要有init这个步骤?

要想回答这个问题,先想想,init是在什么时候会用到?
重写方法的时候! 在初始化一个类的时候,如果这个类的参数需要默认的值, 一般我们都会选择在 init方法中初始化这个值(比如封装一个倒计时按钮,一般都会在init中初始化 time = 60)。
父类不实现,交给子类根据需求去实现,

3 LLVM(编译器)

1、 补充 sieof()知识

sizeof() 是获取类型的大小

32位系统 64位系统
BOOL 1 1
char 1 1
short 2 2
float 4 4
CGFloat 8 8
int 4 4
long 4 8
double 8 8
long long 8 8
void *(指针) 4 8
结构体指针(struct */Class) 4 8
结构体 最大属性内存的倍数 详细算法参考 同32位

结构体的sizeof() 长度 用 补、偏、长的方式 是一个很好理解和计算的方法

struct JEObject_IMPL {
    NSString    *x; // 补0  偏0  长8
    int         a;  // 补0  偏8  长12
    char        y;  // 补0  偏12  长13
    char        t;  // 补0  偏13  长14     14不是8(最长是NSString * 的8位)的倍数 内存对齐为16
};
struct JETemp_IMPL {
    NSString *a;     // 补0  偏0  长8
    int b;           // 补0  偏8  长12      12不是8的倍数  内存对齐为16
};
struct JEObject_IMPL {    
    struct JETemp_IMPL x; // 补0  偏0  长16
    char y;               // 补0  偏16  长17
    char t;               // 补0  偏17  长18
    CGSize  z;            // 补6  偏24  长40     40是8(最长是JETemp_IMPL中NSString * 的8位)的倍数
                           // 这里为什么是补6 ?  因为CGSize是结构体,里面最长位是8  按照8的倍数来补齐
};

如果使用 sizeof(obj) 得到的实际只是 指针的大小 也就是 4/8
想要知道实际上obj的内存,继续下面的问题讲解

2、 malloc_size() ——sizeof() —— class_getInstanceSize()

定义这样一个对象JEObject

@interface JEObject : NSObject {
    @public
    int _age;
    int _height;
    NSString *_name;
}
@end
    JEObject *obj = [JEObject alloc];
    NSLog(@"%zu",class_getInstanceSize([obj class]));
    NSLog(@"%lu",sizeof(obj));

注意和sizeof()的区别
问题:创建一个JEObject

这里涉及到对象的本质,
NSobject的本质就是一个包含一个指针(这个指针是指向结构体的指针)的结构体
可以通过一个命令将OC代码转成C

cd 到待转换文件的路径
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc JEObject.m -o out.cpp
//命令解释:最后的 JEObject.m 是待转换文件的文件名, out.cpp 是输出文件的文件名
// 最后得到一个 out.cpp 的文件

在out.cpp中可以搜索JEObject_IMPL就能看到其具体结构

//NSObject
struct NSObject_IMPL {
    Class isa;
// typedef struct objc_class *Class; ---> 指向结构体的指针
};
struct JEObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
    NSString *_name;
};

有3个函数是获取大小的,但是具体表示的意义不一样
malloc_size()

Returns size of given ptr
系统为其分配的内存大小

sizeof()

获取对象所占内存大小
这是一个运算符 而不是方法,在编译的时候就是一个确定的数据

class_getInstanceSize()

Returns the size of instances of a class
获取类的实例对象的成员变量所占用的大小

alloc的关键步骤:


alloc关键步骤

其中 instanceSize()方法的代码

size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;     // 8位对齐   
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

内存对齐的算法:

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
// 相当于    (x + 7)  >> 3 << 3;

/*  8字节对具体算法 ,同理可得 16字节对其
 详细步骤
 (8 + 7) &  ~7

 二字节方式表述:
 15:       0000 1111
 7:       0000 0111
~7:        1111 1000   (取反) 
~7 & 15    0000 1000 (相同为1 否则为0)
最后得到的就是  0000 1000  也就是8 

‘|’ 运算  有1得1  否则为0 
*/
}

3、类所占内存的大小

对于类所占用的内存要分情况来看,上面提到过,类的本质是结构体,那么类所占用的内存大小 就是这个结构体的内存大小,编译之后具体是怎样的结构体,要看具体是用什么方式声明的属性
1、 内部类的方式

@interface JEObject ()
{
    int _age;
    NSString *_name;
    int _height;
}
@end

⬆️对于这种方式,其转换为C代码之后(前面有提到OC转C的命令)变量的顺序不变,在最前面加上了isa指针,所以用补、偏、长的方法来计算这个JEObject的 class_getInstanceSize()为32 malloc_size()为32

2、用@property的方式声明属性

@interface JEObject ()
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int height;
@end

⬆️对于这种方式,其转换为C代码之后(前面有提到OC转C的命令)变量的顺序发生了变化(最前面的加上isa指针之后,<以最优的内存方式?> 、< 以单个属性从小到大的方式?>),所以用补、偏、长的方法来计算这个JEObject的 class_getInstanceSize()为24 malloc_size()为32
具体转换的结果如下⬇️:

struct JEObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
    NSString *_name;
};

3、既有内部类、又有修饰符、还有.m声明

JEObject.h

@interface JEObject : NSObject
{
    NSString    *_strb;
    NSString    *_stra;
    NSString    *_stringc;
}
@property (nonatomic, strong) NSString *b;
@property (nonatomic, assign) NSString *a;
@property (nonatomic, assign) char c;
@end

JEObject.m

@interface JEObject ()
{
    NSInteger   intIn;
}
@property (nonatomic, assign) char cIn;
@end
@implementation JEObject
@end

⬆️对于这种方式,其转换为C代码之后变量的顺序发生了变化(最前面的加上isa指针之后,.h中的内部类中的属性先排在前面,再排.m中的内部类属性,再将.h、.m 中的修饰符属性按照最优的方式排列
具体转换的结果如下⬇️:

struct JEObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_strb;
    NSString *_stra;
    NSString *_stringc;
    NSInteger intIn;
    char _c;
    char _cIn;
    NSString * _Nonnull _b;
    NSString * _Nonnull _a;
};
//class_getInstanceSize
//malloc_size   用法

// class_getInstanceSize(Class _Nullable cls)  参数是Class
size_t size = class_getInstanceSize([p class]);

//malloc_size(const void *ptr); 参数是指针  不过需要桥接  可以根据报错信息 自动Fix
size_t t1 = malloc_size((__bridge const void *)(p));

总结:
malloc_size()class_getInstanceSize() 区别
文字理解:

malloc_size() 系统创建时 系统为对象分配了多少内存,
class_getInstanceSize() 对象实际利用了多少内存

代码层次的理解:

malloc_size () 可以认为是在 class_getInstanceSize() 之后 进行了一次16位内存对齐

上一篇 下一篇

猜你喜欢

热点阅读