iOS面試關(guān)于屬性copy strong weak assign

copy

copy末誓,引用計數(shù)會+1.然而設(shè)置新值并不會保留舊值里初,而是將其拷貝。

NSString對象為什么盡量用copy來修飾?

我們通過代碼查看copy和strong修飾的區(qū)別

#import "ViewController.h"

@interface ViewController ()

// copy字符串
@property (nonatomic, copy) NSString *myCopyStr;
// 強引用str
@property (nonatomic, strong) NSString *strongStr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
   NSMutableString *testStr = [[NSMutableString alloc] initWithString:@"測試"];
//    NSString *testStr = @"測試";
    self.myCopyStr = testStr;
    self.strongStr = testStr;
    
    NSLog(@"testStr : 指針地址 %p", testStr);
    NSLog(@"myCopyStr : 指針地址 %p", _myCopyStr);
    NSLog(@"strongStr : 指針地址 %p", _strongStr);
    
    NSLog(@"----------------------------------------------------");
    [testStr appendString:@"sss"];
    NSLog(@"testStr : 指針地址 %p ,內(nèi)容%@", testStr , testStr);
    NSLog(@"myCopyStr : 指針地址 %p ,內(nèi)容%@", _myCopyStr,_myCopyStr);
    NSLog(@"strongStr : 指針地址 %p ,內(nèi)容%@", _strongStr,_strongStr);
}

打印結(jié)果

2018-07-17 14:52:35.066351+0800 property[51796:5806435] testStr : 指針地址 0x600000253c20
2018-07-17 14:52:35.066535+0800 property[51796:5806435] myCopyStr : 指針地址 0x600000037d40
2018-07-17 14:52:35.066662+0800 property[51796:5806435] strongStr : 指針地址 0x600000253c20
2018-07-17 14:52:35.066793+0800 property[51796:5806435] ----------------------------------------------------
2018-07-17 14:52:35.067159+0800 property[51796:5806435] testStr : 指針地址 0x600000253c20 ,內(nèi)容測試sss
2018-07-17 14:52:35.067315+0800 property[51796:5806435] myCopyStr : 指針地址 0x600000037d40 ,內(nèi)容測試
2018-07-17 14:52:35.067464+0800 property[51796:5806435] strongStr : 指針地址 0x600000253c20 ,內(nèi)容測試sss

結(jié)論:我們可以看出: 通過strong修飾的字符串医清,strongStr和testStr指向同一塊內(nèi)存地址袱衷,testStr修改對應(yīng)的值敬察,也會相對應(yīng)修改了strong修飾的字符串strongStr薪捍。而copy是重新拷貝了一份,申請了一塊獨立的內(nèi)存抽兆,無法被影響。

接下來我們看看源碼

id 
object_copy(id oldObj, size_t extraBytes)
{
    return _object_copyFromZone(oldObj, extraBytes, malloc_default_zone());
}

static id 
_object_copyFromZone(id oldObj, size_t extraBytes, void *zone)
{
    if (oldObj->isTaggedPointerOrNil()) return oldObj;

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

    Class cls = oldObj->ISA(/*authenticated*/true);
    size_t size;
    id obj = _class_createInstanceFromZone(cls, extraBytes, zone,
                                           OBJECT_CONSTRUCT_NONE, 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); // 拷貝對象的內(nèi)存數(shù)據(jù)

    fixupCopiedIvars(obj, oldObj); // 處理對象的ARC 

    return obj;
}

最終調(diào)用的源碼

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

copy和strong修飾的屬性在底層編譯的不一致族淮,主要還是llvm中對其進(jìn)行了不同的處理的結(jié)果辫红。copy的賦值是通過objc_setProperty,而strong的賦值時通過self + 內(nèi)存平移(即將指針通過平移移至name所在的位置祝辣,然后賦值)贴妻,然后還原成 strong類型

strong & copy 在底層調(diào)用objc_storeStrong,本質(zhì)是新值retain蝙斜,舊值release

strong

strong: 強引用名惩,會使引用計數(shù)+1.setter方法賦值時,會保留新值孕荠,并釋放舊值娩鹉,然后在將新值設(shè)置。

weak

弱引用稚伍,引用計數(shù)不增加弯予。setter方法賦值時,即不保留新值个曙,也不釋放舊值锈嫩。當(dāng)對象被銷毀時,屬性值會自動置nil垦搬。

assign

用于基本數(shù)據(jù)類型呼寸。CGFloat,NSInteger等

unsafe_unretained

