一、語言基礎(chǔ)
1、#import和#include,@class有什么區(qū)別?
import不會(huì)重復(fù)引入頭文件
@class是向前聲明,告訴編譯器有這么一個(gè)類的定義,但是暫時(shí)不引入,保證編譯可以通過,直到運(yùn)行時(shí)采取查看類的實(shí)現(xiàn)文件。這樣也可以避免重復(fù)引用甚至循環(huán)引用等問題。
2、#import<>和#import“”有什么區(qū)別?
import<>只會(huì)去系統(tǒng)目錄下尋找
import“”會(huì)先去用戶目錄下尋找,如果找不到,會(huì)繼續(xù)去系統(tǒng)目錄下尋找
3、Objective-C中堆和棧有什么區(qū)別?
堆一幫用來存放oc對(duì)象,需要手動(dòng)申請(qǐng)和釋放內(nèi)存,ARC環(huán)境下由編譯器管理,不需要手動(dòng)管理。
棧由系統(tǒng)自動(dòng)分配,一般存放非oc對(duì)象的基本類型數(shù)據(jù),例如int,float,不需要手動(dòng)管理。
4、self和super有什么區(qū)別?為什么要使用[super init]?
self是一個(gè)類的隱藏參數(shù),指向當(dāng)前實(shí)例對(duì)象,super是編譯器標(biāo)識(shí)符,在運(yùn)行時(shí),self和super指向同一個(gè)實(shí)例對(duì)象。
區(qū)別在于:當(dāng)self調(diào)用方法時(shí),會(huì)優(yōu)先在當(dāng)前類的方法列表中尋找方法,當(dāng)使用super調(diào)用方法時(shí),會(huì)優(yōu)先從父類的方法列表中尋找方法。
子類初始化時(shí),調(diào)用[super init]方法,主要是為了避免造成未知的錯(cuò)誤,如果父類初始化不成功,返回nil,可以根據(jù)父類初始化結(jié)果,做響應(yīng)容錯(cuò)處理。
二、屬性和實(shí)例變量
1、屬性和實(shí)例變量的區(qū)別是什么?
使用實(shí)例變量的方式聲明的變量,只能在類內(nèi)部訪問,類外無法訪問,而且不能使用“.”語法訪問變量,如果需要對(duì)外提供訪問能力,需要手動(dòng)實(shí)現(xiàn)set和get方法。
使用屬性的方式聲明的變量,編譯器會(huì)自動(dòng)生成set和get方法,也就可以使用“.”語法訪問變量。如果屬性是聲明在h文件中,類內(nèi)部和外部都可以訪問這個(gè)變量,如果是聲明在m文件中,則只能當(dāng)前類內(nèi)部訪問,外部包括子類都無法訪問。
2、修飾屬性的關(guān)鍵字有哪些,分別有什么作用?
修飾屬性的關(guān)鍵字有以下幾個(gè)
(1)原子性:nonatomic,atomic
(2)讀寫控制:readonly,readwrite,getter,setter
(3)內(nèi)存管理:assign,retain,weak,strong,copy,__unsafe_unretained
原子性(nonatomic,atomic)
在多線程中,同一個(gè)變量被多個(gè)線程同時(shí)訪問,會(huì)造成數(shù)據(jù)污染,因此為了安全,Objective-C中默認(rèn)屬性為atomic,即對(duì)set方法加鎖,保證多線程下數(shù)據(jù)安全。同樣的也會(huì)為此承擔(dān)一部分的資源開銷。應(yīng)用中不是特殊情況(多線程通信)一般屬性聲明為nonatomic,這樣可以提高訪問時(shí)的性能。
讀寫控制(readonly,readwrite,getter,setter)
readonly標(biāo)識(shí)只讀,編譯器只提供get方法
readwrite標(biāo)識(shí)讀寫,編譯器提供set和get方法
getter和setter用來指定存取方法
內(nèi)存管理(assign,retain,weak,strong,copy)
assign可以修飾oc對(duì)象和和oc對(duì)象的基礎(chǔ)類型,標(biāo)識(shí)簡單賦值,指針弱引用,不會(huì)對(duì)引用計(jì)數(shù)+1
weak修飾弱引用,只能修飾oc對(duì)象,和assign相同,不同的是,weak修飾的變量在銷毀后,自動(dòng)將指針置為nil,避免野指針。
retain修飾oc對(duì)象,為了持有對(duì)象,聲明強(qiáng)引用,引用計(jì)數(shù)+1。
strong和reatin類似,在ARC中,用strong代替retain。
copy建立一個(gè)和原有對(duì)象內(nèi)容相同且引用計(jì)數(shù)為1的新的對(duì)象。
3、什么時(shí)候使用weak關(guān)鍵字?和assign有什么區(qū)別?
(1)ARC中為了避免循環(huán)引用,可以讓其中一個(gè)對(duì)象使用weak修飾,常見“delegate,block”
(2)Interface Builder中IBOutlet修飾的控件一般也使用weak
區(qū)別:weak只能修飾oc對(duì)象,并且在銷毀后自動(dòng)將指針置為nil,避免野指針,而assign可以修飾oc對(duì)象和非oc對(duì)象的基礎(chǔ)類型數(shù)據(jù),當(dāng)對(duì)象銷毀后,不會(huì)將指針置為nil,形成野指針,再次調(diào)用時(shí)會(huì)導(dǎo)致崩潰。
4、nonatomic和atomic有什么區(qū)別?atomic是絕對(duì)的線程安全么?如果不是該如何實(shí)現(xiàn)?
區(qū)別:對(duì)屬性的存取操作是否添加加鎖操作,來保證多線程下數(shù)據(jù)存取的安全性。在執(zhí)行效率上nonatomic比atomic存取效率更高。
絕對(duì)線程安全么?不是絕對(duì)安全,可以保證大部分情況下數(shù)據(jù)讀取的一致性,比如在多線程下,兩個(gè)線程都對(duì)屬性進(jìn)行循環(huán)+1操作,導(dǎo)致對(duì)屬性的操作,變?yōu)樽x取,+1,存儲(chǔ)的三個(gè)操作,而atomic只能保證讀取和存儲(chǔ)操作,無法保證+1操作時(shí)的原子性。
如何保證絕對(duì)線程的安全?其實(shí)只要給線程中執(zhí)行的代碼塊加鎖就能實(shí)現(xiàn)多線程訪問的安全。
三、實(shí)例方法和類方法
1、什么是類工廠方法?
簡單的說就是用來快速創(chuàng)建對(duì)象的的類方法,可以直接返回一個(gè)初始化好的對(duì)象。UIKit中最經(jīng)典的就是UIButton類中的buttonWithType:類工廠方法。
特征:
(1)一定是類方法
(2)返回值一定是id/instancetype類型
(3)規(guī)范的類方法名,一般以小寫類名為開頭
2、OC中有方法重載么?
沒有,因?yàn)楹瘮?shù)語法定義的問題,OC編譯器不允許定義函數(shù)名相同,參數(shù)個(gè)數(shù)相同,但是返回類型和參數(shù)類型不同的方法。
四、數(shù)據(jù)類型和運(yùn)算符
1、OC中NSInteger和int基礎(chǔ)數(shù)據(jù)類型有什么區(qū)別?
NSInteger是long 和 int 的別名,在預(yù)編譯階段,NSInteger會(huì)根據(jù)系統(tǒng)是32位的還是64位的來動(dòng)態(tài)確定是int類型還是long類型,NSInteger也是官方推薦使用的基本數(shù)據(jù)類型。
2、instancetype和id有什么區(qū)別?
instancetype和id都可以指向任意OC對(duì)象,不同的是:
(1)id可以作為返回值,形參,變量,并將對(duì)象的確定延遲到運(yùn)行時(shí)。
(2)instancetype只能作為返回值,并且在預(yù)編譯時(shí)已經(jīng)確定類型。
五、繼承和多態(tài)
1、OC中有多繼承么?
OC中沒有多繼承,但是可以通過組合,協(xié)議,分類實(shí)現(xiàn)類似多繼承。
2、OC為什么不能實(shí)現(xiàn)多繼承?
因?yàn)镺C的消息機(jī)制,名字查找發(fā)生在運(yùn)行時(shí),而不是編譯時(shí),不能解決多個(gè)基類的二義性。
六、分類和擴(kuò)展
1、什么是Category?作用什么?
Category是OC在不破壞已有類的情況下,為該類添加新方法的一種方式。
作用:
(1)對(duì)現(xiàn)有類添加方法
(2)在沒有源代碼的情況下,對(duì)類進(jìn)行擴(kuò)展
(3)將類中方法的實(shí)現(xiàn)分散到不同文件中,減小單個(gè)文件體積
(4)可以按需動(dòng)態(tài)加載不同的Category
特性:
(1)重名的情況下,類別中的方法優(yōu)先級(jí)高于原類中的方法
(2)不能直接添加成員變量(可以使用runtime實(shí)現(xiàn),較為復(fù)雜)
(3)同一個(gè)類的不同類別聲明了相同方法,調(diào)用時(shí)不確定
(4)可以添加屬性,但是不會(huì)生產(chǎn)set和get 方法,需要通過關(guān)聯(lián)對(duì)象實(shí)現(xiàn)
2、Category的實(shí)現(xiàn)原理是什么?為什么只能添加方法,不能添加屬性?
(1)先看一下Category在runtime源碼中,Categroy定義為一個(gè)結(jié)構(gòu)體
//分類的定義,結(jié)構(gòu)體
typedef struct category_t {
const char *name;//分類名
classref_t cls;//擴(kuò)展的類
struct method_list_t *instanceMethods;//實(shí)例方法列表
struct method_list_t *classMethods;//類方法列表
struct protocol_list_t *protocols;//協(xié)議列表
struct property_list_t *instanceProperties;//屬性列表
} category_t;
從Category的定義里可以看出,分類可以添加實(shí)例方法,類方法,實(shí)現(xiàn)協(xié)議,添加屬性,而不能添加實(shí)例變量,因?yàn)闆]有存儲(chǔ)實(shí)例變量列表的指針。
Category是如何加載的?
(1)_objc_init runtime入口函數(shù),初始化
(2)map_images 加鎖
(3)map_images_nolock 完成類的注冊(cè),初始化,及l(fā)oad方法加載
(4)_read_images 完成類的加載,協(xié)議的加載,類別的加載等工作
(5)remethodizeClass 這一步非常關(guān)鍵,它將類別綁定到目標(biāo)類上
(6)attachCategories 這是最重要的一步,將類別中的方法,屬性綁定到目標(biāo)類
(7)attachLists 將目標(biāo)類中的方法和分類中的方法放到一個(gè)列表中
_read_images具體作用:將類別和目標(biāo)類綁定,并重建目標(biāo)類的方法列表
attachCategories具體作用:分配一個(gè)新的列表空間,用來存放類別的實(shí)例方法,類方法,協(xié)議方法,交給attachLists處理
attachLists具體作用:創(chuàng)建一個(gè)新的列表,與類別中傳過來的列表融合在一起,變成新的方法列表。
如果有重名的方法,類別中的方法位置更靠前,類方法位置靠后,也就解釋了為什么類別的方法優(yōu)先級(jí)要高于目標(biāo)類的方法。
需要注意的是:盡管Category定義中有存放屬性的變量,但是源碼實(shí)現(xiàn)中,并不會(huì)為屬性生成set和get方法,所以需要借助關(guān)聯(lián)對(duì)象,來手動(dòng)實(shí)現(xiàn)。
3、關(guān)聯(lián)對(duì)象是怎么實(shí)現(xiàn)的?
翻一下runtime的源碼,在objc-references.mm文件中有個(gè)方法_object_set_associative_reference:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
_class_setInstancesHaveAssociatedObjects(_object_getClass(object));
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
我們可以看到所有的關(guān)聯(lián)對(duì)象都由AssociationsManager管理,而AssociationsManager定義如下:
class AssociationsManager {
static OSSpinLock _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { OSSpinLockLock(&_lock); }
~AssociationsManager() { OSSpinLockUnlock(&_lock); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
AssociationsManager里面是由一個(gè)靜態(tài)AssociationsHashMap來存儲(chǔ)所有的關(guān)聯(lián)對(duì)象的。這相當(dāng)于把所有對(duì)象的關(guān)聯(lián)對(duì)象都存在一個(gè)全局map里面。而map的的key是這個(gè)對(duì)象的指針地址(任意兩個(gè)不同對(duì)象的指針地址一定是不同的),而這個(gè)map的value又是另外一個(gè)AssociationsHashMap,里面保存了關(guān)聯(lián)對(duì)象的kv對(duì)。
而在對(duì)象的銷毀邏輯里面,見objc-runtime-new.mm:
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa_gen = _object_getClass(obj);
class_t *isa = newcls(isa_gen);
// Read all of the flags at once for performance.
bool cxx = hasCxxStructors(isa);
bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (!UseGC) objc_clear_deallocating(obj);
}
return obj;
}
runtime的銷毀對(duì)象函數(shù)objc_destructInstance里面會(huì)判斷這個(gè)對(duì)象有沒有關(guān)聯(lián)對(duì)象,如果有,會(huì)調(diào)用_object_remove_assocations做關(guān)聯(lián)對(duì)象的清理工作。
4、Category中有+load方法么?什么時(shí)候調(diào)用的?load方法可以繼承么?
load方法是不可以繼承的,因?yàn)閘oad方法不是通過消息傳遞(_objc_msgSend)方式調(diào)用的,是直接通過函數(shù)指針調(diào)用的。因此load方法不存在類的層級(jí)遍歷。
Category中也有l(wèi)oad方法,和類中l(wèi)oad方法不同的是,它不是簡單的繼承或者覆蓋,而是獨(dú)立的load方法。和類中的load方法沒有關(guān)系。
在runtime加載時(shí)調(diào)用load方法,調(diào)用順序:父類,子類,分類。
六、Block
1、Block的原理是什么?使用的時(shí)候需要注意什么?
Block是閉包,可以作為參數(shù),變量,返回值使用。在iOS中廣泛應(yīng)用,比如GCD,動(dòng)畫,循環(huán)。
通過下面一段代碼來分析一下Block原理:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;//聲明一個(gè)變量,存在棧上
void(^testBlock)(int i) = ^(int i){
NSLog(@"a : %d",a);
NSLog(@"i : %d",i);
};
a = 20;
testBlock(a);//調(diào)用block
}
return 0;
}
打印結(jié)果為:a = 10,i=20
通過結(jié)果可以發(fā)現(xiàn),block具有保存變量瞬時(shí)值的特性,記錄了a修改之前的值。
通過Clang來看一下底層C語言的實(shí)現(xiàn):
clang -rewrite-objc main.m
以下為main函數(shù)C語言實(shí)現(xiàn)
//定義了block的結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl;//block的結(jié)構(gòu)體
struct __main_block_desc_0* Desc;//block描述對(duì)象,
int a;//存放變量的a的值
//構(gòu)造函數(shù)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block大括號(hào)對(duì)應(yīng)的實(shí)現(xiàn)
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int i) {
int a = __cself->a; // bound by copy//a的值使用的是結(jié)構(gòu)體中指針指向的值,在構(gòu)造時(shí)已經(jīng)確定為10,而不是使用的是block外部變量a的地址,
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_3r7qfrfs39729zxpgrbp45z40000gp_T_main_b630e6_mi_0,a);//打印輸出時(shí)使用的也是內(nèi)部變量a,并不是外部變量a或者使用a的指針,所以為10.
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_3r7qfrfs39729zxpgrbp45z40000gp_T_main_b630e6_mi_1,i);//i的值為參數(shù)的值,這里為20
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void(*testBlock)(int i) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *, int))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock, a);
}
return 0;
}
這里可以看出,__main_block_impl_0定義有一個(gè)成員變量a,用來保存構(gòu)造函數(shù)中傳入的a的值,相當(dāng)于構(gòu)造時(shí)復(fù)制了一份a的值,block執(zhí)行時(shí),使用的是block內(nèi)部成員變量a的值,而不是block外部的a的值,
仔細(xì)觀察可以發(fā)現(xiàn):struct __main_block_impl_0 其實(shí)是 對(duì) struct __block_impl impl的封裝
struct __block_impl impl的定義:
struct __block_impl {
void *isa; 類似對(duì)象的指針
int Flags;
int Reserved;
void *FuncPtr;
}
isa類似對(duì)象的指針,指向block保存的區(qū)域:
(1)_NSConcreteStackBlock:棧區(qū)存儲(chǔ)的block
(2)_NSConcreteMallocBlock:堆區(qū)存儲(chǔ)的block
(1)_NSConcreteGlobalBlock:全局區(qū)存儲(chǔ)的block
棧block 在函數(shù)的作用域結(jié)束后,釋放
堆block在retainCount為0時(shí),釋放
全局block和程序的生命周期相同
FuncPtr指針,指向block的執(zhí)行函數(shù),即執(zhí)行大括號(hào)內(nèi)的代碼邏輯。
總結(jié)一下:Block底層是由結(jié)構(gòu)體實(shí)現(xiàn)的,block的調(diào)用是由函數(shù)實(shí)現(xiàn)的
Block使用注意事項(xiàng):
在block中使用自動(dòng)變量,無法在block中修改自動(dòng)變量的值,因?yàn)樵跇?gòu)造過程中,a的值已經(jīng)確定了。
如果要修改自動(dòng)變量的值,需要在自動(dòng)變量前加上__block修飾,全局變量和靜態(tài)變量不需要加修飾也可以在block中修改他們。
__block 修飾的自動(dòng)變量,為何能夠修改?改動(dòng)一下代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;//聲明一個(gè)變量,存在棧上
void(^testBlock)(int i) = ^(int i){
NSLog(@"a : %d",a);
NSLog(@"i : %d",i);
a++;
};
a = 20;
testBlock(a);//調(diào)用block
NSLog(@"a = %d",21);
}
return 0;
}
結(jié)果輸出為:a = 20,i = 20 ,a = 21;
重復(fù)剛才的clang命令,會(huì)發(fā)現(xiàn),block結(jié)構(gòu)體的構(gòu)造函數(shù)傳入的是b的地址,也就是說不加修飾的話,是值傳遞,存在拷貝,而加了修飾的話,是指針傳遞,所以block內(nèi)部修改變量的話,外部也會(huì)修改.
最后再說一下block從棧復(fù)制到堆的幾種情況:
(1)手動(dòng)調(diào)用block的copy方法;
(2)將block賦給__strong修飾的對(duì)象,同時(shí)block中還要引用外部變量時(shí)
(3)將block作為函數(shù)返回值時(shí)
(4)向Cocoa框架含有usingBlock的方法或者GCD的API傳遞block參數(shù)時(shí)
2、什么是Block循環(huán)引用?如何解決循環(huán)引用?
Block中直接使用外部強(qiáng)指針會(huì)導(dǎo)致循環(huán)引用。
解決辦法:
(1)對(duì)當(dāng)前對(duì)象弱引用
(2)使用完block后,手動(dòng)將一方置為nil
(3)將外部對(duì)象作為參數(shù)傳入block
(4)使用Weak-Strong Dance方式來解決(也是使用最多的一種方式)
其他
1、OC中l(wèi)oad方法和initialize的方法有什么區(qū)別?
(1)load方法不能繼承,是通過函數(shù)指針調(diào)用,runtime運(yùn)行時(shí)調(diào)用,較早
(2)initialize方法可以繼承,是通過消息傳遞調(diào)用的,第一次收到消息時(shí)調(diào)用,較晚
2、copy方法是深復(fù)制還是淺復(fù)制?
淺復(fù)制是復(fù)制對(duì)象的指針,深復(fù)制是復(fù)制對(duì)象內(nèi)容,生成新的對(duì)象。
copy不管是深復(fù)制還是淺復(fù)制,復(fù)制出的對(duì)象都是不可變的。
mutablCopy復(fù)制的出的都是可變的。
按照容器和非容器類型,可變和不可變類型分。有如下幾種情況。
(1)容器-不可變: NSArray (copy 淺拷貝,mutableCopy深拷貝)
(2)容器-可變 :NSMutableArray (copy 深拷貝,mutableCopy深拷貝)
(3)非容器-不可變 :NSString(copy 淺拷貝,mutableCopy深拷貝)
(4)非容器-可變:NSMutableString (copy 深拷貝,mutableCopy深拷貝)
copy對(duì)可變對(duì)象,為深復(fù)制,原對(duì)象引用計(jì)數(shù)不+1,對(duì)于不可變對(duì)象是淺復(fù)制,引用計(jì)數(shù)+1,始終返回不可變對(duì)象。
mutableCopy始終是深復(fù)制,原對(duì)象引用計(jì)數(shù)不+1,始終返回可變對(duì)象。