iOS Copy解析以及源码分析


Copy解析

测试代码:

NSMutableDictionary *dic1 = [NSMutableDictionary new];  
NSMutableDictionary *dic2 = [dic1 copy];  
NSLog(@"dic1:%p ---- dic2:%p:", dic1, dic2);  
[dic2 setObject:@"test"forKey:@"test"];//会报错No Selector,因为该dic2指向的是NSDictionary对象,没有setObject这个方法  
  
NSDictionary *dic3 = [NSDictionary dictionaryWithObjectsAndKeys:@"dfd",@"dsfds",nil];  
NSDictionary *dic4 = [dic3 copy];  
NSMutableDictionary *dic5 = [dic3 mutableCopy];  
NSLog(@"dic3:%p ---- dic4:%p: ---- dic5:%p", dic3, dic4, dic5);  

输出结果:

dic1:0x618000244260 ---- dic2:0x60800000dbf0:
dic3:0x60000002ff60 ---- dic4:0x60000002ff60: ---- dic5:0x60800005d490

Copy结论:

  1. Copy 得到的对象是 immutable 类,比如NSMutableDictionary 对象 的Copy会返回一个NSDictionary对象。
  2. mutableCopy 得到的对象是 mutable 类,比如NSString执行mutableCopy 会返回NSMutableString,一定是深拷贝。
  3. mutable 对象执行 copy / mutableCopy ,都是深拷贝,因为存在数据同步的问题(比如mutable可以修改了数据),那么就需要新建一块内存复制内容。
  4. immutable 对象执行copy,属于浅拷贝; 执行 mutableCopy 则是深拷贝。(浅拷贝即 retain)

注意的深拷贝也仅仅是新建一个Mutable对象,而原对象如果保存有其他对 象(比如数组),那么里面的对象则是 retain 操作,Apple文档将这个称为集合的单层深拷贝。

定义:

  • 浅复制(shallow copy):在浅复制操作时,对于被复制对象的每一层都是指针复制,即retain操作。
  • 深复制(one-level-deep copy):在深复制操作时,对于被复制对象,至少有一层是深复制,即单层深拷贝。(比如Array只深复制Array的内存地址,里面数组元素浅拷贝)
  • 完全复制(real-deep copy):在完全复制操作时,对于被复制对象的每一层都是对象复制。

集合的完全复制有两种方法:

  1. 调用对象本身写好的API(如initWithArray: copyItems: 和 initWithDictionary: copyItems:,最后的参数copyItems设置为YES)
    执行个这种方法,集合里的每个对象都会收到 copyWithZone: 消息。这个方法要求集合里的所有对象都实现 NSCopying 协议(copy操作),如果对象没有实现 NSCopying 协议,而尝试用这种方法进行深复制,会在运行时出错。
  • 将集合进行归档(archive),然后解档(unarchive),就可以实现完全深复制,mutable对象会进行mutableCopy,而immutable对象会新建一块内存复制内容。(同样需要所有对象都实现 NSCopying、NSMutableCopying 协议)
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

源码分析

我们看下源码,copy和mutableCopy是干什么的

// NSObject.mm
+ (id)copy {
    return (id)self;
}

+ (id)copyWithZone:(struct _NSZone *)zone {
    return (id)self;
}

- (id)copy { 
    return [(id)self copyWithZone:nil];
}

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

+ (id)mutableCopyWithZone:(struct _NSZone *)zone {
    return (id)self;
}

- (id)mutableCopy {
    return [(id)self mutableCopyWithZone:nil];
}

从上面的源码看,是否理解我们继承NSObject的类对象在没有实现NSCopying、NSMutableCopying协议的情况下,执行copy的时候会Crash的原因了么?

因为[(id)self copyWithZone:nil] 是需要我们自定义的类里面实现的

NSCopying、NSMutableCopying协议

@protocol NSCopying  
- (id)copyWithZone:(nullableNSZone *)zone;  
@end  
  
@protocol NSMutableCopying  
- (id)mutableCopyWithZone:(nullableNSZone *)zone;  
@end

写个copy的示例代码:

@interface MyObject : NSObject<NSCopying, NSMutableCopying>  

- (id)copyWithZone:(NSZone*)zone  
{  
    NSLog(@"zone:%p");  // 这里输出nil,因为zone已经被废弃了  

    MyObject obj = [[[self class] allocWithZone:zone] init];
    obj.str = [self.str copy];  
    return obj;  
}  

这段源码的意思是新建一个对象,然后把需要拷贝的源对象的成员变量赋值过来。
那么我们看看OC源码是怎么实现Copy的
OC Foundation框架 功能类 已实现的深拷贝 Copy 源码了。(未验证)

// runtime.h
OBJC_EXPORT id object_copyFromZone(id anObject, size_t nBytes, void *z) 
    __OSX_DEPRECATED(10.0, 10.5, "use object_copy instead") 
/** 
 * Returns a copy of a given object.
 * 
 * @param obj An Objective-C object.
 * @param size The size of the object \e obj.
 * 
 * @return A copy of \e obj.
 */
OBJC_EXPORT id object_copy(id obj, size_t size)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0)
    OBJC_ARC_UNAVAILABLE;