作用于OC對象,引用計數(shù)不增加猴贰。當(dāng)對象被銷毀時对雪,屬性值不會清空,正如字面上的意思糟趾,不安全慌植。

retain

1.判斷是否為nonpointer ->散列表
2.操作引用計數(shù)
a: 如果不是 nonpointer -> 散列表
spinlock_t slock; 開解鎖
RefcountMap refcnts; 引用計數(shù)表
weak_table_t weak_table; 弱應(yīng)用表

b: 是否正在釋放 如果正在釋放就不需要操作引用計數(shù)了
c: extra_rc + 1 滿了 - 散列表
d: carry 滿 extra_rc 滿/2 -> extra_rc 滿/2 -> 散列表 (開鎖關(guān)鎖)

retain源碼

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, RRVariant::Fast);
}

ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    return rootRetain(true, RRVariant::Fast) ? true : false;
}

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }

    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 
 // extra_rc++ 
        
      //蘋果為什么要這么設(shè)計?义郑?蝶柿?
    //因為存儲到散列表中需要開鎖解鎖操作,所以這里只放一半并且蘋果也解釋了  slowpath是很小的可能性

// x 很可能不為 0非驮,希望編譯器進(jìn)行優(yōu)化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能為 0交汤,希望編譯器進(jìn)行優(yōu)化
#define slowpath(x) (__builtin_expect(bool(x), 0))

        if (slowpath(carry)) { //操作散列表 extra_rc  如果滿了 放一半到散列表里面 newisa.extra_rc = RC_HALF;
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}

NONPOINTER_ISA

蘋果將 isa 設(shè)計成了聯(lián)合體,在 isa 中存儲了與該對象相關(guān)的一些內(nèi)存的信息,原因也如上面所說芙扎,并不需要 64 個二進(jìn)制位全部都用來存儲指針星岗。

來看一下 isa 的結(jié)構(gòu):


// x86_64 架構(gòu)
struct {
    uintptr_t nonpointer        : 1;  // 0:普通指針,1:優(yōu)化過戒洼,使用位域存儲更多信息
    uintptr_t has_assoc         : 1;  // 對象是否含有或曾經(jīng)含有關(guān)聯(lián)引用
    uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析構(gòu)函數(shù)或OC的dealloc
    uintptr_t shiftcls          : 44; // 存放著 Class俏橘、Meta-Class 對象的內(nèi)存地址信息
    uintptr_t magic             : 6;  // 用于在調(diào)試時分辨對象是否未完成初始化
    uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
    uintptr_t deallocating      : 1;  // 對象是否正在釋放
    uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 來存儲引用計數(shù)
    uintptr_t extra_rc          : 8;  // 引用計數(shù)能夠用 8 個二進(jìn)制位存儲時,直接存儲在這里
};
 
// arm64 架構(gòu)
struct {
    uintptr_t nonpointer        : 1;  // 0:普通指針圈浇,1:優(yōu)化過寥掐,使用位域存儲更多信息
    uintptr_t has_assoc         : 1;  // 對象是否含有或曾經(jīng)含有關(guān)聯(lián)引用
    uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析構(gòu)函數(shù)或OC的dealloc
    uintptr_t shiftcls          : 33; // 存放著 Class、Meta-Class 對象的內(nèi)存地址信息
    uintptr_t magic             : 6;  // 用于在調(diào)試時分辨對象是否未完成初始化
    uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
    uintptr_t deallocating      : 1;  // 對象是否正在釋放
    uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 來存儲引用計數(shù)
    uintptr_t extra_rc          : 19;  // 引用計數(shù)能夠用 19 個二進(jìn)制位存儲時磷蜀,直接存儲在這里
}

注意這里的 has_sidetable_rc 和 extra_rc召耘,has_sidetable_rc 表明該指針是否引用了 sidetable 散列表,之所以有這個選項褐隆,是因為少量的引用計數(shù)是不會直接存放在 SideTables 表中的污它,對象的引用計數(shù)會先存放在 extra_rc 中,當(dāng)其被存滿時庶弃,才會存入相應(yīng)的 SideTables 散列表中衫贬,SideTables 中有很多張 SideTable,每個 SideTable 也都是一個散列表虫埂,而引用計數(shù)表就包含在 SideTable 之中祥山。

SideTables

引用計數(shù)要么存放在 isa 的 extra_rc 中,要么存放在引用計數(shù)表中掉伏,而引用計數(shù)表包含在一個叫 SideTable 的結(jié)構(gòu)中缝呕,它是一個散列表,也就是哈希表斧散。而 SideTable 又包含在一個全局的 StripeMap 的哈希映射表中供常,這個表的名字叫 SideTables。

