alloc&init底层重识

    Person * p1 = [Person alloc];
    Person * p2 = [p1 init];
    Person * p3 = [p1 init];
    
    NSLog(@"对象%@ - 指针指向的地址%p - 指针地址%p",p1,p1,&p1);
    NSLog(@"对象%@ - 指针指向的地址%p - 指针地址%p",p2,p2,&p2);
    NSLog(@"对象%@ - 指针指向的地址%p - 指针地址%p",p3,p3,&p3);

打印结果

2021-03-24 13:41:24.665807+0800 111[606:63324] 对象<Person: 0x28291c080> - 指针指向的地址0x28291c080 - 指针地址0x16f91d178
2021-03-24 13:41:24.666031+0800 111[606:63324] 对象<Person: 0x28291c080> - 指针指向的地址0x28291c080 - 指针地址0x16f91d170
2021-03-24 13:41:24.666153+0800 111[606:63324] 对象<Person: 0x28291c080> - 指针指向的地址0x28291c080 - 指针地址0x16f91d168

三个对象的内存地址都指向了同一个 0x28291c080,但使用了3个内存指针指向了这个相同的内存地址。
(猜 因为三个对象 有三个isa指针?)

先下载oc的源码,在该库的readme中,作者也探索了alloc的流程,也做了对应的分析和标注。

在源码中 master / objc_debug-master / objc-781 / objc.xcodeproj项目,双击运行
打开项目后,在Public Headers可以看到NSObject.h系统类NSObject的头文件的所有属性,对象方法,类方法。

头文件.jpg

找到alloc方法后,先选中alloc,然后按住Command键,再右键即可跳转(跟踪)到NSObject.mm文件,文件在/Source中可以找到。

alloc方法

+ (id)alloc {
    return _objc_rootAlloc(self);
}

可以看到方法中调用了_objc_rootAlloc函数,并传入了self自己本身

再跟到_objc_rootAlloc函数可以看到

_objc_rootAlloc

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

注释中写到基本点的类实现通过 类方法(+)调用 alloc,并且cls(类)不能为nil

在代码里又调用了 callAlloc方法
参数分别为:
1.cls
2.是否检查为nil,默认为false
3.是否allocWithZone 传入的是true

再跟进到callAlloc函数里

callAlloc

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

注释中写到通过[类 alloc] 或者[类 allocWithZone:nil]方法来调用得到,接着是简化优化
OBJC2 是判断oc 2.0可用
其中用到了两个宏,宏写在了项目的/Project Headers/objc-os.hline.151、152

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

具体的解释可以参考这篇__builtin_expect 说明
__builtin_expect(bool(x), 1)的意思是x的值为真的可能性最大,编译器会更大的可能编译 if(__builtin_expect(bool(x), 1))里的代码。
相反__builtin_expect(bool(x), 0)的意思是x的值为假的可能性最大,编译器会最大的可能性不走判断该方法的代码,会走else里方法。
所以

//最大可能性为真  编译器最大可能走这个判断
#define fastpath(x) (__builtin_expect(bool(x), 1)) 
//最大可能性为假,编译器最大的可能不走这个判断,会走else
#define slowpath(x) (__builtin_expect(bool(x), 0))

借用__builtin_expect 说明的举例

int x, y;
 if(slowpath(x > 0))
    y = 1; 
else 
    y = -1;

代码里,编译器会优先执行y = -1,因为判断里的最大可能性为假,直接走else
通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着执行代码,从而减少指令跳转带来的性能上的下降。

回头看代码

截图.png

fastpathcls->ISA()->hasCustomAWZ())判断一个类是否有自定义的+allocWithZone实现,AWZ就是AllocWithZone的缩写。所以fastpath(!cls->ISA()->hasCustomAWZ())表示的是这个类没有自定义的+allocWithZone时,走if里的代码。
代码中又接着调用了 _objc_rootAllocWithZone,传入cls(类),和一个nil (接收参数为 malloc_zone_t zone __unused)

等等!~
_objc_rootAllocWithZone是不是有点眼熟?

截图.png

alloc下方,allocWithZone方法中直接调用了_objc_rootAllocWithZone,这个等重学allocWithZone的时候再细究

再跟到_objc_rootAllocWithZone

_objc_rootAllocWithZone

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    //allocWithZone 在 oc 2.0之后忽略zone的参数
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

_class_createInstanceFromZone

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    // 1:需要开辟的内存大小,可以看到外部传入的extraBytes为0
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 2;向系统申请内存,返回地址指针
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    // 3: 关联到相应的类
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

这个方法主要的三个步骤
1.计算出需要开辟的内存空间大小
2.根据需要开辟的内存大小向系统申请内存,返回地址指针
3.创建类的isa 与 地址指针绑定

1.计算内存大小

instanceSize

计算需要开辟的内存空间大小是通过 size = cls->instanceSize(extraBytes);内部实现如下

size_t instanceSize(size_t extraBytes) const {
       if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
           return cache.fastInstanceSize(extraBytes);
       }

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

在最后的时候,判断了 size是否小于16,如果是则 size = 16的操作。
在判断代码里cache.hasFastInstanceSize(extraBytes)是判断cache里是否有实例的内存大小,传入的是extraBytes 值是0。前面是fastpath最大可能性为真(前面说过)。
接着通过cache.fastInstanceSize(extraBytes)来计算最终的实例大小。

fastInstanceSize

size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

