刨根问底:为什么不能用 copy 修饰 NSMutableArray ?

前言

安利一波个人 blog 地址:https://zlanchun.github.io

@property (nonatomic,copy) NSMutableArray *imArray;

会有什么问题?

copy 修饰的 imArray 在赋值后,运行时会变成 NSArray 也就是不可变类型,所以之后使用 NSMutableArray 的方法时,会产生 unrecognized selector 异常。

为什么会这样?

底层实现

测试

测试类如下:

@interface FooModel : NSObject
@property (nonatomic,strong) NSString *mString;
@property (nonatomic,copy) NSString *imString;
@property (nonatomic,copy) NSArray *contrastArray;
@property (nonatomic,copy) NSMutableArray *imArray;
@property (nonatomic,strong) NSMutableArray *mArray;
@end

@implementation FooModel
@end

测试

    FooModel *model = [FooModel new];
    
    {   // 1.
        NSLog(@"------1.------");
        NSMutableString *mString = [[NSMutableString alloc] initWithString:@"m string"];
        model.mString = mString;
        NSLog(@"m string: %@",model.mString);
        [mString appendString:@" -end"];
        NSLog(@"m string: %@",model.mString);
    }
    
    {   // 2.
        NSLog(@"------2.------");
        NSMutableString *mString = [[NSMutableString alloc] initWithString:@"m string"];
        model.imString = mString;
        NSLog(@"m string: %@",model.imString);
        [mString appendString:@" -end"];
        NSLog(@"m string: %@",model.imString);
    }
    
    {   // 3.
        NSLog(@"------3.------");
        NSMutableArray *tempArray =  [@[@"m-array-1",@"m-array-2",@"m-array-3"] mutableCopy];
        model.contrastArray =  tempArray;
        NSLog(@"contrastArray: %@",model.contrastArray);
        [tempArray replaceObjectAtIndex:2 withObject:@"replace string"];
        NSLog(@"contrastArray: %@",model.contrastArray);
    }

    {   // 4.
        NSLog(@"------4.------");
        NSMutableArray *tempArray =  [@[@"m-array-1",@"m-array-2",@"m-array-3"] mutableCopy];
        model.imArray =  tempArray;
        [model.imArray replaceObjectAtIndex:2 withObject:@"replace string"];
    }

输出结果:

------1.------
m string: m string
m string: m string -end
------2.------
m string: m string
m string: m string
------3.------
contrastArray: (
    "m-array-1",
    "m-array-2",
    "m-array-3"
)
contrastArray: (
    "m-array-1",
    "m-array-2",
    "m-array-3"
)
------4.------
-[__NSArrayI replaceObjectAtIndex:withObject:]: unrecognized selector sent to instance 0x100b78010

从结论得知:

当 mString 改变时,copy 和 strong 修饰的 NSString 属性变化

  • copy 修饰的 NSString 不会随着外部变化而变化
  • strong 会随着外部变化而变化

对比 NSMutableArray 属性

  • copy 和 strong 修饰的 NSMutableArray 都不会随着外部变化而变化
  • copy 修饰的 NSMutableArray 无法找到replaceObjectAtIndex:withObject: 方法,会发生异常

重写 clang -rewrite-objc

让我们看看运行时是如何实现的,重写(clang -rewrite-objc)FooModel 类

static NSString * _I_FooModel_mString(FooModel * self, SEL _cmd) { return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_mString)); }
static void _I_FooModel_setMString_(FooModel * self, SEL _cmd, NSString *mString) { (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_mString)) = mString; }

static NSString * _I_FooModel_imString(FooModel * self, SEL _cmd) { return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_imString)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_FooModel_setImString_(FooModel * self, SEL _cmd, NSString *imString) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct FooModel, _imString), (id)imString, 0, 1); }

static NSArray * _I_FooModel_contrastArray(FooModel * self, SEL _cmd) { return (*(NSArray *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_contrastArray)); }
static void _I_FooModel_setContrastArray_(FooModel * self, SEL _cmd, NSArray *contrastArray) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct FooModel, _contrastArray), (id)contrastArray, 0, 1); }

static NSMutableArray * _I_FooModel_imArray(FooModel * self, SEL _cmd) { return (*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_imArray)); }
static void _I_FooModel_setImArray_(FooModel * self, SEL _cmd, NSMutableArray *imArray) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct FooModel, _imArray), (id)imArray, 0, 1); }

static NSMutableArray * _I_FooModel_mArray(FooModel * self, SEL _cmd) { return (*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_mArray)); }
static void _I_FooModel_setMArray_(FooModel * self, SEL _cmd, NSMutableArray *mArray) { (*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_FooModel$_mArray)) = mArray; }

发现当设定 copy 属性时,如:

  • NSString 用 copy 修饰
  • NSArray 用 strong/weak 修饰
  • NSMutableArray 用 copy 修饰

都调用了 void objc_setProperty (id, SEL, long, id, bool, bool) 方法。

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    ...
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    ...
}

因此,当属性设定 copy 时,都会调用 objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct FooModel, _xxx), (id)xxx, 0, 1) 方法,同时 copy 值传入的是 1,也就是 true。

然后在实现里,当 copy 是 true 时,设定方法通过 copyWithZone 赋值,然后产出的不可变的新值。

所以 NSArray 和 copy 修饰的 NSString 表现形式是一样的,当外部的可变对象变化时,自身不会变化。

而 NSMutableArray 用 copy 修饰的时候,在赋值之后,自身的类型在运行时被改变了,变成了 NSArray 类型,这时再使用 NSMutableArray 方法就会产生 unrecognized selector 异常。

总结

当 copy 修饰可变类型集合(例如:NSMutableArray)时,赋值后,会导致可变类型属性变为不可变类型,然后在调用可变类型方法时,会产生异常错误。

产生异常的原因是 copy 属性在运行时赋值时调用了 -copyWithZone:赋值,将可变类型转换为不可变类型。

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

推荐阅读更多精彩内容