// objc-runtime-new.m
/***********************************************************************
* object_copy
* fixme
* Locking: none
**********************************************************************/
id object_copy(id oldObj, size_t extraBytes)
{
    return _object_copyFromZone(oldObj, extraBytes, malloc_default_zone());
}
/***********************************************************************
* object_copyFromZone
* fixme
* Locking: none
**********************************************************************/
static id _object_copyFromZone(id oldObj, size_t extraBytes, void *zone)
{
    if (!oldObj) return nil;
    if (oldObj->isTaggedPointer()) return oldObj;

    // fixme this doesn't handle C++ ivars correctly (#4619414)

    Class cls = oldObj->ISA();
    size_t size;
    id obj = _class_createInstanceFromZone(cls, extraBytes, zone, false, &size);
    if (!obj) return nil;

    // Copy everything except the isa, which was already set above.
    uint8_t *copyDst = (uint8_t *)obj + sizeof(Class);
    uint8_t *copySrc = (uint8_t *)oldObj + sizeof(Class);
    size_t copySize = size - sizeof(Class);
    memmove(copyDst, copySrc, copySize); // 拷贝对象的内存数据

    /* fixupCopiedIvars
    * Fix up ARC strong and ARC-style weak variables 
    * after oldObject was memcpy'd to newObject.*/
    fixupCopiedIvars(obj, oldObj); // 处理对象的ARC 

    return obj;
}

看得出源码也是新建了一个对象,但是这里是在新建对象之后使用内存拷贝的方法 memmove 把源对象的成员变量直接拷贝到新建对象。
之前我在写CopyWithZone的示例代码,就是仿造_object_copyFromZone这个方法内部实现写的。


NSZone

有人可能注意到 NSCopying、NSMutableCopying两个协议的方法参数 NSZone,这个是干什么的?

可以这么说,NSZone是OC旧版本遗留下来的,在OC2.0开始已经被废弃了。
既然已经废弃了,那为什么还留着呢?
额,有可能是为了兼容老版本,也有可能是嫌麻烦…
接下来还是用源码说明NSZone还有没有在使用

// objc-runtime-new.mm
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}

从 id_objc_rootAllocWithZone方法看出,zone在__ OBJC2 __ (即objc2.0)开始已经不用啦。
继续看看class_createInstance源码里关于Zone的(有个细节:OC源码私有方法,方法名前面都带有“_”前缀,现在看很多第三方SDK也开始用这种代码风格)

// objc-runtime-new.mm

/* class_createInstance和class_createInstanceFromZone调用的都是同一个方法 */

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil); 
}
id class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
    return _class_createInstanceFromZone(cls, extraBytes, zone);
}
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) { // 使用Zone的地方
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

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

继续!!!

//  objc-object.h
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    assert(!cls->instancesRequireRawIsa());
    assert(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        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
        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;
    }
}

从上面的源码看出,Zone是用来初始化内存时使用的,最后初始化对象值的都是initIsa这个方法。
那还有malloc_zone_calloc这个方法了呢?
继续看一下源码吧

// objc-os.h
typedef void * malloc_zone_t;

static __inline malloc_zone_t malloc_default_zone(void) { return (malloc_zone_t)-1; }
static __inline void *malloc_zone_malloc(malloc_zone_t z, size_t size) { return malloc(size); }
static __inline void *malloc_zone_calloc(malloc_zone_t z, size_t size, size_t count) { return calloc(size, count); }
static __inline void *malloc_zone_realloc(malloc_zone_t z, void *p, size_t size) { return realloc(p, size); }
static __inline void malloc_zone_free(malloc_zone_t z, void *p) { free(p); }
static __inline malloc_zone_t malloc_zone_from_ptr(const void *p) { return (malloc_zone_t)-1; }
static __inline size_t malloc_size(const void *p) { return _msize((void*)p); /* fixme invalid pointer check? */ }

从源码可以看出,就算zone != nil,malloc_zone_calloc函数里面也没有使用z来分配内存

好的,从源码看,即使我们使用 allocWithZone 时传入了非空的NSZone,并且假设#if __ OBJC2 __不成立,那么我们看下接下来调用的流程:

allocWithZone
-> 
class_createInstanceFromZone
-> 
_class_createInstanceFromZone
-> 
malloc_zone_calloc
-> 
initIsa

// 注意
void * malloc_zone_calloc(malloc_zone_t z, size_t size, size_t count) {
   return calloc(size, count); 
}

最后分配内存的代码也没有再使用Zone了!

差不多啦,再看看C函数内存分配的方法说明吧

 _alloc
原型:void *_alloc(size_t size);
本函数与上述的两个函数不同,因为它是在栈上分配了size大小的内存,因此使用此函数分配的内存不用再担心内存释放的情况了。但是使用此函数需要注意的是:在函数内部使用此函数分配的内存随着函数的终结不复存在,因此不能将此函数分配的内存供函数外部使用。

malloc
原型:void * malloc(size_t size);
该函数将在堆上分配一个size byte大小的内存。它分配的单原完全按字节大小计算,因此如此分配N个单原的student_t,那么要这样实现:(stdent_t *)malloc(N * sizeof (student_t));

calloc
原型:void* calloc(size_t size, int count);
该函数解决了上面的函数的不足,它将分配count个size大小的单原,因此在便用此函数的时候就会很方便,比如对上面的例子就可以:(student_t *)calloc(sizeof(t_student), N)就可以了。这样使用就会很清晰的知道分配的内存是一种什么样的逻辑方式。

malloc与calloc没有本质区别,malloc之后的未初始化内存可以使用memset进行初始化。

realloc是在malloc的基础上增加内存分配,free函数用来对分配在堆的内存进行释放以防内存泄漏的产生。

sbrk函数用来向os申请数据段,供malloc,calloc及realloc申请使用。

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

推荐阅读更多精彩内容