代码中_flags & FAST_CACHE_ALLOC_MASK16可以通过位与运算快速提取舍入到下一个16字节边界的实例大小 得到计算后的size
接着将size 加上开辟的内存大小extra 减去 FAST_CACHE_ALLOC_DELTA16(值为0x0008)。理解为实例大小加上内存大小,在进行8位的偏移。
在代码的最后,返回了align16方法的返回值,字面意思就是16对齐

align16

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

代码中可以看到 (x + size_t(15)) & ~size_t(15),说实话我一开始是没懂!也是翻了教程来学,好多种解释。
引用了一个博主的解释

16字节对齐运算

其中X为计算出的实例大小 比如为8,size_t(15)为什么是15?写死?
x +15 = 23,
二进制为 0000 0000 0001 0111

15
二进制为0000 0000 0000 1111

接着将15的二进制取反 (~)取反操作
15取反
二进制为1111 1111 1111 0000

之后将23的二进制与 取反后的二进制进行 与 运算 相同为1 不同为0
与后二进制为 0000 0000 0001 0000 值为16,非常巧妙的计算!

16对齐的目的

  • 提高性能,加快存取速度 通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能。固定16字节的存取长度,可以更快存取数据。
  • 更安全 苹果如今采用16字节对齐,由于在一个对象中,isa占8字节,而对象每个属性也占8字节,当对象无属性时,会预留8字节,即16字节对齐,如果不预留,CPU存取时以16字节长度会导致访问到相邻的其他对象,造成访问混乱。

回过头来看 size = cls->instanceSize(extraBytes),此时size的内存大小就为16,哪怕Int类型为4个字节,那也会开辟出16大小的内存(8个字节)。如果一个类里只有两个int类型的属性,那么这两个int类型的属性会共用一个16大小的内存

isa

isa本身的结构体内部只有一个指针,且isa指针占用内存8个字节。

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

可以用runtimeclass_getInstanceSize来获取实例大小

打印isa大小及内存大小

可以看到Person的类isa大小只有8 但是系统给开辟的空间却有16

现在给Person加三个字段,一个Int,一个NSString,一个Int。顺序很重要!

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
@property(nonatomic,assign) NSInteger age;
@property(nonatomic,strong) NSString * name;
@property(nonatomic,assign) NSInteger height;
@end

内存占用说明.png

图中可看出,苹果的确是做了16字节对齐的操作,Int本该4个字节却开辟了8个字节的空间。

申请内存,返回地址指针

由于在调用_class_createInstanceFromZone时,第三个参数传入的是nil,接收的是zone,而iOS8 以后废弃了用zone来开辟内存的方式,所以直接调用了 obj = (id)calloc(1, size)方法来申请内存。

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
。。。。。。。。
。。。。。。。。
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }

此时打印obj ,po obj出来的只有 )0x开头的地址,并不是<Person 0xxxxxxxxx>这类的信息,因为还没有将类与该类的内存地址进行关联。
calloc只是将拿到的内存大小size去申请内存空间。

关联相应的类

 if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

//initInstanceIsa的实现
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

代码中将类 和 hasCxxDtor传入initInstanceIsa方法,在initInstanceIsa方法中调用了initIsa方法也传入了cls类.
这些操作后就将类的内存地址*相关联起来了。

(lldb) po p1
<Person: 0x60000048b740>

此时类的alloc结束!
总结alloc的作用1.计算类大小,进行16字节对齐。2.拿到类大小去申请开辟内存。3.将类与内存进行关联
引用 K哥的贼船图alloc流程图

流程图

init

+ (id)init {
    return (id)self;
}

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
  • 代码里有两个init方法,一个是类方法,一个是对象方法,都是反悔了对象本身。
  • 类方法返回的是一个id任意类型的self,是为了给开发者自定义构造方法的入口,如重写类方法来设计工厂模式

FruitFactory.h

#import <Foundation/Foundation.h>
#import "FruitProtocol.h"

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger,FruitType){
    FruitTypeApple,
    FruitTypeOrange,
};

@interface FruitFactory : NSObject

+(id<FruitProtocol>)initWithType:(FruitType)type;

@end

NS_ASSUME_NONNULL_END   

FruitFactory.m

#import "FruitFactory.h"
#import "AppleFruit.h"
#import "OrangeFruit.h"

@implementation FruitFactory

+(id<FruitProtocol>)initWithType:(FruitType)type{
    id<FruitProtocol> factory = nil;
    switch (type) {
        case FruitTypeApple:
            factory = [[AppleFruit alloc] init];
            break;
        case FruitTypeOrange:
            factory = [[OrangeFruit alloc] init];
            break;
            
        default:
            break;
    }
    
    return factory;
}

@end

调用工厂方法时就是init开头,但返回的可能是不同的类。所以要拿id来接收

#import "ViewController.h"
#import "FruitFactory.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id<FruitProtocol> factory1 = [FruitFactory initWithType:FruitTypeApple];
    factory1.name = @"苹果";
    [factory1 createProduct];
    
    
    id<FruitProtocol> factory2 = [FruitFactory initWithType:FruitTypeOrange];
    factory2.name = @"橘子";
    [factory2 createProduct];
    
}

@end

new

尝尝用new的机会并不多,但new的源码与alloc + init的本质无区别。

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

new操作无法对工厂设计重载init方法时做的业务操作进行执行。如果不需要重载自己写的方法时,则不需要考虑。
如:

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

推荐阅读更多精彩内容