散列表(Hash table鸡捐,也叫哈希表)栈暇,是根據(jù)建(Key)而直接訪問在內(nèi)存存儲位置的數(shù)據(jù)結(jié)構(gòu)。也就是說箍镜,它通過一個關(guān)于鍵值得函數(shù)源祈,將所需查詢的數(shù)據(jù)映射到表中一個位置來訪問記錄,這加快了查找速度色迂。這個映射函數(shù)稱作散列函數(shù)香缺,存放記錄的數(shù)組稱作散列表

// SideTables
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
 
// SideTable
struct SideTable {
    spinlock_t slock;           // 自旋鎖
    RefcountMap refcnts;        // 引用計數(shù)表
    weak_table_t weak_table;    // 弱引用表
    
    // other code ...
};

weak_table_t weak_table; 弱應(yīng)用表
散列表 在內(nèi)存里面有多張 + 最多能夠多少張?歇僧?图张?
回答:
一個 SideTables 包含眾多 SideTable,每個 SideTable 中又包含了三個元素,spinlock_t 自旋鎖祸轮、RefcountMap 引用計數(shù)表兽埃、weak_table_t 弱引用表。所以既然 SideTables 是一個哈希映射的表适袜,為什么不用 SideTables 直接包含自旋鎖柄错,引用計數(shù)表和弱引用表呢?這是因為在眾多線程同時訪問這個 SideTable 表的時候痪蝇,為了保證數(shù)據(jù)安全鄙陡,需要給其加上自旋鎖,如果只有一張 SideTable 的表躏啰,那么所有數(shù)據(jù)訪問都會出一個進(jìn)一個,單線程進(jìn)行耙册,非常影響效率给僵,雖然自旋鎖已經(jīng)是效率非常高的鎖,這會帶來非常不好的用戶體驗详拙。針對這種情況帝际,將一張 SideTable 分為多張表的 SideTables,再各自加鎖保證數(shù)據(jù)的安全饶辙,這樣就增加了并發(fā)量蹲诀,提高了數(shù)據(jù)訪問的效率,這就是為什么一個 SideTables 下涵蓋眾多 SideTable 表的原因弃揽。

retain總結(jié)

首先判斷是否非nonpointer指針,直接操作散列表脯爪,進(jìn)行引用計數(shù)+1的操作。
如果是nonpointer矿微,操作extra_rc進(jìn)行常規(guī)的引用計數(shù)+1操作痕慢,當(dāng)然這里還有一個判斷。如果屬性值正在進(jìn)行釋放涌矢,也不需要進(jìn)行引用計數(shù)的操作掖举,同時這里還有一個細(xì)節(jié)要注意,蘋果設(shè)計了一個算法是比較到位的娜庇,在真機上只有8位塔次,如果說8位滿了,需要額外借助散列表來存儲名秀,它會把extra_rc滿狀態(tài)的一半存儲2的7次方到散列表中励负,剩下的2的7次方還是存在nonpointer的extra_rc中,來進(jìn)行正常的引用計數(shù)操作泰偿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熄守,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裕照,老刑警劉巖攒发,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異晋南,居然都是意外死亡惠猿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門负间,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偶妖,“玉大人,你說我怎么就攤上這事政溃≈悍茫” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵董虱,是天一觀的道長扼鞋。 經(jīng)常有香客問我,道長愤诱,這世上最難降的妖魔是什么云头? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮淫半,結(jié)果婚禮上溃槐,老公的妹妹穿的比我還像新娘。我一直安慰自己科吭,他們只是感情好昏滴,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著砌溺,像睡著了一般影涉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上规伐,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天蟹倾,我揣著相機與錄音,去河邊找鬼猖闪。 笑死鲜棠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的培慌。 我是一名探鬼主播豁陆,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吵护!你這毒婦竟也來了盒音?” 一聲冷哼從身側(cè)響起表鳍,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祥诽,沒想到半個月后譬圣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡雄坪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年厘熟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片维哈。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡绳姨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阔挠,到底是詐尸還是另有隱情飘庄,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布购撼,位于F島的核電站竭宰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏份招。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一狞甚、第九天 我趴在偏房一處隱蔽的房頂上張望锁摔。 院中可真熱鬧,春花似錦哼审、人聲如沸谐腰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽十气。三九已至,卻和暖如春春霍,著一層夾襖步出監(jiān)牢的瞬間砸西,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工址儒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芹枷,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓莲趣,卻偏偏與公主長得像鸳慈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喧伞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容