iOS面試題及答案.及添加React Native面試題 隨時更新

  • 第一大類OC面試題

一.設計模式是什麼? 你知道哪些設計模式,並簡要敘述?

MVC是最普遍認知的設計模式,MVC模式將頁面的邏輯分爲3塊:Model(模型數據業務)、View(UI展示業務)、Controller(協調者-控制器)

這樣的劃分很好理解,維護時,只要找到對應的那一塊進行修改就好了。

在iOS開發中,UIKIt框架是將控制器Controller與View進行綁定了的,每個控制器都有View對象,代碼添加UI子控件細節或者在xib與storyboard中子視圖可以直接與controller進行關聯,都會導致控制器中難以避免很多本該View去負責的UI子控件細節處理放在了控制器Controller裏面;而在Controller裏面本身要處理的請求、控制器生命週期函數要處理的事情比較多的情況下,控制器就變得很臃腫。實際上這個設計模式在iOS中爲:M-VC

MVVM設計模式介紹

M=Model, V=V+C, VM = ViewModel.  爲什麼會叫ViewModel?

先看這樣劃分後的分工:

View :UI界面的創建及根據傳遞的Model來更新視圖的邏輯 。

Controller :負責控制器本身的生命週期,協調各個部分的綁定關係以及必要的邏輯處理。 

ViewModel :網絡請求、返回數據邏輯和緩存讀寫。

 Model :用來將數據模型化,是數據查看更清晰,使用更方便。
總結來說就是:MVVM由MVP和WPF結合演變過來的,MVVM模式是將業務分爲3塊M-V-新對象,由於這個新對象是爲View量身定製的(即它是視圖的模型),被稱爲ViewModel。MVVM的核心是雙向綁定。

MVVM的雙向綁定

綁定的意思就是講兩個對象建立關聯,其中一個改變另一個自動跟着變。假設Model與View綁定就意味着Model改變時View自動跟着變,不需要手動賦值那一步---即響應式

單向綁定:一般指模型數據變化觸發對應的視圖數據變化。

雙向綁定:指模型數據,視圖數據任意一方變化,都會觸發另一方的同步變化。
雙向綁定如何實現?


通信圖
1. 實際開發中的做法:讓Controller擁有View和ViewModel屬性,VM擁有Model屬性;Controller或者View來接收ViewModel發送的Model改變的通知

2. 用戶的操作點擊或者Controller的視圖生命週期裏面讓ViewModel去執行請求,請求完成後ViewModel將返回數據模型化並保存,從而更新了Model;Controller和View是屬於V部分,即實現V改變M(V綁定M)。如果不需要請求,這直接修改Model就是了。

3. 第2步中的Model的改變,VM是知道的(因爲持有關係),只需要Model改變後發一個通知;Controller或View接收到通知後(一般是Controller先接收再賦值給View),根據這個新Model去改變視圖就完成了M改變V(M綁定V)
使用RAC(RactiveCocoa)框架實現綁定可以簡單到一句話概括:

ViewModel中創建好請求的信號RACSignal, Controller中訂閱這個信號,在ViewModel完成請求後訂閱者調用sendNext:方法,Controller裏面訂閱時寫的block就收到回調了。


結論:主體使用MVC,局部看情況使用MVVM設計模式,這樣比較適用於當前的iOS開發。

 

他們之間的結構關係如下:

 

MVVM 的優勢

低耦合:View 可以獨立於Model變化和修改,一個 viewModel 可以綁定到不同的 View 上

可重用性:可以把一些視圖邏輯放在一個 viewModel裏面,讓很多 view 重用這段視圖邏輯

獨立開發:開發人員可以專注於業務邏輯和數據的開發 viewModel,設計人員可以專注於頁面設計

可測試:通常界面是比較難於測試的,而 MVVM 模式可以針對 viewModel來進行測試

MVVM 的弊端

數據綁定使得Bug 很難被調試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數據綁定使得一個位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那麼容易了。

對於過大的項目,數據綁定和數據轉化需要花費更多的內存(成本)。主要成本在於:

數組內容的轉化成本較高:數組裏面每項都要轉化成Item對象,如果Item對象中還有類似數組,就很頭疼。

轉化之後的數據在大部分情況是不能直接被展示的,爲了能夠被展示,還需要第二次轉化。

只有在API返回的數據高度標準化時,這些對象原型(Item)的可複用程度才高,否則容易出現類型爆炸,提高維護成本。

調試時通過對象原型查看數據內容不如直接通過NSDictionary/NSArray直觀。

同一API的數據被不同View展示時,難以控制數據轉化的代碼,它們有可能會散落在任何需要的地方。

二 .屬性引用self.xx與_xx的區別

iOS的@property已經獨攬了@synthesize的功能主要有三個作用:

1.生成了成員變量get/set方法的聲明
2.生成了私有的帶下劃線的的成員變量因此子類不可以直接訪問,但是可以通過get/set方法訪問。那麼如果想讓定義的成員變量讓子類直接訪問那麼只能在.h文件中定義成員變量了,因爲它默認是@protected
3.生成了get/set方法的實現

值得注意的是:
如果已經手動實現了get和set方法(兩個都實現)的話Xcode不會再自動生成帶有下劃線的私有成員變量了
因爲xCode自動生成成員變量的目的就是爲了根據成員變量而生成get/set方法的,但是如果get和set方法缺一個的話都會生成帶下劃線的變量

@property
聲明的屬性默認會生成一個_類型的成員變量,同時也會生成setter/getter方法。蘋果將默認編譯器從GCC轉換爲LLVM(low level virtual machine),纔不再需要爲屬性聲明實例變量了。在沒有更改之前,屬性的正常寫法需要 成員變量 + @property + @synthesize 成員變量 三個步驟。 

上面我們說到了屬性與成員變量、
@property 以及 @synthesize之間的聯繫與區別。
同時,我們提到了self.xx和_xx的一點區別,其中self.xx是調用的xx屬性的get/set方法,而_xx則只是使用成員變量_xx,並不會調用get/set方法。兩者的更深層次的區別在於,通過存取方法訪問比直接訪問多做了一些其他的事情(例如內存管理,複製值等).
例如如果屬性在@property中屬性的修飾符有retain,那麼當使用self.xx的時候相應的屬性的引用計數器由於生成了setter方法而進行加1操作,此時的retaincount爲2。

在一個類中用self.xx  是調用set和get的方法​​,對這個類中xx進行讀取。在內存管理中,會引用計數+1。可以兼容懶加載

三.frame 和 bounds 有什麼不同?

frame指的是:該view在父view座標系統中的位置和大小。(參照點是父view的座標系統)
bounds指的是:該view在本身座標系統中的位置和大小。(參照點是本身座標系統)

四.Objective-C的類可以多重繼承麼?可以實現多個接口麼?Category是什麼?重寫一個類的方式用繼承好還是分類好?爲什麼?

答:Objective-C的類不可以多重繼承;可以實現多個接口(協議);Category是類別;一般情況用分類好,用Category去重寫類的方法,僅對本Category有效,不會影響到其他類與原有類的關係。

五.@property 的本質是什麼?ivar、getter、setter 是如何生成並添加到這個類中的

@property 的本質是什麼?
	@property = ivar + getter + setter;
“屬性” (property)有兩大概念:ivar(實例變量)、getter+setter(存取方法)

“屬性” (property)作爲 Objective-C 的一項特性,主要的作用就在於封裝對象中的數據。 Objective-C 對象通常會把其所需要的數據保存爲各種實例變量。實例變量一般通過“存取方法”(access method)來訪問。其中,“獲取方法” (getter)用於讀取變量值,而“設置方法” (setter)用於寫入變量值。

六.@property中有哪些屬性關鍵字?/ @property 後面可以有哪些修飾符?

屬性可以擁有的特質分爲四類:
1.原子性--- nonatomic 特質
2.讀/寫權限---readwrite(讀寫)、readonly (只讀)
3.內存管理語義---assign、strong、 weak、unsafe_unretained、copy
4.方法名---getter=<name> 、setter=<name>
5.不常用的:nonnull,null_resettable,nullable

七.屬性關鍵字 readwrite,readonly,assign,retain,copy,nonatomic 各是什麼作用,在那種情況下用?

答:
1). readwrite 是可讀可寫特性。需要生成getter方法和setter方法。
2). readonly 是隻讀特性。只會生成getter方法,不會生成setter方法,不希望屬性在類外改變。
3). assign 是賦值特性。setter方法將傳入參數賦值給實例變量;僅設置變量時,assign用於基本數據類型。
4). retain(MRC)/strong(ARC) 表示持有特性。setter方法將傳入參數先保留,再賦值,傳入參數的retaincount會+1。
5). copy 表示拷貝特性。setter方法將傳入對象複製一份,需要完全一份新的變量時。
6). nonatomic 非原子操作。決定編譯器生成的setter和getter方法是否是原子操作,atomic表示多線程安全,一般使用nonatomic,效率高。

八.什麼情況使用 weak 關鍵字,相比 assign 有什麼不同?

1.在 ARC 中,在有可能出現循環引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性。
2.自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當然,也可以使用strong。

IBOutlet連出來的視圖屬性爲什麼可以被設置成weak?因爲父控件的subViews數組已經對它有一個強引用。

不同點:
assign 可以用非 OC 對象,而 weak 必須用於 OC 對象。
weak 表明該屬性定義了一種“非擁有關係”。在屬性所指的對象銷燬時,屬性值會自動清空(nil)。

九.怎麼用 copy 關鍵字?

 用途:
 1. NSString、NSArray、NSDictionary 等等經常使用copy關鍵字,是因爲他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
 2. block 也經常使用 copy 關鍵字。

 說明:
 block 使用 copy 是從 MRC 遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對於 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。如果不寫 copy ,該類的調用者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”,他們有可能會在調用之前自行拷貝屬性值。這種操作多餘而低效。

十.用@property聲明的 NSString / NSArray / NSDictionary 經常使用 copy 關鍵字,爲什麼?如果改用strong關鍵字,可能造成什麼問題?

答:用 @property 聲明 NSString、NSArray、NSDictionary 經常使用 copy 關鍵字,是因爲他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進行賦值操作(就是把可變的賦值給不可變的),爲確保對象中的字符串值不會無意間變動,應該在設置新屬性值時拷貝一份。

1. 因爲父類指針可以指向子類對象,使用 copy 的目的是爲了讓本對象的屬性不受外界影響,使用 copy 無論給我傳入是一個可變對象還是不可對象,我本身持有的就是一個不可變的副本。
2. 如果我們使用是 strong ,那麼這個屬性就有可能指向一個可變對象,如果這個可變對象在外部被修改了,那麼會影響該屬性。

//總結:使用copy的目的是,防止把可變類型的對象賦值給不可變類型的對象時,可變類型對象的值發送變化會無意間篡改不可變類型對象原來的值。

十一.淺拷貝和深拷貝的區別?

答:
淺拷貝:只複製指向對象的指針,而不復制引用對象本身。
深拷貝:複製引用對象本身。內存中存在了兩份獨立對象本身,當修改A時,A_copy不變。

十二.這個寫法會出什麼問題:@property (nonatomic, copy) NSMutableArray *arr;

問題:添加,刪除,修改數組內的元素的時候,程序會因爲找不到對應的方法而崩潰。

原因:是因爲 copy 就是複製一個不可變 NSArray 的對象,不能對 NSArray 對象進行添加/修改。

十三.如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?

若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分爲可變版本與不可變版本,那麼就要同時實現 NSCopying 與 NSMutableCopying 協議。
具體步驟:
	1. 需聲明該類遵從 NSCopying 協議
	2. 實現 NSCopying 協議的方法。


十四.常見的 Objective-C 的數據類型有那些,和C的基本數據類型有什麼區別?如:NSInteger和int

答:
Objective-C的數據類型有NSString,NSNumber,NSArray,NSMutableArray,NSData等等,這些都是class,創建後便是對象,而C語言的基本數據類型int,只是一定字節的內存空間,用於存放數值;NSInteger是基本數據類型,並不是NSNumber的子類,當然也不是NSObject的子類。NSInteger是基本數據類型Int或者Long的別名(NSInteger的定義typedef long NSInteger),它的區別在於,NSInteger會根據系統是32位還是64位來決定是本身是int還是long。

十五.id 聲明的對象有什麼特性?IOS開發之__bridge,__bridge_transfer和__bridge_retained

答:id 聲明的對象具有運行時的特性,即可以指向任意類型的Objcetive-C的對象。

我們先來看一下ARC無效的時候,我們寫id類型轉void*類型的寫法:

id obj = [[NSObject alloc] init];
void *p = obj;
反過來,當把void*對象變回id類型時,只是簡單地如下來寫,

id obj = p;
[obj release];
但是上面的代碼在ARC有效時,就有了下面的錯誤:

    error: implicit conversion of an Objective-C pointer
        to ’void *’ is disallowed with ARC
        void *p = obj;
                  ^
 
    error: implicit conversion of a non-Objective-C pointer
        type ’void *’ to ’id’ is disallowed with ARC
        id o = p;
                ^

__bridge
爲了解決這一問題,我們使用 __bridge 關鍵字來實現id類型與void*類型的相互轉換。看下面的例子。

id obj = [[NSObject alloc] init];
 
void *p = (__bridge void *)obj;
 
id o = (__bridge id)p;
將Objective-C的對象類型用 __bridge 轉換爲 void* 類型和使用 __unsafe_unretained 關鍵字修飾的變量是一樣的。被代入對象的所有者需要明確對象生命週期的管理,不要出現異常訪問的問題。

除過 __bridge 以外,還有兩個 __bridge 相關的類型轉換關鍵字:

__bridge_transfer
__bridge_retained
接下來,我們將看看這兩個關鍵字的區別。

__bridge_retained
先來看使用 __bridge_retained 關鍵字的例子程序:

id obj = [[NSObject alloc] init];
 
void *p = (__bridge_retained void *)obj;

從名字上我們應該能理解其意義:類型被轉換時,其對象的所有權也將被變換後變量所持有。如果不是ARC代碼,類似下面的實現:

id obj = [[NSObject alloc] init];
 
void *p = obj;
[(id)p retain];

可以用一個實際的例子驗證,對象所有權是否被持有。

void *p = 0;
 
{
    id obj = [[NSObject alloc] init];
    p = (__bridge_retained void *)obj;
}
 
NSLog(@"class=%@", [(__bridge id)p class]);
出了大括號的範圍後,p 仍然指向一個有效的實體。說明他擁有該對象的所有權,該對象沒有因爲出其定義範圍而被銷燬。

__bridge_transfer
相反,當想把本來擁有對象所有權的變量,在類型轉換後,讓其釋放原先所有權的時候,需要使用 __bridge_transfer 關鍵字。文字有點繞口,我們還是來看一段代碼吧。

如果ARC無效的時候,我們可能需要寫下面的代碼。

// p 變量原先持有對象的所有權
id obj = (id)p;
[obj retain];
[(id)p release];
那麼ARC有效後,我們可以用下面的代碼來替換:

// p 變量原先持有對象的所有權
id obj = (__bridge_transfer id)p;
可以看出來,__bridge_retained 是編譯器替我們做了 retain 操作,而 __bridge_transfer 是替我們做了 release1。

Toll-Free bridged
在iOS世界,主要有兩種對象:Objective-C 對象和 Core Foundation 對象0。Core Foundation 對象主要是有C語言實現的 Core Foundation Framework 的對象,其中也有對象引用計數的概念,只是不是 Cocoa Framework::Foundation Framework 的 retain/release,而是自身的 CFRetain/CFRelease 接口。

這兩種對象間可以互相轉換和操作,不使用ARC的時候,單純的用C原因的類型轉換,不需要消耗CPU的資源,所以叫做 Toll-Free bridged。比如 NSArray和CFArrayRef, NSString和CFStringRef,他們雖然屬於不同的 Framework,但是具有相同的對象結構,所以可以用標準C的類型轉換。

比如不使用ARC時,我們用下面的代碼:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (CFStringRef)string;
同樣,Core Foundation類型向Objective-C類型轉換時,也是簡單地用標準C的類型轉換即可。

但是在ARC有效的情況下,將出現類似下面的編譯錯誤:

    Cast of Objective-C pointer type ‘NSString *’ to C pointer type ‘CFStringRef’ (aka ‘const struct __CFString *’) requires a bridged cast
    Use __bridge to convert directly (no change in ownership)
    Use __bridge_retained to make an ARC object available as a +1 ‘CFStringRef’ (aka ‘const struct __CFString *’)
錯誤中已經提示了我們需要怎樣做:用 __bridge 或者 __bridge_retained 來轉型,其差別就是變更對象的所有權。

正因爲Objective-C是ARC管理的對象,而Core Foundation不是ARC管理的對象,所以纔要特意這樣轉換,這與id類型向void*轉換是一個概念。也就是說,當這兩種類型(有ARC管理,沒有ARC管理)在轉換時,需要告訴編譯器怎樣處理對象的所有權。

上面的例子,使用 __bridge/__bridge_retained 後的代碼如下:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge CFStringRef)string;
只是單純地執行了類型轉換,沒有進行所有權的轉移,也就是說,當string對象被釋放的時候,cfString也不能被使用了。

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge_retained CFStringRef)string;
...
CFRelease(cfString); // 由於Core Foundation的對象不屬於ARC的管理範疇,所以需要自己release
使用 __bridge_retained 可以通過轉換目標處(cfString)的 retain 處理,來使所有權轉移。即使 string 變量被釋放,cfString 還是可以使用具體的對象。只是有一點,由於Core Foundation的對象不屬於ARC的管理範疇,所以需要自己release。

實際上,Core Foundation 內部,爲了實現Core Foundation對象類型與Objective-C對象類型的相互轉換,提供了下面的函數。

CFTypeRef  CFBridgingRetain(id  X)  {
    return  (__bridge_retained  CFTypeRef)X;
}
 
id  CFBridgingRelease(CFTypeRef  X)  {
    return  (__bridge_transfer  id)X;
}
所以,可以用 CFBridgingRetain 替代 __bridge_retained 關鍵字:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = CFBridgingRetain(string);
...
CFRelease(cfString); // 由於Core Foundation不在ARC管理範圍內,所以需要主動release。
__bridge_transfer
所有權被轉移的同時,被轉換變量將失去對象的所有權。當Core Foundation對象類型向Objective-C對象類型轉換的時候,會經常用到 __bridge_transfer 關鍵字。

CFStringRef cfString = CFStringCreate...();
NSString *string = (__bridge_transfer NSString *)cfString;
 
// CFRelease(cfString); 因爲已經用 __bridge_transfer 轉移了對象的所有權,所以不需要調用 release
同樣,我們可以使用 CFBridgingRelease() 來代替 __bridge_transfer 關鍵字。

CFStringRef cfString = CFStringCreate...();
NSString *string = CFBridgingRelease(cfString);

十六.Objective-C 如何對內存管理的,說說你的看法和解決方法?

答:Objective-C的內存管理主要有三種方式ARC(自動內存計數)、手動內存計數、內存池。
1). 自動內存計數ARC:由Xcode自動在App編譯階段,在代碼中添加內存管理代碼。
2). 手動內存計數MRC:遵循內存誰申請、誰釋放;誰添加,誰釋放的原則。
3). 內存釋放池Release Pool:把需要釋放的內存統一放在一個池子中,當池子被抽乾後(drain),池子中所有的內存空間也被自動釋放掉。內存池的釋放操作分爲自動和手動。自動釋放受runloop機制影響。

十七.AES和DES加密

1、對稱加密:

需要對加密和解密使用相同密鑰的加密算法。由於其速度快,對稱性加密通常在消息發送方需要加密大量數據時使用。對稱性加密也稱爲密鑰加密。 
所謂對稱,就是採用這種加密方法的雙方使用方式用同樣的密鑰進行加密和解密。密鑰是控制加密及解密過程的指令。算法是一組規則,規定如何進行加密和解密。 
因此 [1] 加密的安全性不僅取決於加密算法本身,密鑰管理的安全性更是重要。因爲加密和解密都使用同一個密鑰,如何把密鑰安全地傳遞到解密者手上就成了必須要解決的問題。 

DES

DES(全程Data Encryption Standard)即數據加密標準,是一種使用密鑰加密的塊算法,1977年被美國聯邦政府的國家標準局確定爲聯邦資料處理標準(FIPS),並授權在非密級政府通信中使用,隨後該算法在國際上廣泛流傳開來。需要注意的是,在某些文獻中,作爲算法的DES稱爲數據加密算法(Data Encryption Algorithm,DEA),已與作爲標準的DES區分開來。


DES代碼

.h文件

// 加密方法
+ (NSString*)encrypt:(NSString*)plainText;
// 解密方法
+ (NSString*)decrypt:(NSString*)encryptText;
.m文件

// 加密方法
+ (NSString*)encrypt:(NSString*)plainText {
    NSData* data = [plainText dataUsingEncoding:NSUTF8StringEncoding];
    size_t plainTextBufferSize = [data length];
    const void *vplainText = (const void *)[data bytes];
    
    CCCryptorStatus ccStatus;
    uint8_t *bufferPtr = NULL;
    size_t bufferPtrSize = 0;
    size_t movedBytes = 0;
    
    bufferPtrSize = (plainTextBufferSize + kCCBlockSize3DES) & ~(kCCBlockSize3DES - 1);
    bufferPtr = malloc( bufferPtrSize * sizeof(uint8_t));
    memset((void *)bufferPtr, 0x0, bufferPtrSize);
    
    const void *vkey = (const void *) [gkey UTF8String];
    const void *vinitVec = (const void *) [gIv UTF8String];
    
    ccStatus = CCCrypt(kCCEncrypt,
                       kCCAlgorithm3DES,
                       kCCOptionPKCS7Padding,
                       vkey,
                       kCCKeySize3DES,
                       vinitVec,
                       vplainText,
                       plainTextBufferSize,
                       (void *)bufferPtr,
                       bufferPtrSize,
                       &movedBytes);
    
    NSData *myData = [NSData dataWithBytes:(const void *)bufferPtr length:(NSUInteger)movedBytes];
    NSString *result = [GTMBase64 stringByEncodingData:myData];
    return result;
}

// 解密方法
+ (NSString*)decrypt:(NSString*)encryptText {
    NSData *encryptData = [GTMBase64 decodeData:[encryptText dataUsingEncoding:NSUTF8StringEncoding]];
    size_t plainTextBufferSize = [encryptData length];
    const void *vplainText = [encryptData bytes];
    
    CCCryptorStatus ccStatus;
    uint8_t *bufferPtr = NULL;
    size_t bufferPtrSize = 0;
    size_t movedBytes = 0;
    
    bufferPtrSize = (plainTextBufferSize + kCCBlockSize3DES) & ~(kCCBlockSize3DES - 1);
    bufferPtr = malloc( bufferPtrSize * sizeof(uint8_t));
    memset((void *)bufferPtr, 0x0, bufferPtrSize);
    
    const void *vkey = (const void *) [gkey UTF8String];
    const void *vinitVec = (const void *) [gIv UTF8String];
    
    ccStatus = CCCrypt(kCCDecrypt,
                       kCCAlgorithm3DES,
                       kCCOptionPKCS7Padding,
                       vkey,
                       kCCKeySize3DES,
                       vinitVec,
                       vplainText,
                       plainTextBufferSize,
                       (void *)bufferPtr,
                       bufferPtrSize,
                       &movedBytes);
    
    NSString *result = [[NSString alloc] initWithData:[NSData dataWithBytes:(const void *)bufferPtr
                                                                      length:(NSUInteger)movedBytes] encoding:NSUTF8StringEncoding];
    return result;
}
.m文件需要導入的頭文件及宏定義

#import "DESUtil.h"
#import <CommonCrypto/CommonCryptor.h>
#import "GTMBase64.h"
#define gkey            @"0123456789ABCDEFGHI"
#define gIv             @"01234567"
使用的話 只需更改gkey及gIv。

重點: 
- 如果密碼位數少於等於64位,加密結果與DES相同; 
- 祕鑰長度128位,192位,即16或24個字符組成的字符串; 
- 常用ECB 和 BCB 模式加密計算。


AES

AES(全稱Advance Encryption Standard)高級加密標準,在密碼學中又成Rijndael加密法,是美國聯邦政府採用的一種區塊加密標準。這個標準用來替代原先的DES。AES屬於塊加密(Block Cipher),塊加密中有CBC、ECB、CTR、OFB、CFB等幾種工作模式。常用的爲CBC和ECB模式,

ECB模式

這種模式是將整個明文分成若干段相同的小段,然後對每一小段進行加密。

CBC模式

這種模式是先將明文切分成若干小段,然後每一小段與初始塊或者上一段的密文段進行異或運算後,再與密鑰進行加密。
需要注意的是CBC模式需要傳一個16位的向量值,不傳的話,默認爲空,空的話就屬於ECB模式。

AES代碼(CBC模式)

.h文件

NSString * aesEncryptString(NSString *content, NSString *key);
NSString * aesDecryptString(NSString *content, NSString *key);
NSData * aesEncryptData(NSData *data, NSData *key);
NSData * aesDecryptData(NSData *data, NSData *key);
.m文件

NSData * cipherOperation(NSData *contentData, NSData *keyData, CCOperation operation) {
    NSUInteger dataLength = contentData.length;
    
    void const *initVectorBytes = [kInitVector dataUsingEncoding:NSUTF8StringEncoding].bytes;
    void const *contentBytes = contentData.bytes;
    void const *keyBytes = keyData.bytes;
    
    size_t operationSize = dataLength + kCCBlockSizeAES128;
    void *operationBytes = malloc(operationSize);
    size_t actualOutSize = 0;
    
    CCCryptorStatus cryptStatus = CCCrypt(operation,
                                          kCCAlgorithmAES,
                                          kCCOptionPKCS7Padding,
                                          keyBytes,
                                          kKeySize,
                                          initVectorBytes,
                                          contentBytes,
                                          dataLength,
                                          operationBytes,
                                          operationSize,
                                          &actualOutSize);
    
    if (cryptStatus == kCCSuccess) {
        return [NSData dataWithBytesNoCopy:operationBytes length:actualOutSize];
    }
    free(operationBytes);
    return nil;
}

NSString * aesEncryptString(NSString *content, NSString *key) {
    NSData *contentData = [content dataUsingEncoding:NSUTF8StringEncoding];
    NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
    NSData *encrptedData = aesEncryptData(contentData, keyData);
    return [encrptedData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}

NSString * aesDecryptString(NSString *content, NSString *key) {
    NSData *contentData = [[NSData alloc] initWithBase64EncodedString:content options:NSDataBase64DecodingIgnoreUnknownCharacters];
    NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
    NSData *decryptedData = aesDecryptData(contentData, keyData);
    return [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding];
}

NSData * aesEncryptData(NSData *contentData, NSData *keyData) {
    return cipherOperation(contentData, keyData, kCCEncrypt);
}

NSData * aesDecryptData(NSData *contentData, NSData *keyData) {
    return cipherOperation(contentData, keyData, kCCDecrypt);
}
.m文件中需要定義兩個常量

NSString const *kInitVector = @"16-Bytes--String";
size_t const kKeySize = kCCKeySizeAES128;
第一個爲向量值,如果直接傳空字符串,則爲ECB模式,否則爲CBC。本例中爲CBC模式。


十八.Category(類別)、 Extension(擴展)和繼承的區別

區別:
1. 分類有名字,類擴展沒有分類名字,是一種特殊的分類。
2. 分類只能擴展方法(屬性僅僅是聲明,並沒真正實現),類擴展可以擴展屬性、成員變量和方法。
3. 繼承可以增加,修改或者刪除方法,並且可以增加屬性。

十九.網絡、Http與Https

1.網絡基礎

001 問題:爲什麼要學習網絡編程?
        回答:(1)網絡編程是一種實時更新應用數據的常用手段
             (2)網絡編程是開發優秀網絡應用的前提和基礎

    002 網絡基本概念
        2-1 客戶端(就是手機或者ipad等手持設備上面的APP)
        2-2 服務器(遠程服務器-本地服務器)
        2-3 請求(客戶端索要數據的方式)
        2-4 響應(需要客戶端解析數據)
        2-5 數據庫(服務器的數據從哪裏來)

2.Http
001 URL
        1-1 如何找到服務器(通過一個唯一的URL)
        1-2 URL介紹
            a. 統一資源定位符
            b. url格式(協議\主機地址\路徑)
                協議:不同的協議,代表着不同的資源查找方式、資源傳輸方式
                主機地址:存放資源的主機(服務器)的IP地址(域名)
                路徑:資源在主機(服務器)中的具體位置

        1-3 請求協議
            【file】訪問的是本地計算機上的資源,格式是file://(不用加主機地址)
            【ftp】訪問的是共享主機的文件資源,格式是ftp://
            【mailto】訪問的是電子郵件地址,格式是mailto:
            【http】超文本傳輸協議,訪問的是遠程的網絡資源,格式是http://(網絡請求中最常用的協議)

    002 http協議
        2-1 http協議簡單介紹
            a.超文本傳輸協議
            b.規定客戶端和服務器之間的數據傳輸格式
            c.讓客戶端和服務器能有效地進行數據溝通

        2-2 http協議優缺點
            a.簡單快速(協議簡單,服務器端程序規模小,通信速度快)
            b.靈活(允許傳輸各種數據)
            c.非持續性連接(1.1之前版本是非持續的,即限制每次連接只處理一個請求,
              服務器對客戶端的請求做出響應後,馬上斷開連接,這種方式可以節省傳輸時間)
        2-3 基本通信過程
            a.請求:客戶端向服務器索要數據
            b.響應:服務器返回客戶端相應的數據

    003 GET和POST請求
        3-1 http裏面發送請求的方法
        GET(常用)、POST(常用)、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT、PATCH

        3-2 GET和POST請求的對比【區別在於參數如何傳遞】
            GET
            在請求URL後面以?的形式跟上發給服務器的參數,多個參數之間用&隔開,比如
            http://ww.test.com/login?username=123&pwd=234&type=JSON
            由於瀏覽器和服務器對URL長度有限制,因此在URL後面附帶的參數是有限制的,通常不能超過1KB

            POST
            發給服務器的參數全部放在請求體中
            理論上,POST傳遞的數據量沒有限制(具體還得看服務器的處理能力)

        3-3 如何選擇【除簡單數據查詢外,其它的一律使用POST請求】
            a.如果要傳遞大量數據,比如文件上傳,只能用POST請求
            b.GET的安全性比POST要差些,如果包含機密\敏感信息,建議用POST
            c.如果僅僅是索取數據(數據查詢),建議使用GET
            d.如果是增加、修改、刪除數據,建議使用POST
    004 iOS中發送http請求的方案
        4-1 蘋果原生
            NSURLConnection 03年推出的古老技術
            NSURLSession    13年推出iOS7之後,以取代NSURLConnection【重點】
            CFNetwork       底層技術、C語言的

        4-2 第三方框架
            ASIHttpRequest
            AFNetworking        【重點】
            MKNetworkKit

    005 http請求通信過程
        5-1 請求
            【包括請求頭+請求體·非必選】
        5-2 響應
            【響應頭+響應體】
        5-3 通信過程
            a.發送請求的時候把請求頭和請求體(請求體是非必須的)包裝成一個請求對象
            b.服務器端對請求進行響應,在響應信息中包含響應頭和響應體,響應信息是對服務器端的描述,
              具體的信息放在響應體中傳遞給客戶端
        5-4 狀態碼
            【200】:請求成功
            【400】:客戶端請求的語法錯誤,服務器無法解析
            【404】:無法找到資源
            【500】:服務器內部錯誤,無法完成請求  

3.Https

1.https簡單說明
  1) HTTPS(全稱:Hyper Text Transfer Protocol over Secure Socket Layer),
      是以安全爲目標的HTTP通道,簡單講是HTTP的安全版。
  2) 即HTTP下加入SSL層,HTTPS的安全基礎是SSL,因此加密的詳細內容就需要SSL。 
      它是一個URI scheme(抽象標識符體系),句法類同http:體系。用於安全的HTTP數據傳輸。
  3) https:URL表明它使用了HTTP,但HTTPS存在不同於HTTP的默認端口及一個加密/身份驗證層(在HTTP與TCP之間)。

2.HTTPS和HTTP的區別主要爲以下四點:
  1) https協議需要到ca申請證書,一般免費證書很少,需要交費。
  2) http是超文本傳輸協議,信息是明文傳輸,https 則是具有安全性的ssl加密傳輸協議。
  3) http和https使用的是完全不同的連接方式,用的端口也不一樣,前者是80,後者是443。
  4) http的連接很簡單,是無狀態的;HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,比http協議安全。

3.簡單說明
  1) HTTPS的主要思想是在不安全的網絡上創建一安全信道,並可在使用適當的加密包和服務器證書可被驗證且可被信任時,
    對竊聽和中間人攻擊提供合理的保護。
  2)HTTPS的信任繼承基於預先安裝在瀏覽器中的證書頒發機構(如VeriSign、Microsoft等)
   (意即“我信任證書頒發機構告訴我應該信任的”)。
  3)因此,一個到某網站的HTTPS連接可被信任,如果服務器搭建自己的https 也就是說採用自認證的方式來建立https信道,
    這樣一般在客戶端是不被信任的。
  4)所以我們一般在瀏覽器訪問一些https站點的時候會有一個提示,問你是否繼續。

4.對開發的影響。
4.1 如果是自己使用NSURLSession來封裝網絡請求,涉及代碼如下。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] 
                                          delegate:self 
                                          delegateQueue:[NSOperationQueue mainQueue]];

    NSURLSessionDataTask *task =  [session dataTaskWithURL:[NSURL URLWithString:@"https://www.apple.com"] 
                                           completionHandler:^(NSData *data, 
                                                               NSURLResponse *response, 
                                                               NSError *error) {
        NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    }];
    [task resume];
}

/*
 只要請求的地址是HTTPS的, 就會調用這個代理方法
 我們需要在該方法中告訴系統, 是否信任服務器返回的證書
 Challenge: 挑戰 質問 (包含了受保護的區域)
 protectionSpace : 受保護區域
 NSURLAuthenticationMethodServerTrust : 證書的類型是 服務器信任
 */
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
        completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, 
                                    NSURLCredential *))completionHandler
{
    //    NSLog(@"didReceiveChallenge %@", challenge.protectionSpace);
    NSLog(@"調用了最外層");
    // 1.判斷服務器返回的證書類型, 是否是服務器信任
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSLog(@"調用了裏面這一層是服務器信任的證書");
        /*
         NSURLSessionAuthChallengeUseCredential = 0,                     使用證書
         NSURLSessionAuthChallengePerformDefaultHandling = 1,            忽略證書(默認的處理方式)
         NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,     忽略書證, 並取消這次請求
         NSURLSessionAuthChallengeRejectProtectionSpace = 3,            拒絕當前這一次, 下一次再詢問
         */
//        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        NSURLCredential *card = [[NSURLCredential alloc]initWithTrust:challenge.protectionSpace.serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential , card);
    }
}

4.2 如果是使用AFN框架,那麼我們不需要做任何額外的操作,AFN內部已經做了處理。

4 URL中文轉碼問題

//1.確定請求路徑

    NSString *urlStr = @"http://120.25.226.186:32812/login2?username=哈哈哈&pwd=123";
    NSLog(@"%@",urlStr);
    //中文轉碼操作
    urlStr =  [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"%@",urlStr);

    NSURL *url = [NSURL URLWithString:urlStr];

二十. 設計一個簡單Memory Cache 使用LRU算法

一.Swift語言代碼

 class Node(object):
    def __init__(self, key=None, value=None, next=None, prev=None):
        self.key = key
        self.value = value
        self.next = next
        self.prev = prev
class LRUCache(object):
    def __init__(self, capacity):
        """
        :type capacity: int
        """
        self.capacity = capacity
        #帶有頭節點的單個鏈表
        #總是將新節點放在尾部
        #也將修改後的節點移到尾部
        self.head = Node()
        self.tail = self.head
        self.head.next = self.tail
        # <key, node.prev>
        self.hash_table = {}
    def pop_front(self):
        del self.hash_table[self.head.next.key]
        p_next = self.head.next.next
        self.head.next = p_next
        #更新新前端節點的引用
        self.hash_table[self.head.next.key] = self.head
    def append(self, node):
        self.hash_table[node.key] = self.tail
        self.tail.next = node
        self.tail = node
    def move_to_end(self, prev):
        node = prev.next
        if node == self.tail:
            return
        # 斷開節點
        prev.next = node.next
        node.next = None
        self.hash_table[prev.next.key] = prev
        # 追加節點
        self.append(node)
    def get(self, key):
        """
        :type key: int
        :rtype: int
        """
        if key not in self.hash_table:
            return -1
        prev = self.hash_table[key]
        val = prev.next.value
        self.move_to_end(prev)
        return val
    def put(self, key, value):
        """
        :type key: int
        :type value: int
        :rtype: void
        """
        if key in self.hash_table:
            prev = self.hash_table[key]
            prev.next.value = value
            self.move_to_end(prev)
        else:
            self.append(Node(key, value))
            if len(self.hash_table) > self.capacity:
                self.pop_front()

  
二.OC語言代碼
  
  struct Node{
    int _key;
    int _value;
    Node* _next;
    Node(int key,int value,Node* next):_key(key),_value(value),_next(next){}
};

class LRUCache{
public:
    LRUCache(int capacity) {
        _capacity   = capacity;
        _size       = 0;
        _last       = 0;
        _cur_begin  = _begin = (char *) malloc(sizeof(Node)*(capacity+1));
        _head       = new (_cur_begin) Node(0,0,NULL);//在指定內存上構造對象
        _cur_begin += sizeof(Node);
    }
    ~LRUCache(){
        if(_begin!=NULL){
            while(_cur_begin > _begin){
                _cur_begin -= sizeof(Node);
                ((Node*)_cur_begin)->~Node();//先釋放內存上的對象
            }
            free(_begin);//再釋放內存
        }
    }
    int get(int key) {
        int value             = -1;//初始時假設key對應的結點不存在
        Node* pre_node_of_key = umap_prenodes[key];//key對應的結點的前驅結點
        if(pre_node_of_key !=NULL){//key結點存在
            Node* node             = pre_node_of_key->_next;//key對應的結點
            pre_node_of_key->_next = node->_next;
            if(pre_node_of_key->_next!=NULL){
                umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;
            }
            node->_next            = _head->_next;
            if(node->_next!=NULL){//node有後繼,更新後繼的前驅結點
                umap_prenodes[node->_next->_key] = node;
            }
            _head->_next           = node;
            umap_prenodes[key]     = _head;              
            /*更新_last*/
            if(_last == key ){
                _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); 
            }

            value = node->_value;
        }
        return value;
    }

    void set(int key, int value) {
        Node* node            = NULL;
        Node* pre_node_of_key = umap_prenodes[key];//key對應的結點的前驅結點
        if(pre_node_of_key != NULL){//key對應的結點存在,孤立key對應的結點,也就是從鏈表中把結點取出來,重新鏈接鏈表
            node                   = pre_node_of_key->_next;//key對應的結點
            pre_node_of_key->_next = node->_next;

            if(pre_node_of_key->_next!=NULL){
                umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;//更新前驅
            }

            node->_value           = value; //重置結點值

            /*更新_last*/
            if(_last == key ){
                _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); 
            }
        }else{//結點不存在

            if(_capacity == 0){//緩衝區爲空
                return ;
            }

            if(_size == _capacity){//緩存滿,重用最後一個結點

                Node* pre_node_of_last    = umap_prenodes[_last];//最後一個結點的前驅結點

                umap_prenodes[pre_node_of_last->_next->_key] = NULL;
                
                node                      = new (pre_node_of_last->_next) Node(key,value,NULL);//重用最後一個結點

                pre_node_of_last->_next   = NULL;//移出最後一個結點

                _last = ( pre_node_of_last == _head ? key : pre_node_of_last->_key ); //更新指向最後一個結點的key

            }else{//緩衝未滿,使用新結點

                node    = new (_cur_begin) Node(key,value,NULL);
                _cur_begin += sizeof(Node);
                _size++;
                if(_size==1){
                    _last = key;
                }
            }
        }

        /*把node插入到第一個結點的位置*/
        node->_next            = _head->_next;
        if(node->_next!=NULL){//node有後繼,更新後繼的前驅結點
            umap_prenodes[node->_next->_key] = node;
        }
        _head->_next           = node;
        umap_prenodes[key]     = _head;  

    }

private:
    int   _size;
    int   _capacity;
    int   _last;//_last是鏈表中最後一個結點的key
    Node* _head;
    unordered_map<int,Node*> umap_prenodes;//存儲key對應的結點的前驅結點,鏈表中第一個結點的前驅結點爲_head
    
    char* _begin;//緩存的起始位置 
    char* _cur_begin;//用於分配結點內存的起始位置
};

二十一.爲什麼我們常見的delegate屬性都用是week而不是retain/strong?

答:是爲了防止delegate兩端產生不必要的循環引用。
@property (nonatomic, weak) id<UITableViewDelegate> delegate;

二十二.delete Notification KVO 區別

一.delegate的優勢:
  1.非常嚴格的語法。所有將聽到的事件必須是在delegate協議中有清晰的定義。
  2.如果delegate中的一個方法沒有實現那麼就會出現編譯警告/錯誤
  3.協議必須在controller的作用域範圍內定義
  4.在一個應用中的控制流程是可跟蹤的並且是可識別的;
  5.在一個控制器中可以定義定義多個不同的協議,每個協議有不同的delegates
  6.沒有第三方對象要求保持/監視通信過程。
  7.能夠接收調用的協議方法的返回值。這意味着delegate能夠提供反饋信息給controller
 缺點:
  1.需要定義很多代碼:1.協議定義;2.controller的delegate屬性;3.在delegate本身中實現delegate方法定義
  2.在釋放代理對象時,需要小心的將delegate改爲nil。一旦設定失敗,那麼調用釋放對象的方法將會出現內存crash
  3.在一個controller中有多個delegate對象,並且delegate是遵守同一個協議,但還是很難告訴多個對象同一個事件,不過有可能。

二.notification
  優勢:
  1.不需要編寫多少代碼,實現比較簡單;
  2.對於一個發出的通知,多個對象能夠做出反應,即1對多的方式實現簡單
  3.controller能夠傳遞context對象(dictionary),context對象攜帶了關於發送通知的自定義的信息
 缺點:
  1.在編譯期不會檢查通知是否能夠被觀察者正確的處理; 
  2.在釋放註冊的對象時,需要在通知中心取消註冊;
  3.在調試的時候應用的工作以及控制過程難跟蹤;
  4.需要第三方對喜愛那個來管理controller與觀察者對象之間的聯繫;
  5.controller和觀察者需要提前知道通知名稱、UserInfo dictionary keys。如果這些沒有在工作區間定義,那麼會出現不同步的情況;
  6.通知發出後,controller不能從觀察者獲得任何的反饋信息。

三.KVO
 優點:
  1.能夠提供一種簡單的方法實現兩個對象間的同步。例如:model和view之間同步;
  2.能夠對非我們創建的對象,即內部對象的狀態改變作出響應,而且不需要改變內部對象(SKD對象)的實現;
  3.能夠提供觀察的屬性的最新值以及先前值;
  4.用key paths來觀察屬性,因此也可以觀察嵌套對象;
  5.完成了對觀察對象的抽象,因爲不需要額外的代碼來允許觀察值能夠被觀察
 缺點:
  1.我們觀察的屬性必須使用strings來定義。因此在編譯器不會出現警告以及檢查;
  2.對屬性重構將導致我們的觀察代碼不再可用;
  3.複雜的“IF”語句要求對象正在觀察多個值。這是因爲所有的觀察代碼通過一個方法來指向;
  4.當釋放觀察者時不需要移除觀察者。

二十三.開發中常用的鎖有如下幾種:

(1).@synchronized

 - (void)lock1 {
    @synchronized (self) {
        // 加鎖操作
    }
}

(2).NSLock 對象鎖

- (void)lock2 {
    NSLock *xwlock = [[NSLock alloc] init];
    XWLogBlock logBlock = ^ (NSArray *array) {
        [xwlock lock];
        for (id obj in array) {
            NSLog(@"%@",obj);
        }
        [xwlock unlock];
    };

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *array = @[@1,@2,@3];
        logBlock(array);
    });
}

死鎖
- (void)lock5 {
    NSLock *commonLock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^XWRecursiveBlock)(int);

        XWRecursiveBlock = ^(int  value) {
            [commonLock lock];
            if (value > 0) {
                NSLog(@"加鎖層數: %d",value);
                sleep(1);
                XWRecursiveBlock(--value);
            }
            NSLog(@"程序退出!");
            [commonLock unlock];
        };

        XWRecursiveBlock(3);
    });
}

(3).NSRecursiveLock 遞歸鎖

- (void)lock4 {
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^XWRecursiveBlock)(int);

        XWRecursiveBlock = ^(int  value) {
            [recursiveLock lock];
            if (value > 0) {
                NSLog(@"加鎖層數: %d",value);
                sleep(1);
                XWRecursiveBlock(--value);
            }
            NSLog(@"程序退出!");
            [recursiveLock unlock];
        };

        XWRecursiveBlock(3);
    });
}


(4).NSConditionLock 條件鎖

- (void)lock11 {
    NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
    NSMutableArray *arrayM = [NSMutableArray array];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionLock lock];

        for (int i = 0; i < 6; i++) {
            [arrayM addObject:@(i)];
            NSLog(@"異步下載第 %d 張圖片",i);
            sleep(1);
            if (arrayM.count == 4) {
                [conditionLock unlockWithCondition:4];
            }
        }
    });

    dispatch_async(dispatch_get_main_queue(), ^{
        [conditionLock lockWhenCondition:4];
        NSLog(@"已經獲取到4張圖片->主線程渲染");
        [conditionLock unlock];
    });
}

(5).pthread_mutex 互斥鎖(C語言)

 __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"+++++ 線程1 start");
        pthread_mutex_lock(&mutex);
        sleep(2);
        pthread_mutex_unlock(&mutex);
        NSLog(@"+++++ 線程1 end");
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"----- 線程2 start");
        pthread_mutex_lock(&mutex);
        sleep(3);
        pthread_mutex_unlock(&mutex);
        NSLog(@"----- 線程2 end");
    });
}

(6).dispatch_semaphore 信號量實現加鎖(GCD)

- (void)lock7 {
//    dispatch_semaphore_create //創建一個信號量 semaphore
//    dispatch_semaphore_signal //發送一個信號 信號量+1
//    dispatch_semaphore_wait   // 等待信號 信號量-1

    /// 需求: 異步線程的兩個操作同步執行

    dispatch_semaphore_t semaphone = dispatch_semaphore_create(0);
    NSLog(@"start");

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"async .... ");
        sleep(5);
        /// 線程資源 + 1
        dispatch_semaphore_signal(semaphone);//信號量+1
    });
    /// 當前線程資源數量爲 0 ,等待激活
    dispatch_semaphore_wait(semaphone, DISPATCH_TIME_FOREVER);
    NSLog(@"end");
}


(7).OSSpinLock

二十四.KVC的底層實現?

當一個對象調用setValue方法時,方法內部會做以下操作:
1). 檢查是否存在相應的key的set方法,如果存在,就調用set方法。
2). 如果set方法不存在,就會查找與key相同名稱並且帶下劃線的成員變量,如果有,則直接給成員變量屬性賦值。
3). 如果沒有找到_key,就會查找相同名稱的屬性key,如果有就直接賦值。
4). 如果還沒有找到,則調用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
這些方法的默認實現都是拋出異常,我們可以根據需要重寫它們。

二十五.KVO內部實現原理

1.KVO是基於runtime機制實現的
2.當某個類的屬性對象第一次被觀察時,系統就會在運行期動態地創建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內實現真正的通知機制
3.如果原類爲Person,那麼生成的派生類名爲NSKVONotifying_Person
4.每個類對象中都有一個isa指針指向當前類,當一個類對象的第一次被觀察,那麼系統會偷偷將isa指針指向動態生成的派生類,從而在給被監控屬性賦值時執行的是派生類的setter方法
5.鍵值觀察通知依賴於NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey:;在一個被觀察屬性發生改變之前, willChangeValueForKey:一定會被調用,這就 會記錄舊的值。而當改變發生後,didChangeValueForKey:會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用。
6.補充:KVO的這套實現機制中蘋果還偷偷重寫了class方法,讓我們誤認爲還是使用的當前類,從而達到隱藏生成的派生類

二十六.你是否接觸過OC中的反射機制?簡單聊一下概念和使用

1). class反射
	通過類名的字符串形式實例化對象。
		Class class = NSClassFromString(@"student"); 
		Student *stu = [[class alloc] init];
	將類名變爲字符串。
		Class class =[Student class];
		NSString *className = NSStringFromClass(class);
2). SEL的反射
	通過方法的字符串形式實例化方法。
		SEL selector = NSSelectorFromString(@"setName");  
		[stu performSelector:selector withObject:@"Mike"];
	將方法變成字符串。
		NSStringFromSelector(@selector*(setName:));

二十七.const、static、extern  inline簡介

const作用:限制類型.   
* 1.const僅僅用來修飾右邊的變量(基本數據變量p,指針變量*p)
* 2.被const修飾的變量是隻讀的。

static和extern簡單使用
* "static作用":* 修飾局部變量:
1.延長局部變量的生命週期,程序結束纔會銷燬。
2.局部變量只會生成一份內存,只會初始化一次。
3.改變局部變量的作用域。
* 修飾全局變量
1.只能在本文件中訪問,修改全局變量的作用域,生命週期不會改
2.避免重複定義全局變量

static與const聯合使用
* static與const作用:聲明一個只讀的靜態變量
* 開發使用場景:在"一個文件中"經常使用的字符串常量,可以使用static與const組合


* "extern作用":
* 只是用來獲取全局變量(包括全局靜態變量)的值,不能用於定義變量
* "extern工作原理":
* 先在當前文件查找有沒有全局變量,沒有找到,纔會去其他文件查找。

extern與const聯合使用
* 開發中使用場景:在"多個文件中"經常使用的同一個字符串常量,可以使用extern與const組合。
* 原因:
* static與const組合:在每個文件都需要定義一份靜態全局變量。
* extern與const組合:只需要定義一份全局變量,多個文件共享。
*  全局常量正規寫法:開發中便於管理所有的全局變量,通常搞一個GlobeConst文件,裏面專門定義全局變量,統一管理,要不然項目文件多不好找。

static自不用多說,表示在當前文件中應用,如 static A, 在其它文件中也可以出現static A.不會導致重名的錯誤.
inline.內聯函數.
作用:替代宏.

雖然static inline修飾的是函數(或者方法,swift出來後,我覺着方法==函數,朋友們不用咬文嚼字,鄙視我輩了).但它在這裏就是宏的作用,即你可以將CGFloatFromPixel當作一個宏.
當然inline函數與宏有區別,inline可以:

解決函數調用效率的問題:
函數之間調用,是內存地址之間的調用,當函數調用完畢之後還會返回原來函數執行的地址。函數調用有時間開銷,內聯函數就是爲了解決這一問題。
不用inline修飾的函數, 彙編時會出現 call 指令.調用call指令就是就需要:
(1)將下一條指令的所在地址入棧
(2)並將子程序的起始地址送入PC(於是CPU的下一條指令就會轉去執行子程序).

爲什麼inline能取代宏?

優點相比於函數:

inline函數避免了普通函數的,在彙編時必須調用call的缺點:取消了函數的參數壓棧,減少了調用的開銷,提高效率.所以執行速度確比一般函數的執行速度要快.
2)集成了宏的優點,使用時直接用代碼替換(像宏一樣);

優點相比於宏:

1)避免了宏的缺點:需要預編譯.因爲inline內聯函數也是函數,不需要預編譯.

2)編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。然後進行一系列的相關檢查,就像對待任何一個真正的函數一樣。這樣就消除了它的隱患和侷限性。

3)可以使用所在類的保護成員及私有成員。

inline內聯函數的說明

1.內聯函數只是我們向編譯器提供的申請,編譯器不一定採取inline形式調用函數.
2.內聯函數不能承載大量的代碼.如果內聯函數的函數體過大,編譯器會自動放棄內聯.
3.內聯函數內不允許使用循環語句或開關語句.
4.內聯函數的定義須在調用之前.

二十八. iOS開發中nil、Nil、NULL和[NSNull null]的區別

1、nil--- 當一個對象置爲nil時,這個對象的內存地址就會被系統收回。置空之後是不能進行retain,copy等跟引用計數有關的任何操作的。

2、Nil--- nil完全等同於Nil,只不過由於編程習慣,人們一般把對象置空用nil,把類置空用Nil。

3、NULL--- 這個是從C語言繼承來的,就是一個簡單的空指針

4、[NSNull null]

這個纔是重點:[NSNull null]和nil的區別在於,nil是一個空對象,已經完全從內存中消失了,而如果我們想表達“我們需要有這樣一個容器,但這個容器裏什麼也沒有”的觀念時,我們就用到[NSNull null],它就是爲“值爲空的對象”。如果你查閱開發文檔你會發現NSNull這個類是繼承NSObject,並且只有一個“+ (NSNull *) null;”類方法。這就說明NSNull對象擁有一個有效的內存地址,所以在程序中對它的任何引用都是不會導致程序崩潰的。
 

二十九.UIView 和 CALayer 的關係

UIView 有一個名叫 layer ,類型爲 CALayer 的對象屬性,它們的行爲很相似,主要區別在於:CALayer 繼承自 NSObject ,不能夠響應事件。

這是因爲 UIView 除了負責響應事件 ( 繼承自 UIReponder ) 外,它還是一個對 CALayer 的底層封裝。可以說,它們的相似行爲都依賴於 CALayer 的實現,UIView 只不過是封裝了它的高級接口而已。

1.職責不同
UIVIew 的主要職責是負責接收並響應事件;而 CALayer 的主要職責是負責顯示 UI。

2.需要複用
在 macOS 和 App 系統上,NSView 和 UIView 雖然行爲相似,在實現上卻有着顯著的區別,卻又都依賴於 CALayer 。在這種情況下,只能封裝一個 CALayer 出來。

CALayerDelegate

你可以使用 delegate (CALayerDelegate) 對象來提供圖層的內容,處理任何子圖層的佈局,並提供自定義操作以響應與圖層相關的更改。如果圖層是由 UIView 創建的,則該 UIView 對象通常會自動指定爲圖層的委託。

注意:

在 iOS 中,如果圖層與 UIView 對象關聯,則必須將此屬性設置爲擁有該圖層的 UIView 對象。
delegate 只是另一種爲圖層提供處理內容的方式,並不是唯一的。UIView 的顯示跟它圖層委託沒有太大關係。

三十.iOS開發中id,NSObject *,id,instancetype四者有什麼區別

instancetype 和 id 都是萬能指針,指向對象。
不同點:
1.id在編譯的時候不能判斷對象的真實類型,instancetype在編譯的時候可以判斷對象的真實類型

2.id可以用來定義變量,可以作爲返回值類型,可以作爲形參類型;instancetype只能作爲返回值類型

3、id和instancetype都能省去具體類型,提高代碼的通用性。而NSObject *則沒有這種功能。

4、instancetype只能用於方法的返回類型,而id用處和NSObject *類似。

5、instancetype會告訴編譯器當前的類型,這點和NSObject *類似,但id對於編譯器卻是無類型的,調用任何方法不會給出錯誤提示。

6、對於init方法,id和instancetype是沒有區別的。因爲編譯器會把id優化成instancetype。當明確返回的類型就是當前Class時,使用instancetype能避免id帶來的編譯不出的錯誤情況。

7、NSObject *和id都是僅包含了一個Class isa。但NSObject 包含了更多方法的定義。

8、instancetype應是對id和NSObject *兩者不足的一個補充。

三十一.什麼是謂詞?

謂詞就是通過NSPredicate給定的邏輯條件作爲約束條件,完成對數據的篩選。
//定義謂詞對象,謂詞對象中包含了過濾條件(過濾條件比較多)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];
//使用謂詞條件過濾數組中的元素,過濾之後返回查詢的結果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];

三十二.isa指針問題

isa:是一個Class 類型的指針. 每個實例對象有個isa的指針,他指向對象的類,而Class裏也有個isa的指針, 指向meteClass(元類)。
元類保存了類方法的列表。當類方法被調 用時,先會從本身查找類方法的實現,如果沒有,元類會向他父類查找該方法。
同時注意的是:元類(meteClass)也是類,它也是對象。
元類也有isa指針,它的isa指針最終指向的是一個根元類(root meteClass)。
根元類的isa指針指向本身,這樣形成了一個封閉的內循環。

三十三.如何訪問並修改一個類的私有屬性?

1.KVC

我們可以用setValue:的方法設置私有屬性,並利用valueForKey:的方法訪問私有屬性。假設我們有一個類Person,並且這個類有一個私有屬性name。看代碼:

 Person * ls = [[Person alloc] init];

 [ls setValue:@"wo" forKey:@"name"];

2.runtime

我們可以利用runtime獲取某個類的所有屬性(私有屬性、非私有屬性),在獲取到某個類的屬性後就可以對該屬性進行訪問以及修改了。

 

三十四.一個objc對象的isa的指針指向什麼?有什麼作用? 

isa 指的就是 是個什麼,對象的isa指向類,類的isa指向元類(meta class),元類isa指向元類的根類。isa幫助一個對象找到它的方法。isa:是一個Class 類型的指針. 每個實例對象有個isa的指針,他指向對象的類,而Class裏也有個isa的指針, 指向meteClass(元類)。元類保存了類方法的列表。當類方法被調用時,先會從本身查找類方法的實現,如果沒有,元類會向他父類查找該方法。同時注意的是:元類(meteClass)也是類,它也是對象。元類也有isa指針,它的isa指針最終指向的是一個根元類(root meteClass).根元類的isa指針指向本身,這樣形成了一個封閉的內循環。

三十五.isKindOfClass、isMemberOfClass、selector作用分別是什麼

isKindOfClass:作用是某個對象屬於某個類型或者繼承自某類型。
isMemberOfClass:某個對象確切屬於某個類型。
selector:通過方法名,獲取在內存中的函數的入口地址。

三十六.delegate 和 notification 的區別

1). 二者都用於傳遞消息,不同之處主要在於一個是一對一的,另一個是一對多的。
2). notification通過維護一個array,實現一對多消息的轉發。
3). delegate需要兩者之間必須建立聯繫,不然沒法調用代理的方法;notification不需要兩者之間有聯繫。

三十七.iOS開發之layoutSubviews的作用和調用機制

1、layoutSubviews作用

layoutSubviews是對subviews重新佈局。比如,我們想更新子視圖的位置的時候,可以通過調用layoutSubviews方法,即可以實現對子視圖重新佈局。 
layoutSubviews默認是不做任何事情的,用到的時候,需要在子類進行重寫。

2、layoutSubviews調用機制

①、直接調用setLayoutSubviews。
②、addSubview的時候觸發layoutSubviews。
③、當view的frame發生改變的時候觸發layoutSubviews。
④、第一次滑動UIScrollView的時候觸發layoutSubviews。
⑤、旋轉Screen會觸發父UIView上的layoutSubviews事件。
⑥、改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件。
注意: 
init初始化不會觸發layoutSubviews,但是使用initWithFrame進行初始化時,當rect的值不爲CGRectZero時,也會觸發。

3、其他

①、- (void)layoutSubviews; 
這個方法,默認沒有做任何事情,需要子類進行重寫;
②、- (void)setNeedsLayout; 
標記爲需要重新佈局,異步調用layoutIfNeeded刷新佈局,不立即刷新,但layoutSubviews一定會被調用;
③、- (void)layoutIfNeeded; 
如果,有需要刷新的標記,立即調用layoutSubviews進行佈局(如果沒有標記,不會調用layoutSubviews)。

三十八.block的注意點

1). 在block內部使用外部指針且會造成循環引用情況下,需要用__week修飾外部指針:
	__weak typeof(self) weakSelf = self; 
2). 在block內部如果調用了延時函數還使用弱指針會取不到該指針,因爲已經被銷燬了,需要在block內部再將弱指針重新強引用一下。
	__strong typeof(self) strongSelf = weakSelf;
3). 如果需要在block內部改變外部棧區變量的話,需要在用__block修飾外部變量。

三十九.BAD_ACCESS在什麼情況下出現?

答:這種問題在開發時經常遇到。原因是訪問了野指針,比如訪問已經釋放對象的成員變量或者發消息、死循環等。

四十.lldb(gdb)常用的控制檯調試命令?

1). p 輸出基本類型。是打印命令,需要指定類型。是print的簡寫
	p (int)[[[self view] subviews] count]
2). po 打印對象,會調用對象description方法。是print-object的簡寫
	po [self view]
3). expr 可以在調試時動態執行指定表達式,並將結果打印出來。常用於在調試過程中修改變量的值。
4). bt:打印調用堆棧,是thread backtrace的簡寫,加all可打印所有thread的堆棧
5). br l:是breakpoint list的簡寫

四十一.iOS中常用的數據存儲方式有哪些?

數據存儲有四種方案:NSUserDefault、KeyChain、file、DB。
	其中File有三種方式:plist、Archive(歸檔)
	DB包括:SQLite、FMDB、CoreData

四十二.iOS的沙盒目錄結構是怎樣的?

沙盒結構:
1). Application:存放程序源文件,上架前經過數字簽名,上架後不可修改。
2). Documents:常用目錄,iCloud備份目錄,存放數據。(這裏不能存緩存文件,否則上架不被通過)
3). Library:
		Caches:存放體積大又不需要備份的數據。(常用的緩存路徑)
		Preference:設置目錄,iCloud會備份設置信息。
4). tmp:存放臨時文件,不會被備份,而且這個文件下的數據有可能隨時被清除的可能。

四十三.iOS多線程技術有哪幾種方式?

答:pthread、NSThread、GCD、NSOperation

四十四.用dispatch_source實現可取消的定時器

1 在開發的過程中,定時器是一個必不可少的功能,我們可以用NStimer,CADisplayLink,GCD Timer
CADisplayLink 是一個保持屏幕同頻率的計時器類,一般用在動畫或者視頻的渲染,不是作爲定時器事件來用的。
NSTimer不多講,這個剛入門的iOS開發者用的大多都是這個,而且大部分情況下能夠實現我們的功能。比如取消已經在隊列的任務。這個就需要用到GCD Timer了
而且GCD相對於NStimer有很多優勢
1 GCD的定時器和NSTimer是不一樣的,NSTimer受RunLoop影響,但是GCD的定時器不受影響,因爲RunLoop也是基於GCD的
2 dispatch_source_t支持的類型比較多,不僅僅是timer,還有以下類型
1 Timer dispatch source:定期產生通知
2 Signal dispatch source:UNIX信號到達時產生通知
3 Descriptor dispatch source:各種文件和socket操作的通知 數據可讀  數據可寫 文件在文件系統中被刪除、移動、重命名 文件元數據信息改變
4 Process dispatch source:進程相關的事件通知  當進程退出時 當進程發起fork或exec等調用 信號被遞送到進程
5  Mach port dispatch source:Mach相關事件的通知
6 Custom dispatch source:
學會GCD Timer你不會喫虧
下面只介紹dispatch_source定時器的實現 先上代碼

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW,HXMessageShowTime * NSEC_PER_SEC),10 * NSEC_PER_SEC  , 0);
        dispatch_source_set_event_handler(timer, ^{
       
                        [self doSomething];
                        dispatch_cancel(timer);
                        
             });
                    dispatch_resume(timer);
         }];

1 首先創建一個dispatch_source_create timer類型的信號量。
2 調用dispatch_source_set_timer設置信號的頻率
3 用dispatch_source_set_event_handler設置定時器出發的事件
4 啓用定時器。
5 在handler裏面完成我們要做的工作,取消定時器。

我們也可以在程序的任何地方隨時dispatch_cancel 這個定時器,這樣就能取消定時器事件了

四十五.寫出使用GCD方式從子線程回到主線程的方法代碼

答:dispatch_sync(dispatch_get_main_queue(), ^{ });

四十六.如何用GCD同步若干個異步調用?(如根據若干個url異步加載多張圖片,然後在都下載完成後合成一張整圖)

// 使用Dispatch Group追加block到Global Group Queue,這些block如果全部執行完畢,就會執行Main Dispatch Queue中的結束處理的block。
// 創建隊列組
dispatch_group_t group = dispatch_group_create();
// 獲取全局併發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{ /*加載圖片1 */ });
dispatch_group_async(group, queue, ^{ /*加載圖片2 */ });
dispatch_group_async(group, queue, ^{ /*加載圖片3 */ }); 
// 當併發隊列組中的任務執行完畢後纔會執行這裏的代碼
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合併圖片
});

四十七.dispatch_barrier_async(柵欄函數)的作用是什麼?

函數定義:dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
作用:
	1.在它前面的任務執行結束後它才執行,它後面的任務要等它執行完成後纔會開始執行。
	2.避免數據競爭

// 1.創建併發隊列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
// 2.向隊列中添加任務
dispatch_async(queue, ^{  // 1.2是並行的
    NSLog(@"任務1, %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"任務2, %@",[NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"任務 barrier, %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{   // 這兩個是同時執行的
    NSLog(@"任務3, %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"任務4, %@",[NSThread currentThread]);
});

// 輸出結果: 任務1 任務2 ——》 任務 barrier ——》任務3 任務4 
// 其中的任務1與任務2,任務3與任務4 由於是並行處理先後順序不定。

四十八. RunLoop總結深度理解

  1.  RunLoop的結構組成

  • 主要有以下六種狀態:

  • kCFRunLoopEntry -- 進入runloop循環
  • kCFRunLoopBeforeTimers -- 處理定時調用前回調
  • kCFRunLoopBeforeSources -- 處理input sources的事件
  • kCFRunLoopBeforeWaiting -- runloop睡眠前調用
  • kCFRunLoopAfterWaiting -- runloop喚醒後調用
  • kCFRunLoopExit -- 退出runloop

RunLoop位於蘋果的Core Foundation庫中,而Core Foundation庫則位於iOS架構分層的Core Service層中(值得注意的是,Core Foundation是一個跨平臺的通用庫,不僅支持Mac,iOS,同時也支持Windows):

 

在CF中,和RunLoop相關的結構有下面幾個類:(RunLoop應用場景)

1.CFRunLoopRef
2.CFRunLoopModeRef
3.CFRunLoopSourceRef
4.CFRunLoopTimerRef
5.CFRunLoopObserverRef

RunLoop的組成結構如下圖: 

CFRunLoopRef 與 NSRunLoop之間的轉換時toll-free的。關於RunLoop的具體實現代碼,我們會在下面提到。

RunLoop提供瞭如下功能(括號中CF**表明了在CF庫中對應的數據結構名稱):

1.RunLoop(CFRunLoop)使你的線程保持忙碌(有事幹時)或休眠狀態(沒事幹時)間切換(由於休眠狀態的存在,使你的線程不至於意外退出)。
2.RunLoop提供了處理事件源(source0,source1)機制(CFRunLoopSource)。
3.RunLoop提供了對Timer的支持(CFRunLoopTimer)。
4.RunLoop自身會在多種狀態間切換(run,sleep,exit等),在狀態切換時,RunLoop會通知所註冊的5.Observer(CFRunLoopObserver),使得系統可以在特定的時機執行對應的操作。相關的如AutoreleasePool 的Pop/Push,手勢識別等。
RunLoop在run時,會進入如下圖所示的do while循環: 

  1.  

(1)Thread & RunLoop(RunLoop和線程之間有什麼關係?)

 

1.RunLoop和Thread是一一對應的(key: pthread value:runLoop)
2.Thread默認是沒有對應的RunLoop的,僅當主動調用Get方法時,纔會創建
3.所有Thread線程對應的RunLoop被存儲在全局的__CFRunLoops字典中。同時,主線程在static CFRunLoopRef __main,子線程在TSD中,也存儲了線程對應的RunLoop,用於快速查找。
這裏有一點要弄清,Thread和RunLoop不是包含關係,而是平等的對應關係。Thread的若干功能,是通過RunLoop實現的。另一點是,RunLoop自己是不會Run的,需要我們手動調用Run方法(Main RunLoop會由系統啓動),我們的RunLoop纔會跑圈。靜止(注意,這裏的靜止不是休眠的意思)的RunLoop是不會做任何事情的
 

(2)RunLoopMode(NSDefaultRunLoopMode | NSRunLoopCommonModes)

每次RunLoop開始Run的時候,都必須指定一個Mode,稱爲RunLoopMode。

如,timer是基於RunLoop實現的,我們在創建timer時,可以指定timer的mode:

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
       NSLog(@"do timer");
    }];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // 指定timer在common modes(default mode + event tracking mode) 下運行

這也就解釋了,爲什麼當我們在滑動scrollview的時候,timer事件不會被回調。因爲如果我們將timer添加到默認的主線程 的defaultmode時,當用戶滑動scrollview的時候,main RunLoop 會切換到event tracking mode下來接收處理密集的滑動事件,這時候,添加在default mode下的timer是不會被觸發的。解決方法就是,我們將timer添加到common modes下,讓其在default mode和Event tracking mode下面都可以被調用。

(3)RunLoop Source

蘋果文檔將RunLoop能夠處理的事件分爲Input sources和timer事件。下面這張圖取自蘋果官網,不要注意那些容易讓人混淆的細節,只看Thread , Input sources 和 Timer sources三個大方塊的關係即可,不要關注裏面的內容。

source0 VS source1

相同 
1. 均是__CFRunLoopSource類型,這就像一個協議,我們甚至可以自己拓展__CFRunLoopSource,定義自己的source。 
2. 均是需要被Signaled後,才能夠被處理。 
3. 處理時,均是調用__CFRunLoopSource._context.version(0?1).perform,其實這就是調用一個函數指針。

不同

source0需要手動signaled,source1系統會自動signaled
source0需要手動喚醒RunLoop,才能夠被處理: CFRunLoopWakeUp(CFRunLoopRef rl)。而source1 會自動喚醒(通過mach port)RunLoop來處理。
Source1 由RunLoop和內核管理,Mach Port驅動。 
Source0 則偏向應用層一些,如Cocoa裏面的UIEvent處理,會以source0的形式發送給main RunLoop。

(4)Timer

我們經常使用的timer有幾種?

NSTimer & PerformSelector:afterDelay:(由RunLoop處理,內部結構爲CFRunLoopTimerRef)
GCD Timer(由GCD自己實現,不通過RunLoop)
CADisplayLink(通過向RunLoop投遞source1 實現回調)

關於Timer的計時,是通過內核的mach time或GCD time來實現的。在RunLoop中,NSTimer在激活時,會將休眠中的RunLoop通過_timerPort喚醒,(如果是通過GCD實現的NSTimer,則會通過另一個CGD queue專用mach port),之後,RunLoop會調用來回調到timer的fire函數。

(5)Observer

Observer的作用是可以讓外部監聽RunLoop的運行狀態,從而根據不同的時機,做一些操作。 
系統會在APP啓動時,向main RunLoop裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之後。

 

四十九. Runtime常用的幾種方法

1.相關函數

// 遍歷某個類所有的成員變量      // 遍歷某個類所有的方法        // 獲取指定名稱的成員變量
(1)class_copyMethodList      (2)class_copyIvarList     (3)class_getInstanceVariable


// 獲取成員變量名              // 獲取成員變量類型編碼         // 獲取某個對象成員變量的值
(4)ivar_getName              (5)ivar_getTypeEncoding      (6)object_getIvar


// 設置某個對象成員變量的值      // 給對象發送消息
(7)object_setIvar            (8) objc_msgSend


2.相關應用

(1)更改屬性值  (2)動態添加屬性  (3)動態添加方法  (4)交換方法的實現

(5)攔截並替換方法  (6)在方法上增加額外功能  (7)歸檔解檔  (8)字典轉模型

3.代碼實現

3.1 更改屬性值用

runtime 修改一個對象的屬性值

unsigned int count = 0;
    // 動態獲取類中的所有屬性(包括私有)
    Ivar *ivar = class_copyIvarList(_person.class, &count);
    // 遍歷屬性找到對應字段
    for (int i = 0; i < count; i ++) {
        Ivar tempIvar = ivar[i];
        const char *varChar = ivar_getName(tempIvar);
        NSString *varString = [NSString stringWithUTF8String:varChar];
        if ([varString isEqualToString:@"_name"]) {
            // 修改對應的字段值
            object_setIvar(_person, tempIvar, @"更改屬性值成功");
            break;
        }
    }

3.2 動態添加屬性用

 runtime 爲一個類添加屬性, iOS 分類裏一般會這樣用, 我們建立一個分類, NSObject+NNAddAttribute.h, 並添加以下代碼:

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

這樣只要引用 NSObject+NNAddAttribute.h, 用 NSObject 創建的對象就會有一個 name 屬性, 我們可以直接這樣寫:

    NSObject *person = [NSObject new];
    person.name = @"以夢爲馬";

3.3 動態添加方法

person 類中沒有 coding 方法,我們用 runtime 給 person 類添加了一個名字叫 coding 的方法,最終再調用coding方法做出相應. 下面代碼的幾個參數需要注意一下:

- (void)buttonClick:(UIButton *)sender {
    /*
     動態添加 coding 方法
     (IMP)codingOC 意思是 codingOC 的地址指針;
     "v@:" 意思是,v 代表無返回值 void,如果是 i 則代表 int;@代表 id sel; : 代表 SEL _cmd;
     “v@:@@” 意思是,兩個參數的沒有返回值。
     */
    class_addMethod([_person class], @selector(coding), (IMP)codingOC, "v@:");
    // 調用 coding 方法響應事件
    if ([_person respondsToSelector:@selector(coding)]) {
        [_person performSelector:@selector(coding)];
        self.testLabelText = @"添加方法成功";
    } else {
        self.testLabelText = @"添加方法失敗";
    }
}

// 編寫 codingOC 的實現
void codingOC(id self,SEL _cmd) {
    NSLog(@"添加方法成功");
}

3.4 交換方法的實現

某個類有兩個方法, 比如 person 類有兩個方法, coding 方法與 eating 方法, 我們用 runtime 交換一下這兩個方法, 就會出現這樣的情況, 當我們調用 coding 的時候, 執行的是 eating, 當我們調用 eating 的時候, 執行的是 coding, 如下面的動態效果圖.

Method oriMethod = class_getInstanceMethod(_person.class, @selector(coding));
    Method curMethod = class_getInstanceMethod(_person.class, @selector(eating));
    method_exchangeImplementations(oriMethod, curMethod);

3.5 攔截並替換方法

這個功能和上面的其實有些類似, 攔截並替換方法可以攔截並替換同一個類的, 也可以在兩個類之間進行, 我這裏用了兩個不同的類, 下面是簡單的代碼實現.

    _person = [NNPerson new];
    _library = [NNLibrary new];
    self.testLabelText = [_library libraryMethod];
    Method oriMethod = class_getInstanceMethod(_person.class, @selector(changeMethod));
    Method curMethod = class_getInstanceMethod(_library.class, @selector(libraryMethod));
    method_exchangeImplementations(oriMethod, curMethod);

3.6 在方法上增加額外功能

這個使用場景還是挺多的, 比如我們需要記錄 APP 中某一個按鈕的點擊次數, 這個時候我們便可以利用 runtime 來實現這個功能. 我這裏寫了個 UIButton 的子類, 然後在 + (void)load 中用 runtime 給它增加了一個功能, 核心代碼及實現效果圖如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self.class, @selector(sendAction:to:forEvent:));
        Method cusMethod = class_getInstanceMethod(self.class, @selector(customSendAction:to:forEvent:));
        // 判斷自定義的方法是否實現, 避免崩潰
        BOOL addSuccess = class_addMethod(self.class, @selector(sendAction:to:forEvent:), method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSuccess) {
            // 沒有實現, 將源方法的實現替換到交換方法的實現
            class_replaceMethod(self.class, @selector(customSendAction:to:forEvent:), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            // 已經實現, 直接交換方法
            method_exchangeImplementations(oriMethod, cusMethod);
        }
    });
}

3.7 歸檔解檔

當我們使用 NSCoding 進行歸檔及解檔時, 如果不用 runtime, 那麼不管模型裏面有多少屬性, 我們都需要對其實現一遍 encodeObject 和 decodeObjectForKey 方法, 如果模型裏面有 10000 個屬性, 那麼我們就需要寫 10000 句encodeObject 和 decodeObjectForKey 方法, 這個時候用 runtime, 便可以充分體驗其好處(以下只是核心代碼, 具體代碼請見 demo).

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    // 獲取類中所有屬性
    Ivar *ivars = class_copyIvarList(self.class, &count);
    // 遍歷屬性
    for (int i = 0; i < count; i ++) {
        // 取出 i 位置對應的屬性
        Ivar ivar = ivars[i];
        // 查看屬性
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        // 利用 KVC 進行取值,根據屬性名稱獲取對應的值
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int count = 0;
        // 獲取類中所有屬性
        Ivar *ivars = class_copyIvarList(self.class, &count);
        // 遍歷屬性
        for (int i = 0; i < count; i ++) {
            // 取出 i 位置對應的屬性
            Ivar ivar = ivars[i];
            // 查看屬性
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            // 進行解檔取值
            id value = [aDecoder decodeObjectForKey:key];
            // 利用 KVC 對屬性賦值
            [self setValue:value forKey:key];
        }
    }
    return self;
}

3.8 字典轉模型

字典轉模型我們通常用的都是第三方, MJExtension, YYModel 等, 但也有必要了解一下其實現方式: 遍歷模型中的所有屬性,根據模型的屬性名,去字典中查找對應的 key,取出對應的值,給模型的屬性賦值。

/** 字典轉模型 **/
+ (instancetype)modelWithDict:(NSDictionary *)dict {
    id objc = [[self alloc] init];
    unsigned int count = 0;
    // 獲取成員屬性數組
    Ivar *ivarList = class_copyIvarList(self, &count);
    // 遍歷所有的成員屬性名
    for (int i = 0; i < count; i ++) {
        // 獲取成員屬性
        Ivar ivar = ivarList[i];
        // 獲取成員屬性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSString *key = [ivarName substringFromIndex:1];
        // 從字典中取出對應 value 給模型屬性賦值
        id value = dict[key];
        // 獲取成員屬性類型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 判斷 value 是不是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            Class modalClass = NSClassFromString(ivarType);
            // 字典轉模型
            if (modalClass) {
                // 字典轉模型
                value = [modalClass modelWithDict:value];
            }
        }
        if ([value isKindOfClass:[NSArray class]]) {
            // 判斷對應類有沒有實現字典數組轉模型數組的協議
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                // 轉換成id類型,就能調用任何對象的方法
                id idSelf = self;
                // 獲取數組中字典對應的模型
                NSString *type = [idSelf arrayContainModelClass][key];
                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數組,生成模型數組
                for (NSDictionary *dict in value) {
                    // 字典轉模型
                    id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }
                // 把模型數組賦值給value
                value = arrM;
            }
        }
        // KVC 字典轉模型
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

五十._objc_msgForward 函數是做什麼的,直接調用它將會發生什麼?

答:_objc_msgForward是 IMP 類型,用於消息轉發的:當向一個對象發送一條消息,但它並沒有實現的時候,_objc_msgForward會嘗試做消息轉發。

五十一.什麼是 TCP / UDP ?

TCP:傳輸控制協議。
UDP:用戶數據協議。

TCP 是面向連接的,建立連接需要經歷三次握手,是可靠的傳輸層協議。
UDP 是面向無連接的,數據傳輸是不可靠的,它只管發,不管收不收得到。
簡單的說,TCP注重數據安全,而UDP數據傳輸快點,但安全性一般。

五十二.靜態庫和動態庫的區別

一.使用靜態庫的好處(.a)

1.模塊化,分工合作
2.避免少量改動經常導致大量的重複編譯連接
3.也可以重用,注意不是共享使用
4.靜態庫在程序編譯時會被鏈接到目標代碼中,程序運行時將不再需要改靜態庫;
5.鏈接時會被完整的複製到可執行文件中,被多次使用就有多份拷貝。
(靜態庫問題)
   靜態框架採用靜態鏈接,linker會剔除所有它認爲無用的代碼。不幸的是,linker不會檢查xib文件,因此如果類是在xib中引用,而沒有在O-C代碼中引用,linker將從最終的可執行文件中刪除類。這是linker的問題,不是框架的問題(當你編譯一個靜態庫時也會發生這個問題)。蘋果內置框架不會發生這個問題,因爲他們是運行時動態加載的,存在於iOS設備固件中的動態庫是不可能被刪除的。

(靜態庫問題解決)
  框架的最終用戶關閉linker的優化選項,通過在他們的項目的Other Linker Flags中添加-ObjC和-all_load。在框架的另一個類中加一個該類的代碼引用。例如,假設你有個MyTextField類,被linker剔除了。

二.動態庫使用有如下好處(.framework)

1.使用動態庫,可以將最終可執行文件體積縮小
2.使用動態庫,多個應用程序共享內存中得同一份庫文件,節省資源
3.使用動態庫,可以不重新編譯連接可執行程序的前提下,更新動態庫文件達到更新應用程序的目的。
4.動態庫在程序編譯時並不會被鏈接到目標代碼中,只是在程序運行時才被載入,因爲在程序運行期間還需要動態庫的存在。
5.鏈接時不復制,程序運行時由系統動態加載到內存,系統只加載一次,多個程序共用(如系統的UIKit.framework等),節省內存。

三.共同特點

1 注意理解:無論是.a靜態庫還.framework靜態庫,我們需要的都是二進制文件+.h+其它資源文件的形式,不同的是,.a本身就是二進制文件,需要我們自己配上.h和其它文件才能使用,而.framework本身已經包含了.h和其它文件,可以直接使用。

2 圖片資源的處理:兩種靜態庫,一般都是把圖片文件單獨的放在一個.bundle文件中,一般.bundle的名字和.a或.framework的名字相同。.bundle文件很好弄,新建一個文件夾,把它改名爲.bundle就可以了,右鍵,顯示包內容可以向其中添加圖片資源。

3 category是我們實際開發項目中經常用到的,把category打成靜態庫是沒有問題的,但是在用這個靜態庫的工程中,調用category中的方法時會有找不到該方法的運行時錯誤(selector not recognized),解決辦法是:在使用靜態庫的工程中配置other linker flags的值爲-ObjC。

4 如果一個靜態庫很複雜,需要暴露的.h比較多的話,就可以在靜態庫的內部創建一個.h文件(一般這個.h文件的名字和靜態庫的名字相同),然後把所有需要暴露出來的.h文件都集中放在這個.h文件中,而那些原本需要暴露的.h都不需要再暴露了,只需要把.h暴露出來就可以了。

五十三.OC中創建線程的方法是什麼?如果在主線程中執行代碼,方法是什麼?

// 創建線程的方法
- [NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]
- [self performSelectorInBackground:nil withObject:nil];
- [[NSThread alloc] initWithTarget:nil selector:nil object:nil];
- dispatch_async(dispatch_get_global_queue(0, 0), ^{});
- [[NSOperationQueue new] addOperation:nil];

// 主線程中執行代碼的方法
- [self performSelectorOnMainThread:nil withObject:nil waitUntilDone:YES];
- dispatch_async(dispatch_get_main_queue(), ^{});
- [[NSOperationQueue mainQueue] addOperation:nil];

五十四.用僞代碼寫一個線程安全的單例模式

static id _instance;
+ (id)allocWithZone:(struct _NSZone *)zone {
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       _instance = [super allocWithZone:zone];
   });
   return _instance;
}

+ (instancetype)sharedData {
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       _instance = [[self alloc] init];
   });
   return _instance;
}

- (id)copyWithZone:(NSZone *)zone {
   return _instance;
}

五十五.在手勢對象基礎類UIGestureRecognizer的常用子類手勢類型中哪兩個手勢發生後,響應只會執行一次?

答:UITapGestureRecognizer,UISwipeGestureRecognizer是一次性手勢,手勢發生後,響應只會執行一次。

五十六.請簡單的介紹下APNS發送系統消息的機制

APNS優勢:杜絕了類似安卓那種爲了接受通知不停在後臺喚醒程序保持長連接的行爲,由iOS系統和APNS進行長連接替代。
APNS的原理:
	1). 應用在通知中心註冊,由iOS系統向APNS請求返回設備令牌(device Token)
	2). 應用程序接收到設備令牌併發送給自己的後臺服務器
	3). 服務器把要推送的內容和設備發送給APNS
	4). APNS根據設備令牌找到設備,再由iOS根據APPID把推送內容展示

五十七.oc和swift的區別

1.Swift和Objective-C的聯繫

Swift和Objective-C共用一套運行時環境,Swift的類型可以橋接到Objective-C(下面我簡稱OC),反之亦然。
其次就是,OC之前積累的很多類庫,在Swift中大部分依然可以直接使用,當然,Swift3之後,一些語法改變了很多,不過還是有跡可循的。OC出現過的絕大多數概念,比如引用計數、ARC、屬性、協議、接口、初始化、擴展類、命名參數、匿名函數等,在Swift中繼續有效(可能最多換個術語)。Swift大多數概念與OC一樣。當然Swift也多出了一些新興概念,這些在OC中是沒有的,比如範型、元組等。

2. Swift比Objective-C有什麼優勢

1、Swift容易閱讀,語法和文件結構簡易化。
2、Swift更易於維護,文件分離後結構更清晰。
3、Swift更加安全,它是類型安全的語言。
4、Swift代碼更少,簡潔的語法,可以省去大量冗餘代碼
5、Swift速度更快,運算性能更高。

3. Swift目前存在的缺點

1、版本不穩定,之前升級Swift3大動刀,苦了好多人
2、使用人數比例偏低,目前還是OC爲主
3、社區的開源項目偏少,畢竟OC獨大好多年,很多優秀的類庫都不支持Swift,不過這種狀況正在改變,現在有好多優秀的Swift的開源類庫了
4、公司使用的比例不高,很多公司以穩爲主,還是在使用OC開發,很少一些在進行混合開發,更少一些是純Swift開發。
5、偶爾開發中遇到的一些問題,很難查找到相關資料,這是一個弊端。
6、純Swift的運行時和OC有本質區別,一些OC中運行時的強大功能,在純Swift中變無效了。
7、對於不支持Swift的一些第三方類庫,如果非得使用,只能混合編程,利用橋接文件實現。

4. Swift其他功能說明

1 Swift的內存管理

Swift使用自動引用計數(ARC)來簡化內存管理,與OC一致。

2 Swift的可選項類型(Optionals)介紹

Swift引入了可選項類型,用於處理變量值不存在的情況。Optionals類似於OC中指向nil的指針,但是適用於所有數據類型,而非僅僅侷限於類,Optionals相比於OC中的nil指針,更加安全和簡明,並且也是Swift諸多最強大功能的核心。

3 Swift中的 !和 ?

這兩個符號是用來標記這個變量的值是否可選,!表示可選變量必須保證轉換能夠成功,否則報錯,但定義的變量可以直接使用;?表示可選變量即使轉換不成功也不會報錯,變量值爲nil,如果轉換成功,要使用該變量時,後面需要加!進行修飾。

4 Swift中範型的簡單說明

範型是用來使代碼能安全工作,swift中的範型可以在函數數據和普通數據類型中使用,例如類、結構體或枚舉。範型可以解決代碼複用的問題,

5 Swift的訪問權限變更

swift新增了兩種訪問權限,權限更細化。具體查看這裏:
訪問權限由大到小依次爲:open,public,internal(默認),fileprivate,private

fileprivate

在原有的swift中的 private其實並不是真正的私有,如果一個變量定義爲private,在同一個文件中的其他類依然是可以訪問到的。這個場景在使用extension的時候很明顯。

這樣帶來了兩個問題:
(1)當我們標記爲private時,意爲真的私有還是文件內可共享呢?
(2)當我們如果意圖爲真正的私有時,必須保證這個類或者結構體在一個單獨的文件裏。否則可能同文件裏其他的代碼訪問到。
由此,在swift 3中,新增加了一個 fileprivate來顯式的表明,這個元素的訪問權限爲文件內私有。

過去的private對應現在的fileprivate。現在的private則是真正的私有,離開了這個類或者結構體的作用域外面就無法訪問。
所以fileprivate > private .
open

open則是彌補public語義上的不足。

現在的pubic有兩層含義:
(1)這個元素可以在其他作用域被訪問
(2)這個元素可以在其他作用域被繼承或者override
繼承是一件危險的事情。尤其對於一個framework或者module的設計者而言。在自身的module內,類或者屬性對於作者而言是清晰的,能否被繼承或者override都是可控的。但是對於使用它的人,作者有時會希望傳達出這個類或者屬性不應該被繼承或者修改。這個對應的就是 final。

final的問題在於在標記之後,在任何地方都不能override。而對於lib的設計者而言,希望得到的是在module內可以被override,在被import到其他地方後其他用戶使用的時候不能被override。
通俗的理解public和open就是:

public:可以被任何人訪問,但其他module中不可以被override和繼承,而在本module內可以被override和繼承。
open:可以被任何人使用,包括override和繼承。
internal

internal是系統默認訪問級別,internal修飾符可寫可不寫。

(1)internal訪問級別所修飾的屬性或方法在源代碼所在的整個模塊都可以訪問。
(2)如果是框架或者庫代碼,則在整個框架內部都可以訪問,框架由外部代碼所引用時,則不可以訪問。
(3)如果是App代碼,也是在整個App代碼,也是在整個App內部可以訪問。
5. Swift Foundation框架

爲了方便使用,Swift的基本類型都可以無縫轉換到 Foundation 框架中的對應類型。

因爲 Cocoa 框架所接受和返回的基本數據類型都是自身框架內的類型,也就是 Foundation 中所定義的像 NSString,NSNumber,NSArray 等這些東西。而脫離 Cocoa 框架進行 app 開發是不可能的事情。因此我們在使用 Swift 開發 app 時無法避免地需要在 Swift 類型和 Foundation 類型間進行轉換。如果需要每次顯式地書寫轉換的話,大概就沒人會喜歡用 Swift 了。還好 Swift 與 Foundation 之間的類型轉換是可以自動完成的,這使得通過 Swift 使用 Cocoa 時順暢了很多。

而且這個轉換不僅是自動的,而且是雙向的,而且無論何時只要有可能,轉換的結果會更傾向於使用 Swift 類型。也就是說,只要你不寫明類型是需要 NS 開頭的類型的時候,你都會得到一個 Swift 類型。

Swift中的類型和OC的類型對應關係

String - NSString
Int, Float, Double, Bool 以及其他與數字有關的類型 - NSNumber
Array - NSArray
Dictionary - NSDictionary
6. Swift便捷的函數式編程

Swift提供了Map、FlatMap、Filter、Reduce等函數方法,能夠大大方便我們對對象處理。

Map

var results = [1,3,5,7]
let results = values.map ({ (element) -> Int in
    return element * 2
})
//"[2, 6, 10, 14]"
Filter:

var values = [1,3,5,7,9]
let flattenResults = values.filter{ $0 % 3 == 0}
//[3, 9]
Reduce

var values = [1,3,5]
let initialResult = 0
var reduceResult = values.reduce(initialResult, combine: { (tempResult, element) -> Int in
    return tempResult + element
})
print(reduceResult)
//9
7. 其他補充 - swift獨有

範圍運算符

a...b 表示 [a,b] 包括a和b 。 (如3...5  就是範圍取3,4,5)
a..<b 表示 [a,b) 包括a,不包括b 。 (如3...5  就是範圍取3,4)
常見的如for循環:for i in 0...9{}
獨有的元組類型

元組(tuples)把多個值組合成一個複合值。元組內的值可以使任意類型,並不要求是相同類型

var value = (Int,String) = (x:15,y:"abc")
swift中使用let定義常量,var定義變量

使用常量,更加安全,不能夠被修改,在需要對對象進行修改的時候 只能用var修飾.

if let 、 guard let 的用法

縮減代碼量,安全處理數據邏輯。

8. 細節使用區別

1、swift不分.h和.m文件 ,一個類只有.swift一個文件,所以整體的文件數量比起OC有一定減少。
2、swift句尾不需要分號 ,除非你想在一行中寫三行代碼就加分號隔開。
3、swift數據類型都會自動判斷 , 只區分變量var 和常量let
4、強制類型轉換格式不同 OC強轉:(int)a Swift強轉:Int(a)
5、關於BOOL類型更加嚴格 ,Swift不再是OC的非0就是真,而是true纔是真false纔是假
6、swift的 循環語句中必須加{} 就算只有一行代碼也必須要加
7、swift的switch語句後面可以跟各種數據類型了 ,如Int、字符串都行,並且裏面不用寫break(OC好像不能字符串)
8、swift if後的括號可以省略: if a>b {},而OC裏 if後面必須寫括號。
9、swift打印 用print("") 打印變量時可以 print("(value)"),不用像OC那樣記很多%@,d%等。
10、Swift3的【Any】可以代表任何類型的值,無論是類、枚舉、結構體還是任何其他Swift類型,這個對應OC中的【id】類型。

五十八.GCD 與NSOperation  區別

1.GCD的核心是C語言寫的系統服務,執行和操作簡單高效,因此NSOperation底層也通過GCD實現,換個說法就是NSOperation是對GCD更高層次的抽象,這是他們之間最本質的區別.因此如果希望自定義任務,建議使用NSOperation;

2.依賴關係,NSOperation可以設置兩個NSOperation之間的依賴,第二個任務依賴於第一個任務完成執行,GCD無法設置依賴關係,不過可以通過dispatch_barrier_async來實現這種效果;

3.KVO(鍵值對觀察),NSOperation和容易判斷Operation當前的狀態(是否執行,是否取消),對此GCD無法通過KVO進行判斷;

4.優先級,NSOperation可以設置自身的優先級,但是優先級高的不一定先執行,GCD只能設置隊列的優先級,無法在執行的block設置優先級;

5.繼承,NSOperation是一個抽象類實際開發中常用的兩個類是NSInvocationOperation和NSBlockOperation,同樣我們可以自定義NSOperation,GCD執行任務可以自由組裝,沒有繼承那麼高的代碼複用度;

6.效率,直接使用GCD效率確實會更高效,NSOperation會多一點開銷,但是通過NSOperation可以獲得依賴,優先級,繼承,鍵值對觀察這些優勢,相對於多的那麼一點開銷確實很划算,魚和熊掌不可得兼,取捨在於開發者自己;

關於主要的區別都已經總結,根據實際開發中來說,GCD使用情況較多,簡單高效,從變成原則上來看,應該是使用高層次的抽象,避免使用低層次的抽象,那麼無疑我們應該選擇NSOperation,因爲複雜的任務可以自己通過NSOperation實現,日常還是GCD的天下,畢竟GCD有更高的併發和執行能力。

NSOperation是對GCD面向對象的ObjC封裝,但是相比GCD基於C語言開發,效率卻更高,建議如果任務之間有依賴關係或者想要監聽任務完成狀態的情況下優先選擇NSOperation否則使用GCD

五十九.檢查內存泄漏

項目的代碼很多,前兩天老大突然跟我說項目中某一個ViewController的dealloc()方法沒有被調用,存在內存泄漏問題,需要排查原因,解決內存泄漏問題。由於剛加入項目組不久,對出問題的模塊的代碼還不太熟悉,所以剛拿到問題時覺得很棘手,再加上作爲一個iOS菜鳥,對內存泄漏的排查方法和原因確實基本上不了解。所以,也藉着這樣的機會,我研究了一下關於iOS開發中內存泄漏的排查方法和原因分析。

  首先,補充兩個基本概念的解釋:

  • 內存溢出 (out of memory):是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory。通俗理解就是內存不夠,通常在運行大型軟件或遊戲時,軟件或遊戲所需要的內存遠遠超出了你主機內安裝的內存所承受大小,就叫內存溢出。
  • 內存泄露( memory leak):是指程序在申請內存後,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積後果很嚴重,無論多少內存,遲早會被佔光。

一、排查方法

靜態分析方法(Analyze)和動態分析方法(Instrument的leak)。

Analyze 優點:

1、使用操作容易。

2、能夠在編碼階段,開發自行進行代碼檢查。早期發現代碼隱患。

3、直接分析源代碼來發現程序中的錯誤,而不需要實際運行。

4、自動檢測Objective-C程序中的BUG,發現內存泄露和其它問題。

5、內存問題發現越早,解決的代價就越小。

主要分析以下四種問題:

1、邏輯錯誤:訪問空指針或未初始化的變量等;

2、內存管理錯誤:如內存泄漏等;

3、聲明錯誤:從未使用過的變量;

4、Api調用錯誤:未包含使用的庫和框架。

Instruments裏面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:檢查是否訪問了殭屍對象,但是這個工具只能從上往下檢查,不智能。
3). Allocations:用來檢查內存,寫算法的那批人也用這個來檢查。
4). Leaks:檢查內存,看是否有內存泄露。

1.1 靜態內存泄漏分析方法

  通過xcode打開項目,然後點擊product-->Analyze,如下圖左側的圖所示,這樣就開始對項目進行靜態內存泄漏分析,分析結果如下圖右側的圖所示。根據分析結果進行休整之後在進行分析就好了。

       

  靜態分析方法能發現大部分的問題,但是隻能是靜態分析結果,有一些並不準確,還有一些動態分配內存的情形並沒有進行分析。所以僅僅使用靜態內存泄漏分析得到的結果並不是非常可靠,如果需要,我們需要將對項目進行更爲完善的內存泄漏分析和排查。那就需要用到我們下面要介紹的動態內存泄漏分析方法Instruments中的Leaks方法進行排查。

1.2 動態內存泄漏分析方法

  分析內存泄露不能把所有的內存泄露查出來,有的內存泄露是在運行時,用戶操作時才產生的。那就需要用到Instruments了。具體操作是通過xcode打開項目,然後點擊product-->profile,如下圖左側圖所示。

       

  按上面操作,build成功後跳出Instruments工具,如上圖右側圖所示。選擇Leaks選項,點擊右下角的【choose】按鈕,這時候項目程序也在模擬器或手機上運行起來了,在手機或模擬器上對程序進行操作,工具顯示效果如下:

  點擊左上角的紅色圓點,這時項目開始啓動了,由於leaks是動態監測,所以手動進行一系列操作,可檢查項目中是否存在內存泄漏問題。如圖所示,橙色矩形框中所示綠色爲正常,如果出現如右側紅色矩形框中顯示紅色,則表示出現內存泄漏。

  選中Leaks Checks,在Details所在欄中選擇CallTree,並且在右下角勾選Invert Call Tree 和Hide System Libraries,會發現顯示若干行代碼,雙擊即可跳轉到出現內存泄漏的地方,修改即可。

二、內存泄漏的原因分析

  在目前主要以ARC進行內存管理的開發模式,導致內存泄漏的根本原因是代碼中存在循環引用,從而導致一些內存無法釋放,這就會導致dealloc()方法無法被調用。主要原因大概有一下幾種類型。

2.1 ViewController中存在NSTimer

如果你的ViewController中有NSTimer,那麼你就要注意了,因爲當你調用

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self  selector:@selector(updateTime:) userInfo:nil  repeats:YES];

時的 target:self 就增加了ViewController的return count,如果你不將這個timer invalidate,將別想調用dealloc。

  • 理由:這時  target: self,增加了ViewController的retain count
    self強引用timertimer強引用self。造成循環引用。
  • 解決方案:在恰當時機調用[timer invalidate]即可。

2.2 ViewController中的代理delegate

  一個比較隱祕的因素,你去找找與這個類有關的代理,有沒有強引用屬性?如果你這個VC需要外部傳某個Delegate進來,來通過Delegate+protocol的方式傳參數給其他對象,那麼這個delegate一定不要強引用,儘量assign或者weak,否則你的VC會持續持有這個delegate,直到它自身被釋放。

  • 理由:如果代理用strong修飾,ViewController(self)會強引用ViewView強引用delegatedelegate內部強引用ViewController(self)。造成內存泄漏。
  • 解決方案:代理儘量使用weak修飾。

@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end


@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;

- (void)startAnimation; //!< 開始動畫
- (void)stopAnimation; //!< 結束動畫
- (void)reverseAnimation; //!< 最後的修改動畫
 

2.3 ViewController中Block

  這個可能就是經常容易犯的一個問題了,Block體內使用實例變量也會造成循環引用,使得擁有這個實例的對象不能釋放。因爲該block本來就是當前viewcontroller的一部分,現在蓋子部門又強引用self,導致循環引用無法釋放。 例如你這個類叫OneViewController,有個屬性是NSString *name; 如果你在block體中使用了self.name,或者_name,那樣子的話這個類就沒法釋放。 要解決這個問題其實很簡單,就是在block之前申明當前的self引用爲弱引用即可。

  • 理由:如果block被當前ViewController(self)持有,這時,如果block內部再持有ViewController(self),就會造成循環引用。
  • 解決方案:在block外部對弱化self,再在block內部強化已經弱化的weakSelf
//MRC下代碼如下
__block Viewcontroller *weakSelf = self;
//ARC下代碼如下
__weak Viewcontroller *weakSelf = self;

__weak typeof(self) weakSelf = self;

    [self.operationQueue addOperationWithBlock:^{

        __strong typeof(weakSelf) strongSelf = weakSelf;

        if (completionHandler) {
            
            KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);
            
            completionHandler([strongSelf serialReaderWithRequest:request]);
        }
    }];

2.4 ViewController的子視圖對self的持有

這個問題也是我的項目中內存泄漏的問題所在。我們有時候需要在子視圖或者某個cell中點擊跳轉等操作,需要在子視圖或cell中持有當前的ViewController對象,這樣跳轉之後的back鍵才能直接返回該頁面,同時也不銷燬當前ViewController。此時,你就要注意在子視圖或者cell中對當前頁面的持有對象不能是強引用,儘量assign或者weak,否則會造成循環引用,內存無法釋放。

六十.__block和__weak的區別

__block

1.__block對象在block中是可以被修改、重新賦值的。
2.__block對象在block中不會被block強引用一次,從而不會出現循環引用問題。

__weak

使用了__weak修飾符的對象,作用等同於定義爲weak的property。自然不會導致循環引用問題,因爲蘋果文檔已經說的很清楚,當原對象沒有任何強引用的時候,弱引用指針也會被設置爲nil。

兩者區別

1.__block不管是ARC還是MRC模式下都可以使用,可以修飾對象,還可以修飾基本數據類型。 
2.__weak只能在ARC模式下使用,也只能修飾對象(NSString),不能修飾基本數據類型(int)。 
3.__block對象可以在block中被重新賦值,__weak不可以。 

__weak 本身是可以避免循環引用的問題的,但是其會導致外部對象釋放了之後,block 內部也訪問不到這個對象的問題,我們可以通過在 block 內部聲明一個 __strong 的變量來指向 weakObj,使外部對象既能在 block 內部保持住,又能避免循環引用的問題

__block 本身無法避免循環引用的問題,但是我們可以通過在 block 內部手動把 blockObj 賦值爲 nil 的方式來避免循環引用的問題。另外一點就是 __block 修飾的變量在 block 內外都是唯一的,要注意這個特性可能帶來的隱患。


六十一.iOS中的事件的產生和傳遞

1.事件的產生

發生觸摸事件後,系統會將該事件加入到一個由UIApplication管理的事件隊列中,爲什麼是隊列而不是棧?因爲隊列的特點是FIFO,即先進先出,先產生的事件先處理才符合常理,所以把事件添加到隊列。
UIApplication會從事件隊列中取出最前面的事件,並將事件分發下去以便處理,通常,先發送事件給應用程序的主窗口(keyWindow)。
主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這也是整個事件處理過程的第一步。
找到合適的視圖控件後,就會調用視圖控件的touches方法來作具體的事件處理。
2.事件的傳遞

觸摸事件的傳遞是從父控件傳遞到子控件
也就是UIApplication->window->尋找處理事件最合適的view
注 意: 如果父控件不能接受觸摸事件,那麼子控件就不可能接收到觸摸事件

應用如何找到最合適的控件來處理事件?

1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件
2.判斷觸摸點是否在自己身上
3.子控件數組中從後往前遍歷子控件,重複前面的兩個步驟(所謂從後往前遍歷子控件,就是首先查找子控件數組中最後一個元素,然後執行1、2步驟)
4.view,比如叫做fitView,那麼會把這個事件交給這個fitView,再遍歷這個fitView的子控件,直至沒有更合適的view爲止。
5.如果沒有符合條件的子控件,那麼就認爲自己最合適處理這個事件,也就是自己是最合適的view。
UIView不能接收觸摸事件的三種情況:

不允許交互:userInteractionEnabled = NO
隱藏:如果把父控件隱藏,那麼子控件也會隱藏,隱藏的控件不能接受事件
透明度:如果設置一個控件的透明度<0.01,會直接影響子控件的透明度。alpha:0.0~0.01爲透明。
注 意:默認UIImageView不能接受觸摸事件,因爲不允許交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要設置UIImageView的userInteractionEnabled = YES。

響應者鏈條

響應者鏈條其實就是很多響應者對象(繼承自UIResponder的對象)一起組合起來的鏈條。
一般默認做法是控件將順着響應者鏈條向上傳遞,將事件交給上一個響應者處理。
那麼如何判斷當前響應者的上一個響應者是誰呢?
判斷當前是否爲控制器的view,如果是,上一個響應者就是控制器,如果不是,上一個響應者就是父控件。


傳遞的過程爲:
1.判斷當前是否爲控制器的view,是,事件就傳遞給控制器,不是,事件就傳遞給父控件。
2.在視圖層次結構的最頂層,如果也不能處理收到的事件,則將事件傳遞給window對象處理。
3.如果window對象也不處理,則將事件傳遞給UIApplication對象。
4.如果UIApplication對象也不處理,則將事件丟棄。

hitTest:withEvent:

這是iOS事件的傳遞和響應中最重要的方法之一,在前邊也有提到,現在來具體的介紹一下這個方法。
只要事件一傳遞給一個控件,這個控件就會調用自己的hitTest:withEvent:方法。
他的作用就是尋找並返回最適合的view,無論這個控件能不能處理事件,也不管觸摸點在不在這個控件上,事件都會先傳遞給這個控件,隨後就調用該方法。
事件傳遞給窗口或控件的後,就調用hitTest:withEvent:方法尋找更合適的view。所以是,先傳遞事件,再根據事件在自己身上找更合適的view。
不管子控件是不是最合適的view,系統默認都要先把事件傳遞給子控件,經過子控件調用子控件自己的hitTest:withEvent:方法驗證後才知道有沒有更合適的view。即便父控件是最合適的view了,子控件的hitTest:withEvent:方法還是會調用,不然怎麼知道有沒有更合適的!即,如果確定最終父控件是最合適的view,那麼該父控件的子控件的hitTest:withEvent:方法也是會被調用的。
如果hitTest:withEvent:方法中返回nil,那麼調用該方法的控件本身和其子控件都不是最合適的view,也就是在自己身上沒有找到更合適的view。那麼最合適的view就是該控件的父控件。


六十二.說說你理解weak屬性?

weak實現原理:

Runtime維護了一個weak表,用於存儲指向某個對象的所有weak指針。weak表其實是一個hash(哈希)表,Key是所指對象的地址,Value是weak指針的地址(這個地址的值是所指對象的地址)數組。

1、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。

2、添加引用時:objc_initWeak函數會調用 objc_storeWeak() 函數, objc_storeWeak() 的作用是更新指針指向,創建對應的弱引用表。

3、釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取所有weak指針地址的數組,然後遍歷這個數組把其中的數據設爲nil,最後把這個entry從weak表中刪除,最後清理對象的記錄。

2.當weak引用指向的對象被釋放時,又是如何去處理weak指針的呢?

1、調用objc_release

2、因爲對象的引用計數爲0,所以執行dealloc

3、在dealloc中,調用了_objc_rootDealloc函數

4、在_objc_rootDealloc中,調用了object_dispose函數

5、調用objc_destructInstance

6、最後調用objc_clear_deallocating,詳細過程如下:

a. 從weak表中獲取廢棄對象的地址爲鍵值的記錄

b. 將包含在記錄中的所有附有 weak修飾符變量的地址,賦值爲 nil

c. 將weak表中該記錄刪除

d. 從引用計數表中刪除廢棄對象的地址爲鍵值的記錄

第三方框架

AFNetworking 底層原理分析


1). AFHTTPRequestOperationManager:內部封裝的是 NSURLConnection, 負責發送網絡請求, 使用最多的一個類。(3.0廢棄)
2). AFHTTPSessionManager:內部封裝是 NSURLSession, 負責發送網絡請求,使用最多的一個類。
3). AFNetworkReachabilityManager:實時監測網絡狀態的工具類。當前的網絡環境發生改變之後,這個工具類就可以檢測到。
4). AFSecurityPolicy:網絡安全的工具類, 主要是針對 HTTPS 服務。
5). AFURLRequestSerialization:序列化工具類,基類。上傳的數據轉換成JSON格式
(AFJSONRequestSerializer).使用不多。
6). AFURLResponseSerialization:反序列化工具類;基類.使用比較多:
7). AFJSONResponseSerializer; JSON解析器,默認的解析器.
8). AFHTTPResponseSerializer; 萬能解析器; JSON和XML之外的數據類型,直接返回二進制數據.對服務器返回的數據不做任何處理.
9). AFXMLParserResponseSerializer; XML解析器;

AFNetworking是封裝的NSURLSession的網絡請求,由五個模塊組成:分別由NSURLSession,Security,Reachability,Serialization,UIKit五部分組成

NSURLSession:網絡通信模塊(核心模塊) 對應 AFNetworking中的 AFURLSessionManager和對HTTP協議進行特化處理的AFHTTPSessionManager,AFHTTPSessionManager是繼承於AFURLSessionmanager的

Security:網絡通訊安全策略模塊 對應 AFSecurityPolicy

Reachability:網絡狀態監聽模塊 對應AFNetworkReachabilityManager

Seriaalization:網絡通信信息序列化、反序列化模塊 對應 AFURLResponseSerialization

UIKit:對於iOS UIKit的擴展庫

SDWebImage內部實現原理

詳細解說:

圖片解釋:內存層面的相當是個緩存器,以Key-Value的形式存儲圖片。當內存不夠的時候會清除所有緩存圖片。用搜索文件系統的方式做管理,文件替換方式是以時間爲單位,剔除時間大於一週的圖片文件。當SDWebImageManager向SDImageCache要資源時,先搜索內存層面的數據,如果有直接返回,沒有的話去訪問磁盤,將圖片從磁盤讀取出來,然後做Decoder,將圖片對象放到內存層面做備份,再返回調用層。

1、入口 setImageWithURL:placeholderImage:options: 會先把 placeholderImage 顯示,然後 SDWebImageManager 根據 URL 開始處理圖片。

2、進入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交給 SDImageCache 從緩存查找圖片是否已經下載 queryDiskCacheForKey:delegate:userInfo:.

3、如果內存中已經有圖片緩存,SDImageCacheDelegate 回調 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

4、SDWebImageManagerDelegate 回調 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示圖片。

5、如果內存緩存中沒有,生成 NSInvocationOperation 添加到隊列開始從硬盤查找圖片是否已經緩存。

6、根據 URLKey 在硬盤緩存目錄下嘗試讀取圖片文件。這一步是在 NSOperation 進行的操作,所以回主線程進行結果回調 notifyDelegate:。

7、如果硬盤中有該圖片,將圖片添加到內存緩存中(如果空閒內存過小,會先清空內存緩存)。SDImageCacheDelegate 回調 imageCache:didFindImage:forKey:userInfo:。進而回調展示圖片。

8、如果從硬盤緩存目錄讀取不到圖片,說明所有緩存都不存在該圖片,需要下載圖片,回調 imageCache:didNotFindImageForKey:userInfo:。

9、共享或重新生成一個下載器 SDWebImageDownloader 開始下載圖片。

10、圖片下載由 NSURLConnection 來做,實現相關 delegate 來判斷圖片下載中、下載完成和下載失敗。

11、imageDownloader:didFinishWithImage: 回調給 SDWebImageManager 告知圖片下載完成。

12、通知所有的 downloadDelegates 下載完成,回調給需要的地方展示圖片。

13、將圖片保存到 SDImageCache 中,內存緩存和硬盤緩存同時保存。寫文件到硬盤也在以單獨 NSInvocationOperation 完成,避免拖慢主線程。SDImageCache 在初始化的時候會註冊一些消息通知,在內存警告或退到後臺的時候清理內存圖片緩存,應用結束的時候清理過期圖片。

總結解說:
1.首先會在 SDWebImageCache 中尋找圖片是否有對應的緩存, 它會以url 作爲數據的索引先在內存中尋找是否有對應的緩存
2.如果緩存未找到就會利用通過MD5處理過的key來繼續在磁盤中查詢對應的數據, 如果找到了, 就會把磁盤中的數據加載到內存中,並將圖片顯示出來
3.如果在內存和磁盤緩存中都沒有找到,就會向遠程服務器發送請求,開始下載圖片
4.下載後的圖片會加入緩存中,並寫入磁盤中
5.整個獲取圖片的過程都是在子線程中執行,獲取到圖片後回到主線程將圖片顯示出來

SDWebImage原理:
調用類別的方法:
1. 從內存(字典)中找圖片(當這個圖片在本次使用程序的過程中已經被加載過),找到直接使用。
2. 從沙盒中找(當這個圖片在之前使用程序的過程中被加載過),找到使用,緩存到內存中。
3. 從網絡上獲取,使用,緩存到內存,緩存到沙盒。

 六十三.iOS開發幾大算法資料整理

CSDN八大內部排序算法介紹

github上搜集的幾大算法原理和實現代碼,只有JavaScript、Python、Go、Java的實現代碼

github上搜集的幾大算法時間複雜度和空間複雜度比較

iOS 開發中常用的排序(冒泡、選擇、快速、插入、希爾、歸併、基數)算法 幾種常用算法OC實現(他的歸併排序好像寫的有點問題)

幾大算法文字理解和OC代碼實現

1. 冒泡排序算法(Bubble Sort)

相鄰元素進行比較,按照升序或者降序,交換兩個相鄰元素的位置 是一種“穩定排序算法”

1.1 網上文字理論

是一種簡單直觀的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
作爲最簡單的排序算法之一,冒泡排序給我的感覺就像 Abandon 在單詞書裏出現的感覺一樣,每次都在第一頁第一位,所以最熟悉。冒泡排序還有一種優化算法,就是立一個 flag,當在一趟序列遍歷中元素沒有發生交換,則證明該序列已經有序。但這種改進對於提升性能來說並沒有什麼太大作用。

1.2 算法步驟

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

1.3 動圖演示

bubbleSort.gif

1.4 什麼時候最快

當輸入的數據已經是正序時。

1.5 什麼時候最慢

當輸入的數據是反序時。

1.6 冒泡排序代碼示例

- (void)bubbleSortWithArray:(NSMutableArray *)array {
    for (int i = 0; i < array.count - 1; i++) {
         //外層for循環控制循環次數
        for (int j = 0; j < array.count - 1 - i; j++) {
            //內層for循環控制交換次數
            if ([array[j] integerValue] > [array[j + 1] integerValue]) {
                [array exchangeObjectAtIndex:j withObjectAtIndex:j + 1];
            }
        }
    }
}

2. 快速排序算法(quick sort)

快速排序圖文理解,通過哨兵站崗理解快速排序

2.1 網上文字理解

快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序 n 個項目要 Ο(nlogn) 次比較。在最壞狀況下則需要 Ο(n2) 次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他 Ο(nlogn) 算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。

快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。

快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。

快速排序的名字起的是簡單粗暴,因爲一聽到這個名字你就知道它存在的意義,就是快,而且效率高!它是處理大數據最快的排序算法之一了。雖然 Worst Case 的時間複雜度達到了 O(n²),但是人家就是優秀,在大多數情況下都比平均時間複雜度爲 O(n logn) 的排序算法表現要更好,可是這是爲什麼呢,我也不知道。好在我的強迫症又犯了,查了 N 多資料終於在《算法藝術與信息學競賽》上找到了滿意的答案: 快速排序的最壞運行情況是 O(n²),比如說順序數列的快排。但它的平攤期望時間是 O(nlogn),且 O(nlogn) 記號中隱含的常數因子很小,比複雜度穩定等於 O(nlogn) 的歸併排序要小很多。所以,對絕大多數順序性較弱的隨機數列而言,快速排序總是優於歸併排序。

2.2 算法步驟

  1. 從數列中挑出一個元素,稱爲 “基準”(pivot);
  2. 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作;
  3. 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序;

遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會退出,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

2.3 動圖演示

quickSort.gif

2.4 快速排序代碼示例

- (void)quickSortArray:(NSMutableArray *)array
            leftIndex:(NSInteger)left
           rightIndex:(NSInteger)right {
   if (left > right) {
       return;
   }
   NSInteger i = left;
   NSInteger j = right;
   //記錄基準數 pivoty
   NSInteger key = [array[i] integerValue];
   while (i < j) {
       //首先從右邊j開始查找(從最右邊往左找)比基準數(key)小的值<---
       while (i < j && key <= [array[j] integerValue]) {
           j--;
       }
       //如果從右邊j開始查找的值[array[j] integerValue]比基準數小,則將查找的小值調換到i的位置
       if (i < j) {
           array[i] = array[j];
       }
       
       //從i的右邊往右查找到一個比基準數小的值時,就從i開始往後找比基準數大的值 --->
       while (i < j && [array[i] integerValue] <= key) {
           i++;
       }
       //如果從i的右邊往右查找的值[array[i] integerValue]比基準數大,則將查找的大值調換到j的位置
       if (i < j) {
           array[j] = array[i];
       }
   }
   //將基準數放到正確的位置,----改變的是基準值的位置(數組下標)---
   array[i] = @(key);
   //遞歸排序
   //將i左邊的數重新排序
   [self quickSortArray:array leftIndex:left rightIndex:i - 1];
   //將i右邊的數重新排序
   [self quickSortArray:array leftIndex:i + 1 rightIndex:right];
}
 

3. 選擇排序算法(select sort)

它的改進(相比較冒泡算法)在於:先並不急於調換位置,先從A[0]開始逐個檢查,看哪個數最小就記下該數所在的位置P,等一躺掃描完畢,再把A[P]和A[0]對調,這時A[0]到A[n]中最小的數據就換到了最前面的位置。是一個“不穩定排序算法”

它是一種簡單直觀的排序算法,無論什麼數據進去都是 O(n²) 的時間複雜度。所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間。

選擇排序算法一: 直接選擇排序(straight select sort)

3.1 算法步驟

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
  3. 重複第二步,直到所有元素均排序完畢。

3.2 動圖演示

selectionSort.gif

3.3 直接選擇排序示例代碼

- (void)selectSortWithArray:(NSMutableArray *)array {
    for (int i = 0; i < array.count; i++) {
        for (int j = i + 1; j < array.count; j++) {
            if (array[i] > array[j]) {
                [array exchangeObjectAtIndex:i withObjectAtIndex:j];
            }
        }
    }
}

選擇排序算法二:

堆排序(heap sort 涉及到完全二叉樹的概念)

參考了網上搜羅的java堆排序寫法和概念,計算機語言通用,OC也能實現
堆排序理解(java例子)

網上文字理解

堆排序是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以說是一種利用堆的概念來排序的選擇排序。分爲兩種方法:

  1. 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;
  2. 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;
    堆排序的平均時間複雜度爲 Ο(nlogn)。

算法步驟

  1. 創建一個堆 H[0……n-1];
  2. 把堆首(最大值)和堆尾互換;
  3. 把堆的尺寸縮小 1,並調用 shift_down(0),目的是把新的數組頂端數據調整到相應位置;
  4. 重複步驟 2,直到堆的尺寸爲 1。

動圖演示

heapSort.gif

堆排序代碼示例

- (void)heapSortWithArray:(NSMutableArray *)array {
    //循環建立初始堆
    for (NSInteger i = array.count * 0.5; i >= 0; i--) {
        [self heapAdjustWithArray:array parentIndex:i length:array.count];
    }
    //進行n-1次循環,完成排序
    for (NSInteger j = array.count - 1; j > 0; j--) {
        //最後一個元素和第一個元素進行交換
        [array exchangeObjectAtIndex:j withObjectAtIndex:0];
        //篩選R[0]結點,得到i-1個結點的堆
        [self heapAdjustWithArray:array parentIndex:0 length:j];
        NSLog(@"第%ld趟:", array.count - j);
        [self printHeapSortResult:array begin:0 end:array.count - 1];
    }
}

- (void)heapAdjustWithArray:(NSMutableArray *)array
                parentIndex:(NSInteger)parentIndex
                     length:(NSInteger)length {
    NSInteger temp = [array[parentIndex] integerValue]; //temp保存當前父結點
    NSInteger child = 2 * parentIndex + 1; //先獲得左孩子
    
    while (child < length) {
        //如果有右孩子結點,並且右孩子結點的值大於左孩子結點,則選取右孩子結點
        if (child + 1 < length && [array[child] integerValue] < [array[child + 1] integerValue]) {
            child++;
        }
        
        //如果父結點的值已經大於孩子結點的值,則直接結束
        if (temp >= [array[child] integerValue]) {
            break;
        }
        
        //把孩子結點的值賦值給父結點
        array[parentIndex] = array[child];
        
        //選取孩子結點的左孩子結點,繼續向下篩選
        parentIndex = child;
        child = 2 * child + 1;
    }
    array[parentIndex] = @(temp);
}

- (void)printHeapSortResult:(NSMutableArray *)array
                      begin:(NSInteger)begin
                        end:(NSInteger)end {
    for (NSInteger i = 0; i < begin; i++) {

    }
    for (NSInteger i = begin; i <= end; i++) {
        
    }
    //打印堆排序
    NSLog(@"堆排序升序結果是--->%@",array);
}
 

4.  插入排序(insert sort)

4.1 網上文字理解

插入排序的代碼實現雖然沒有冒泡排序和選擇排序那麼簡單粗暴,但它的原理應該是最容易理解的了,因爲只要打過撲克牌的人都應該能夠秒懂。插入排序是一種最簡單直觀的排序算法,它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。

插入排序和冒泡排序一樣,也有一種優化算法,叫做拆半插入。

4.2 算法步驟

  1. 將第一待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
  2. 從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。(如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面)

4.3 動圖演示

insertionSort.gif

4.4 插入排序代碼示例

- (void)insertSortWithArray:(NSMutableArray *)array {
    NSInteger j;
    for (NSInteger i = 1; i < array.count; i++) {
        //取出每一個待插入的數據,從array[1]開始查找
        NSInteger temp = [array[i] integerValue];
        
        for (j = i - 1; j >= 0 && temp < [array[j] integerValue]; j--) {
            //如果之前的數比temp大,就將這個數往後移動一個位置,留出空來讓temp插入,和整理撲克牌類似
            [array[j + 1]  integerValue] = [array[j] integerValue]];
            array[j] = [NSNumber numberWithInteger:temp];
        }
    }
}

5. 歸併排序(merge sort)

5.1 網上文字理解

歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。

作爲一種典型的分而治之思想的算法應用,歸併排序的實現由兩種方法:

  • 自上而下的遞歸(所有遞歸的方法都可以用迭代重寫,所以就有了第 2 種方法);
  • 自下而上的迭代;

在《數據結構與算法 JavaScript 描述》中,作者給出了自下而上的迭代方法。

和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是 O(nlogn) 的時間複雜度。代價是需要額外的內存空間。

5.2 算法步驟

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;
  4. 重複步驟 3 直到某一指針達到序列尾;
  5. 將另一序列剩下的所有元素直接複製到合併序列尾。

5.3 動圖演示

mergeSort.gif

5.4 歸併排序代碼示例 參考簡書作者OC代碼

//自頂向下的歸併排序
/**
 遞歸使用歸併排序,對array[left...right]的範圍進行排序
 @param array 數組
 @param left 左邊界
 @param right 右邊界
 */
- (void)mergeSortWithArray:(NSMutableArray *)array
                      left:(NSInteger)left
                     right:(NSInteger)right {
    //判斷遞歸到底的情況
    if (left >= right) {
        //這時候只有一個元素或者是不存在的情況
        return;
    }
    //中間索引的位置
    NSInteger middle = (right + left) / 2;
    //對 left --- middle 區間的元素進行排序操作
    [self mergeSortWithArray:array left:left right:middle];
    //對 middle + 1 ---- right 區間的元素進行排序操作
    [self mergeSortWithArray:array left:middle + 1 right:right];
    //兩邊排序完成後進行歸併操作
    [self mergeSortWithArray:array left:left middle:middle right:right];
}

/**
 對 [left middle] 和 [middle + 1 right]這兩個區間歸併操作
 @param array 傳入的數組
 @param left 左邊界
 @param middle 中間位置
 @param right 右邊界
 */
- (void)mergeSortWithArray:(NSMutableArray *)array
                      left:(NSInteger)left
                    middle:(NSInteger)middle
                     right:(NSInteger)right {
    //拷貝一個數組出來
    NSMutableArray *copyArray = [NSMutableArray arrayWithCapacity:right - left + 1];
    for (NSInteger i = left; i <= right; i++) {
        //這裏要注意有left的偏移量,所以copyArray賦值的時候要減去left
        copyArray[i - left] = array[i];
    }
    
    NSInteger i = left, j = middle + 1;
    //循環從left開始到right區間內給數組重新賦值,注意賦值的時候也是從left開始的,不要習慣寫成了從0開始,還有都是閉區間
    for (NSInteger k = left; k <= right; k++) {
        //當左邊界超過中間點時 說明左半部分數組越界了 直接取右邊部分的數組的第一個元素即可
        if (i > middle) {
            //給數組賦值 注意偏移量left 因爲這裏是從left開始的
            array[k] = copyArray[j - left];
            //索引++
            j++;
        } else if (j > right) {//當j大於右邊的邊界時證明有半部分數組越界了,直接取左半部分的第一個元素即可
            array[k] = copyArray[i - left];
            //索引++
            i++;
        } else if (copyArray[i - left] > copyArray[j - left]) {//左右兩半部分數組比較
            //當右半部分數組的第一個元素要小時 給數組賦值爲右半部分的第一個元素
            array[k] = copyArray[j - left];
            //右半部分索引加1
            j++;
        } else {//右半部分數組首元素大於左半部分數組首元素
            array[k] = copyArray[i - left];
            i++;
        }
    }
}

6. 希爾排序(shell sort)

6.1 網上文字理解

希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率;
  • 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位;

希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

6.2 算法步驟

  1. 選擇一個增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列個數 k,對序列進行 k 趟排序;
  3. 每趟排序,根據對應的增量 ti,將待排序列分割成若干長度爲 m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲 1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

6.4 希爾排序代碼示例

- (void)shellAscendingOrderSort:(NSMutableArray *)ascendingArr {
    NSMutableArray *buckt = [self createBucket];
    NSNumber *maxnumber = [self listMaxItem:ascendingArr];
    NSInteger maxLength = numberLength(maxnumber);
    for (int digit = 1; digit <= maxLength; digit++) {
        // 入桶
        for (NSNumber *item in ascendingArr) {
            NSInteger baseNumber = [self fetchBaseNumber:item digit:digit];
            NSMutableArray *mutArray = buckt[baseNumber];
            [mutArray addObject:item];
        }
        NSInteger index = 0;
        for (int i = 0; i < buckt.count; i++) {
            NSMutableArray *array = buckt[i];
            while (array.count != 0) {
                NSNumber *number = [array objectAtIndex:0];
                ascendingArr[index] = number;
                [array removeObjectAtIndex:0];
                index++;
            }
        }
    }
    NSLog(@"希爾升序排序結果:%@", ascendingArr);
}

- (NSMutableArray *)createBucket {
    NSMutableArray *bucket = [NSMutableArray array];
    for (int index = 0; index < 10; index++) {
        NSMutableArray *array = [NSMutableArray array];
        [bucket addObject:array];
    }
    return bucket;
}

- (NSNumber *)listMaxItem:(NSArray *)list {
    NSNumber *maxNumber = list[0];
    for (NSNumber *number in list) {
        if ([maxNumber integerValue] < [number integerValue]) {
            maxNumber = number;
        }
    }
    return maxNumber;
}

NSInteger numberLength(NSNumber *number) {
    NSString *string = [NSString stringWithFormat:@"%ld", (long)[number integerValue]];
    return string.length;
}

- (NSInteger)fetchBaseNumber:(NSNumber *)number digit:(NSInteger)digit {
    if (digit > 0 && digit <= numberLength(number)) {
        NSMutableArray *numbersArray = [NSMutableArray array];
        NSString *string = [NSString stringWithFormat:@"%ld", [number integerValue]];
        for (int index = 0; index < numberLength(number); index++) {
            [numbersArray addObject:[string substringWithRange:NSMakeRange(index, 1)]];
        }
        NSString *str = numbersArray[numbersArray.count - digit];
        return [str integerValue];
    }
    return 0;
}

7. 基數排序(radix sort)

7.1 文字理解

基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

7.2 基數排序 vs 計數排序 vs 桶排序

基數排序有兩種方法:
這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

  • 基數排序:根據鍵值的每位數字來分配桶;
  • 計數排序:每個桶只存儲單一鍵值;
  • 桶排序:每個桶存儲一定範圍的數值;

7.3 動圖演示

radixSort.gif

7.4 基數排序代碼示例

- (void)radixAscendingOrderSort:(NSMutableArray *)ascendingArr {
    NSMutableArray *buckt = [self createBucket];
    NSNumber *maxnumber = [self listMaxItem:ascendingArr];
    NSInteger maxLength = numberLength(maxnumber);
    for (int digit = 1; digit <= maxLength; digit++) {
        // 入桶
        for (NSNumber *item in ascendingArr) {
            NSInteger baseNumber = [self fetchBaseNumber:item digit:digit];
            NSMutableArray *mutArray = buckt[baseNumber];
            [mutArray addObject:item];
        }
        NSInteger index = 0;
        for (int i = 0; i < buckt.count; i++) {
            NSMutableArray *array = buckt[i];
            while (array.count != 0) {
                NSNumber *number = [array objectAtIndex:0];
                ascendingArr[index] = number;
                [array removeObjectAtIndex:0];
                index++;
            }
        }
    }
    NSLog(@"基數升序排序結果:%@", ascendingArr);
}

8. 計數排序(counting sort)

8.1 文字理解

計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

8.2 動圖演示

countingSort.gif

8.3 計數排序代碼示例(無)

9. 桶排序(bucket sort)

9.1 文字理解

桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。爲了使桶排序更加高效,我們需要做到這兩點:

  1. 在額外空間充足的情況下,儘量增大桶的數量
  2. 使用的映射函數能夠將輸入的 N 個數據均勻的分配到 K 個桶中

同時,對於桶中元素的排序,選擇何種比較排序算法對於性能的影響至關重要。

9.2 什麼時候最快

當輸入的數據可以均勻的分配到每一個桶中。

9.3 什麼時候最慢

當輸入的數據被分配到了同一個桶中。

 

六十四.​在 Objective-C 中,enum 建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型。

//定義一個枚舉(比較嚴密)
typedef NS_ENUM(NSInteger, BRUserGender) {
    BRUserGenderUnknown,	// 未知
    BRUserGenderMale,		// 男性
    BRUserGenderFemale,		// 女性
    BRUserGenderNeuter		// 無性
};

六十五.談談 UITableView 的優化

1). 正確的複用cell。
2). 設計統一規格的Cell
3). 提前計算並緩存好高度(佈局),因爲heightForRowAtIndexPath:是調用最頻繁的方法;
4). 異步繪製,遇到複雜界面,遇到性能瓶頸時,可能就是突破口;
4). 滑動時按需加載,這個在大量圖片展示,網絡加載的時候很管用!
5). 減少子視圖的層級關係
6). 儘量使所有的視圖不透明化以及做切圓操作。
7). 不要動態的add 或者 remove 子控件。最好在初始化時就添加完,然後通過hidden來控制是否顯示。
8). 使用調試工具分析問題。

如何實行cell的動態的行高

如果希望每條數據顯示自身的行高,必須設置兩個屬性,1.預估行高,2.自定義行高。
設置預估行高 tableView.estimatedRowHeight = 200。
設置定義行高 tableView.estimatedRowHeight = UITableViewAutomaticDimension。 
如果要讓自定義行高有效,必須讓容器視圖有一個自下而上的約束。

六十六.什麼是野指針、空指針?

野指針:不知道指向了哪裏的指針叫野指針。即指針指向不確定,指針存的地址是一個垃圾值,未初始化。
空指針:不指向任何位置的指針叫空指針。即指針沒有指向,指針存的地址是一個空地址,NULL。

六十七.什麼是 OOA / OOD / OOP ?

OOA(Object Oriented Analysis)   --面向對象分析
OOD(Object Oriented Design)     --面向對象設計
OOP(Object Oriented Programming)--面向對象編程

 

  •   第二大類開發之 Swift 常問面試題

(一)Swift 與 Objective-C 的聯繫與區別?

Swift和Objective-C 共用一套運行時環境,Swift 的類型可以橋接到Objective-C(下面我簡稱OC),反之亦然。兩者可以互相引用混合編程。
其次就是,OC 之前積累的很多類庫,在 Swift 中大部分依然可以直接使用,當然,Swift3之後,一些語法改變了很多,不過還是有跡可循的。OC出現過的絕大多數概念,比如引用計數、ARC、屬性、協議、接口、初始化、擴展類、命名參數、匿名函數等,在Swift中繼續有效(可能最多換個術語)。Swift大多數概念與OC一樣。當然Swift也多出了一些新興概念,這些在OC中是沒有的,比如範型、元組等。

(二)Swift 比 Objective-C 有什麼優勢?

  1. Swift 容易閱讀,語法和文件結構簡易化。
  2. Swift 更易於維護,文件分離後結構更清晰。
  3. Swift 更加安全,它是類型安全的語言。
  4. Swift 代碼更少,簡潔的語法,可以省去大量冗餘代碼。
  5. Swift 速度更快,運算性能更高。

(三)Swift目前存在的缺點

  1. 版本不穩定,之前升級Swift3大動刀,苦了好多人。
  2. 使用人數比例偏低,目前還是OC的天下。
  3. 社區的開源項目偏少,畢竟OC獨大好多年,很多優秀的類庫都不支持Swift,不過這種狀況正在改變,現在有好多優秀的Swift的開源類庫了。
  4. 公司使用的比例不高,很多公司以穩爲主,還是在使用OC開發,很少一些在進行混合開發,更少一些是純Swift開發。
  5. 偶爾開發中遇到的一些問題,很難查找到相關資料,這是一個弊端。
  6. 純Swift的運行時和OC有本質區別,一些OC中運行時的強大功能,在純Swift中變無效了。
  7. 對於不支持Swift的一些第三方類庫,如果非得使用,只能混合編程,利用橋接文件實現。

(四)Swift 相比 Objective-C 獨有的語法

  1. 範圍運算符
    a...b 表示 [a,b] 包括a和b 。 (如3...5 就是範圍取3,4,5)
    a..<b 表示 [a,b) 包括a,不包括b 。 (如3...5 就是範圍取3,4)
    常見的如for循環:for i in 0...9{}

  2. 獨有的元組類型
    元組(tuples)把多個值組合成一個複合值。元組內的值可以使任意類型,並不要求是相同類型。eg:

var value = (Int,String) = (x:15,y:"abc")
  1. swift中使用let定義常量,var定義變量
    使用常量,更加安全,不能夠被修改,在需要對對象進行修改的時候 只能用var修飾.
  2. if let 、 guard let 的用法
    縮減代碼量,安全處理數據邏輯。

(五)Swift 相比 Objective-C 細節使用區別

  1. swift不分.h和.m文件 ,一個類只有.swift一個文件,所以整體的文件數量比起OC有一定減少。
  2. swift句尾不需要分號 ,除非你想在一行中寫三行代碼就加分號隔開。
  3. swift數據類型都會自動判斷 , 只區分變量var 和常量let
  4. 強制類型轉換格式不同 OC強轉:(int)a Swift強轉:Int(a)
  5. 關於BOOL類型更加嚴格 ,Swift不再是OC的非0就是真,而是true纔是真false纔是假
  6. swift的 循環語句中必須加{} 就算只有一行代碼也必須要加
  7. swift的switch語句後面可以跟各種數據類型了 ,如Int、字符串都行,並且裏面不用寫break(OC好像不能字符串)
  8. swift if後的括號可以省略: if a>b {},而OC裏 if後面必須寫括號。
  9. swift打印 用print("") 打印變量時可以 print("(value)"),不用像OC那樣記很多%@,d%等。
  10. Swift3的【Any】可以代表任何類型的值,無論是類、枚舉、結構體還是任何其他Swift類型,這個對應OC中的【id】類型。

(六)Swift 是面向對象還是函數式的編程語言?

Swift 既是面向對象的,又是函數式的編程語言。
說 Swift 是面向對象的語言,是因爲 Swift 支持類的封裝、繼承、和多態,從這點上來看與 Java 這類純面向對象的語言幾乎毫無差別。
說 Swift 是函數式編程語言,是因爲 Swift 支持 map, reduce, filter, flatmap 這類去除中間狀態、數學函數式的方法,更加強調運算結果而不是中間過程。

(七)請說明並比較以下關鍵詞:Open, Public, Internal, File-private, Private

Swift 有五個級別的訪問控制權限,從高到底依次爲比如 Open, Public, Internal, File-private, Private。
他們遵循的基本原則是:高級別的變量不允許被定義爲低級別變量的成員變量。比如一個 private 的 class 中不能含有 public 的 String。反之,低級別的變量卻可以定義在高級別的變量中。比如 public 的 class 中可以含有 private 的 Int。

  • Open 具備最高的訪問權限。其修飾的類和方法可以在任意 Module 中被訪問和重寫;它是 Swift 3 中新添加的訪問權限。
  • Public 的權限僅次於 Open。與 Open 唯一的區別在於它修飾的對象可以在任意 Module 中被訪問,但不能重寫。
  • Internal 是默認的權限。它表示只能在當前定義的 Module 中訪問和重寫,它可以被一個 Module 中的多個文件訪問,但不可以被其他的 Module 中被訪問。
  • File-private 也是 Swift 3 新添加的權限。其被修飾的對象只能在當前文件中被使用。例如它可以被一個文件中的 class,extension,struct 共同使用。
  • Private 是最低的訪問權限。它的對象只能在定義的作用域內使用。離開了這個作用域,即使是同一個文件中的其他作用域,也無法訪問。

(八)請說明並比較以下關鍵詞:strong, weak, unowned

Swift 的內存管理機制與 Objective-C一樣爲 ARC(Automatic Reference Counting)。它的基本原理是,一個對象在沒有任何強引用指向它時,其佔用的內存會被回收。反之,只要有任何一個強引用指向該對象,它就會一直存在於內存中。

  • strong 代表着強引用,是默認屬性。當一個對象被聲明爲 strong 時,就表示父層級對該對象有一個強引用的指向。此時該對象的引用計數會增加1。
  • weak 代表着弱引用。當對象被聲明爲 weak 時,父層級對此對象沒有指向,該對象的引用計數不會增加1。它在對象釋放後弱引用也隨即消失。繼續訪問該對象,程序會得到 nil,不虧崩潰
  • unowned 與弱引用本質上一樣。唯一不同的是,對象在釋放後,依然有一個無效的引用指向對象,它不是 Optional 也不指向 nil。如果繼續訪問該對象,程序就會崩潰。

加分回答:

  • weak 和 unowned 的引入是爲了解決由 strong 帶來的循環引用問題。簡單來說,就是當兩個對象互相有一個強指向去指向對方,這樣導致兩個對象在內存中無法釋放(詳情請參考第3章第3節第8題)。

weak 和 unowned 的使用場景有如下差別:

  • 當訪問對象時該對象可能已經被釋放了,則用 weak。比如 delegate 的修飾。
  • 當訪問對象確定不可能被釋放,則用 unowned。比如 self 的引用。
  • 實際上爲了安全起見,很多公司規定任何時候都使用 weak 去修飾。

(九)在Swift和Objective-C的混編項目中,如何在Swift文件中調用Objective-C文件中已經定義的方法?如何在Objective-C文件中調用Swift文件中定義的方法?

  • Swift中若要使用Objective-C代碼,可以在ProjectName-Bridging-Header.h裏添加Objective-C的頭文件名稱,Swift文件中即可調用相應的Objective-C代碼。一般情況Xcode會在Swift項目中第一次創建Objective-C文件時自動創建ProjectName-Bridging-Header.h文件。
  • Objective-C中若要調用Swift代碼,可以導入Swift生成的頭函數ProjectName-Swift.h來實現。
  • Swift文件中若要規定固定的方法或屬性暴露給Objective-C使用,可以在方法或屬性前加上@objc來聲明。如果該類是NSObject子類,那麼Swift會在非private的方法或屬性前自動加上@objc。

(十)用Swift 將協議(protocol)中的部分方法設計成可選(optional),該怎樣實現?

@optional 和 @required 是 Objective-C 中特有的關鍵字。
Swift中,默認所有方法在協議中都是必須實現的。而且,協議裏方法不可以直接定義 optional。先給出兩種解決方案:

  • 在協議和方法前都加上 @objc 關鍵字,然後再在方法前加上 optional 關鍵字。該方法實際上是把協議轉化爲Objective-C的方式然後進行可選定義。示例如下:
@objc protocol SomeProtocol {
  func requiredFunc()
  @objc optional func optionalFunc()
}
  • 用擴展(extension)來規定可選方法。Swift中,協議擴展(protocol extension)可以定義部分方法的默認實現,這樣這些方法在實際調用中就是可選實現的了。示例如下:
protocol SomeProtocol {
  func requiredFunc()
  func optionalFunc()
}
extension SomeProtocol {
  func optionalFunc() {
    print(“Dumb Implementation”)
  }
}
Class SomeClass: SomeProtocol {
  func requiredFunc() {
    print(“Only need to implement the required”)
  }
}

(十一)swift中,如何阻止一個方法屬性,屬性,下標被子類改寫?

在類的定義中使用final關鍵字聲明類、屬性、方法和下標。final聲明的類不能被繼承,final聲明的屬性、方法和下標不能被重寫。

(十二)swift中,實現一個將整形數組全部轉化成對應的字符串數組(eg: [1,2,3,4,5] -> ["1","2","3","4","5"])

var sampleArray: [Int] = [1,2,3,4,5]
sampleArray.map {
    String($0)
}
//["1", "2", "3", "4", "5"]

(十三)swift中,關鍵字 guard 和 defer 的用法

guard也是基於一個表達式的布爾值去判斷一段代碼是否該被執行。與if語句不同的是,guard只有在條件不滿足的時候纔會執行這段代碼。

guard let name = self.text else {  return }

defer的用法是,這條語句並不會馬上執行,而是被推入棧中,直到函數結束時纔再次被調用。

defer {
   //函數結束才調用
}

(十四)open與public的區別

  • public:可以別任何人訪問,但是不可以被其他module複寫和繼承。
  • open:可以被任何人訪問,可以被繼承和複寫。

(十五)struct與class 的區別

  • struct是值類型,class是引用類型

    • 值類型的變量直接包含它們的數據,對於值類型都有它們自己的數據副本,因此對一個變量操作不可能影響另一個變量。
    • 引用類型的變量存儲對他們的數據引用,因此後者稱爲對象,因此對一個變量操作可能影響另一個變量所引用的對象。
    • 二者的本質區別:struct是深拷貝,拷貝的是內容;class是淺拷貝,拷貝的是指針
  • property的初始化不同:class 在初始化時不能直接把 property 放在 默認的constructor 的參數裏,而是需要自己創建一個帶參數的constructor;而struct可以,把屬性放在默認的constructor 的參數裏。
  • 變量賦值方式不同:struct是值拷貝;class是引用拷貝。
  • immutable變量:swift的可變內容和不可變內容用var和let來甄別,如果初始爲let的變量再去修改會發生編譯錯誤。struct遵循這一特性;class不存在這樣的問題
  • mutating function: struct 和 class 的差別是 struct 的 function 要去改變 property 的值的時候要加上 mutating,而 class 不用。
  • 繼承: struct不可以繼承,class可以繼承。
  • struct比class更輕量:struct分配在棧中,class分配在堆中。

(十六)swift把struct作爲數據模型

16.1優點

  1. 安全性: 因爲 Struct 是用值類型傳遞的,它們沒有引用計數。
  2. 內存: 由於他們沒有引用數,他們不會因爲循環引用導致內存泄漏。
  3. 速度: 值類型通常來說是以棧的形式分配的,而不是用堆。因此他們比 Class 要快很多!
  4. 拷貝:Objective-C 裏拷貝一個對象,你必須選用正確的拷貝類型(深拷貝、淺拷貝),而值類型的拷貝則非常輕鬆!
  5. 線程安全: 值類型是自動線程安全的。無論你從哪個線程去訪問你的 Struct ,都非常簡單。

16.2 缺點

  1. Objective-C與swift混合開發:OC調用的swift代碼必須繼承於NSObject。
  2. 繼承:struct不能相互繼承。
  3. NSUserDefaults:Struct 不能被序列化成 NSData 對象。
  1. 如何設置實時渲染?
@IBDesignable讓Interface Bulider在特定視圖上執行實時渲染
複製代碼
  1. 異步同步任務的區別?
  2. `同步`:等待任務完成,一個接一個,順可預測(Predictable Execution Order),通常情況在Main
    `異步`:不分先後執行順序完成任務,順序不可預測(Unpredictable Order),通常在Background
    複製代碼
  3. 什麼是NSError對象? NSError有三部分組成,分別爲 Domain Code UserInfor Domain是一個字符串,標記一個錯誤域
  4. NSError(domain: <#String#>, code: <#Int#>, userInfo: <#[String : Any]?#>)
    複製代碼
  5. 什麼是Enum? enum 是一種類型,包含了相關的一組數據
  6. 爲什麼使用synchronized? 保證在一定時間內,只有一個線程訪問它
  7. strong, weak,copy 有什麼不同? strong:引用計數會增加 weak:不會增加引用計數 Copy: 意味着我們在創建對象時複製該對象的值
  8. 什麼是ABI? 應用程序二進制接口
  9. 在Cocoa中有哪些常見的設計模式 創造性:單例(Singleton) 結構性: 適配器(Adapter) 行爲:觀察者(Observer)
  10. Realm數據庫的好處 a. 開源的DB framework b. 快 c. ios 安卓都可以使用
  11. Swift 優勢是什麼? a. 類型安全 b. 閉包 c. 速度快
  12. 什麼是泛型? 泛型可以讓我們定義出靈活,且可重用的函數和類型,避免重複代碼
  13. 解釋 Swift 中的 lazy? lazy是 Swift 中的一個關鍵字,他可以延遲屬性的初始化時間,知道用到這個屬性時,纔去加載它
  14. 解釋什麼是 defer? 延遲執行,當你離開當前代碼塊,就會去執行
  15. KVC 和 KCO 的區別? KVC: 它是一種用間接方式訪問類的屬性的機制 KVO: 它是一種觀察者模式,被觀察的對象如果有改變,觀察者就會收到通知
  16. Gurad的好處? 可以使語句變得更簡潔,避免嵌套很多層,可以使用break,return提前退.

1.React Native相對於原生的ios和Android有哪些優勢。

react native一套代碼可以開發出跨平臺app, 減少了人力、節省了時間、避免了 iOS 與 Android 版本發佈的時間差,開發新功能可以更迅速。等等

2.React Native的優點和缺點在哪裏。

缺點:內存、轉化爲原生的

3.父傳子,子傳父數據傳遞方式。

props state refs 方面回答

4.如何實現底部TabBar的高度不一樣呢。(類似新浪微博底部加號)

主要考察flex佈局絕對定位問題

5.請您簡單介紹一下redux、mobx。

redux ==> action/reducer/store
mobx ==>數據雙向綁定

6.當你調用setState的時候,發生了什麼事。

當調用 setState 時,React會做的第一件事情是將傳遞給 setState 的對象合併到組件的當前狀態。 這將啓動一個稱爲和解(reconciliation)的過程。 和解(reconciliation)的最終目標是以最有效的方式,根據這個新的狀態來更新UI。 爲此,React將構建一個新的 React 元素樹(您可以將其視爲 UI 的對象表示)。 一旦有了這個樹,爲了弄清 UI 如何響應新的狀態而改變,React 會將這個新樹與上一個元素樹相比較( diff )。 通過這樣做, React 將會知道發生的確切變化,並且通過了解發生什麼變化,只需在絕對必要的情況下進行更新即可最小化 UI 的佔用空間。

7.React中Element 和 Component 有何區別。

簡單地說,一個 React element 描述了你想在屏幕上看到什麼。 換個說法就是,一個 React element 是一些 UI 的對象表示。 一個 React Component 是一個函數或一個類, 它可以接受輸入並返回一個 React element (通常是通過 JSX ,它被轉化成一個 createElement 調用)。

8.shouldComponentUpdate 應該做什麼

其實這個問題也是跟reconciliation有關係。 “和解( reconciliation )的最終目標是以最有效的方式,根據新的狀態更新用戶界面”。 如果我們知道我們的用戶界面(UI)的某一部分不會改變, 那麼沒有理由讓 React 很麻煩地試圖去弄清楚它是否應該渲染。 通過從 shouldComponentUpdate 返回 false, React 將假定當前組件及其所有子組件將保持與當前組件相同

9.描述事件在React中的處理方式

爲了解決跨瀏覽器兼容性問題, 您的 React 中的事件處理程序將傳遞 SyntheticEvent 的實例, 它是 React 的瀏覽器本機事件的跨瀏覽器包裝器。 這些 SyntheticEvent 與您習慣的原生事件具有相同的接口,除了它們在所有瀏覽器中都兼容。 有趣的是,React 實際上並沒有將事件附加到子節點本身。 React 將使用單個事件監聽器監聽頂層的所有事件。 這對於性能是有好處的,這也意味着在更新DOM時,React 不需要擔心跟蹤事件監聽器

10.reactJS的props.children.map函數來遍歷會收到異常提示,爲什麼。應該如何遍歷。

this.props.children 的值有三種可能

  1. 當前組件沒有子節點,它就是 undefined;
  2. 有一個子節點,數據類型是 object;
  3. 有多個子節點,數據類型就是 array.

系統提供React.Children.map()方法安全的遍歷子節點對象

11.XSS與CSRF介紹

XSS是一種跨站腳本攻擊,是屬於代碼注入的一種,攻擊者通過將代碼注入網頁中,其他用戶看到會受到影響(代碼內容有請求外部服務器);
CSRF是一種跨站請求僞造,冒充用戶發起請求,完成一些違背用戶請求的行爲(刪帖,改密碼,發郵件,發帖等)

12.在使用redux過程中,如何防止定義的action-type的常量重複。

ES6引入了一種新的原始數據類型Symbol,表示獨一無二的值。 Symbol函數前不能使用new命令,否則會報錯。這是因爲生成的Symbol是一個原始類型的值,不是對象 Symbol函數可以接受一個字符串作爲參數,表示對Symbol實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。

13 PureComponent 爲什麼比Component高效?

追查:這兩個組件的實現在ReactBaseClasses.js中間,除掉註釋後如下

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.isReactComponent = {};

ReactComponent.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

ReactComponent.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};


function ReactPureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;

ReactPureComponent.prototype = new ComponentDummy();
ReactPureComponent.prototype.constructor = ReactPureComponent;
_assign(ReactPureComponent.prototype, ReactComponent.prototype);
ReactPureComponent.prototype.isPureReactComponent = true;

module.exports = {
  Component: ReactComponent,
  PureComponent: ReactPureComponent
};

發現Component就只實現了構造方法,定義了setState方法就完了。而ReactPureComponent更狠,就只是用js原型模擬繼承的方法繼承了Component,然後定義屬性isPureReactComponent爲true。全局搜索isPureReactComponent屬性,發現在ReactCompositeComponent.js中有使用,這個類就是管理組件的更新、加載等的。關鍵代碼在updateComponent方法中,如下

var shouldUpdate = true;    // 這個變量決定是否需要重新渲染組件
  if (!this._pendingForceUpdate) {
    var prevState = inst.state;
    shouldUpdate = willReceive || nextState !== prevState;
    // inst代表組件實例,這個判斷條件就是組件是否自己實現了shouldComponentUpdate方法
    if (inst.shouldComponentUpdate) {
      if (__DEV__) {
        shouldUpdate = measureLifeCyclePerf(
          () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
          this._debugID,
          'shouldComponentUpdate',
        );
      } else {
        shouldUpdate = inst.shouldComponentUpdate(
          nextProps,
          nextState,
          nextContext,
        );
      }
    } else {// 組件沒有實現shouldComponentUpdate方法,且是PureComponent,採用shallowEqual淺比較
      if (this._compositeType === ReactCompositeComponentTypes.PureClass) {
        shouldUpdate = !shallowEqual(prevProps, nextProps) ||
          !shallowEqual(inst.state, nextState);
      }
    }
  }

shallowEqual的實現在shallowEqual.js中,大意就是作淺比較,也就是對象數組等只比較對象所處的地址是否相等,而不比較具體的內容,因爲深層次遞歸比較對象內容是否一致很耗費性能。

結論
PureComponent是Component的子類,當PureComponent手動實現了shouldComponentUpdate方法時兩個組件沒有區別,但若沒有手動實現該方法,則PureComponent採用默認的shallowEqual比較對象是否相等性能更佳。由此可能引發的頁面不刷新現象可以採用別的辦法解決,如重新生成新的對象、採用immutable.js對象等

14 TextInput首次focus時鍵盤出現緩慢,卡頓

追查:原因就是鍵盤是懶加載模式,初次出現時需要先初始化鍵盤視圖耗費時間,要想縮減首次耗時間隔,可以事先就讓鍵盤初始化完畢。js端沒想到如何做,但是原生端可以在didFinishLaunchingWithOptions方法中寫:

  UITextField *textField = [[UITextField alloc] init];
  [self.window addSubview:textField];
  [textField becomeFirstResponder];
  [textField resignFirstResponder];
  [textField removeFromSuperview];

鍵盤消失與否

TextInput聚焦時彈出了鍵盤,點擊非TextInput空白處鍵盤是不會消失的,若想實現該功能只需要讓TextInput嵌入在ScrollView中即可。
那麼問題又來了,這樣做之後,除了TextInput外屏幕上任意地方點擊鍵盤都會先消失,導致例如頁面上有個按鈕A,點擊A時會先退下鍵盤,再次點擊才能觸發A的事件,很扯淡。解決方法大體如下:

_addEvent = (event) => {
  this.events.push(event.nativeEvent.target);
};

_onStartShouldSetResponder(event) {
  const target = event.nativeEvent.target;
  if (!this.events.includes(target)) {
    Keyboard.dismiss();
  }
  return false;
}

render() {
  return (
    <ScrollView keyboardShouldPersistTaps="always">
      <View
        style={{ alignItems: 'center', flex: 1, height: SCREEN_HEIGHT }}
        onStartShouldSetResponder={(event) => this._onStartShouldSetResponder(event)}
      >
        <Button
          text="登陸"
          onLayout={(event) => this._addEvent(event)}
        />
      </View>
    </ScrollView>
  );
}

ScrollView的keyboardShouldPersistTaps屬性設置爲always,則鍵盤不再攔截點擊事件,點擊空白處鍵盤不會自動消失。
onStartShouldSetResponderCapture是點擊事件發生時調用,詢問該視圖是否要攔截事件,自定義處理,當點擊屏幕除了指定位置外都退下鍵盤。指定位置A(比如登錄按鈕)點擊時,鍵盤不退下。
A的onLayout在視圖佈局完成回調,event.nativeEvent.target能唯一的標識該組件。

15 redux中的reducer爲何要返回Object.assign

在redux-devtools中,我們可以查看到redux下所有通過reducer更新state的記錄,每一個記錄都對應着內存中某一個具體的state,讓用戶可以追溯到每一次歷史操作產生與執行時,當時的具體狀態,這也是使用redux管理狀態的重要優勢之一.

  • 若不創建副本,redux的所有操作都將指向內存中的同一個state,我們將無從獲取每一次操作前後,state的具體狀態與改變,
  • 若沒有副本,redux-devtools列表裏所有的state都將被最後一次操作的結果所取代.我們將無法追溯state變更的歷史記錄.
    創建副本也是爲了保證向下傳入的this.props與nextProps能得到正確的值,以便我們能夠利用前後props的改變情況以決定如何render組件

16 使用redux時,不能在componentWillReceiveProps方法中使用action。

原因:有時候一個action改變數據後,我們希望拿到改變後的數據做另外一個action,比如初始化action讀取硬盤中的數據到內存,然後用該參數進行請求網絡數據action。此時我們可以在componentWillReceiveProps方法中拿到參數,若此時發出再發出action,則數據返回後改變reducer會再次進入componentWillReceiveProps方法,又繼續發出action,陷入死循環。可以如下解決

componentWillReceiveProps(nextProp) {
    if(nextProp.app.user && nextProp.app.sessionId && !this.isFirstLoad){
        this.props.action(nextProp.app);   // action操作
        this.isFirstLoad = true;
    }
}

追查:發現在iphone7plus模擬器中黑線看不到,但是iphone6模擬器能看見。查看源代碼,在navigation組件中的Header.js第300行找到了黑線樣式定義,

let platformContainerStyles;
if (Platform.OS === 'ios') {
    platformContainerStyles = {
        borderBottomWidth: StyleSheet.hairlineWidth,  // hairlineWidth爲當前分辨率下能顯示的最小寬度,模擬器下可能看不見
        borderBottomColor: 'rgba(0, 0, 0, .3)',
    };
} else {
    platformContainerStyles = {
        shadowColor: 'black',
        shadowOpacity: 0.1,
        shadowRadius: StyleSheet.hairlineWidth,
        shadowOffset: {
            height: StyleSheet.hairlineWidth,
        },
        elevation: 4,
    };
}

可見在ios中下方黑線使用邊框的形式實現,而安卓則是設置圖層陰影。若想隱藏該線,ios中設置headerStyle的borderBottomWidth爲0,安卓中設置elevation/shadowOpacity爲0.
同上,可在TabBarBottom.js中180行找到tabbar上方那跟線的默認設置,更改則可在TabNavigator中的tabBarOptions的style中設置borderTopWidth和borderTopColor

18 爲何需要render的組件被保存到數組中需要設置key?

追查:跟虛擬DOM和Diff算法有關。
一次DOM操作流程包括,拿到頁面所有DOM節點,拿到css樣式表,生成render樹,佈局計算節點位置,渲染等操作。 傳統應用,一個操作如果需要改變10個DOM節點,則會相應的進行10次DOM操作,很多重複浪費性能。
虛擬DOM就是剛開始就將所有的DOM節點轉換成js的相關代碼保存到內存中,一個操作改變10次DOM節點全部在內存中完成,再將內存中的js轉換爲實際的DOM節點渲染,性能高。
虛擬DOM一個操作中10次改變DOM節點,每次只是改變了必要的那一個節點,不需要全部改變,爲了減少時間複雜度,引入Diff算法,只比較節點改變了的那一點,進行增刪改操作等。比如現在的render樹是A、B、C節點,想再A節點後面插入D節點,若沒有key,React無法區分各個節點,只能根據渲染樹的排列依次卸載B、裝載D、卸載C、裝載B、裝載C,效率低下。如果ABC節點都有key,則React就能根據key找出對應的節點,直接渲染A、D、B、C,效率高。

19 Diffing算法相關

在任何一個單點時刻 render() 函數的作用是創建 React 元素樹。在下一個 state 或props 更新時,render() 函數將會返回一個不同的 React 元素樹。 React 通過Diffing算法找出兩顆元素樹的差異,更新必須的部分,其假定規則是:
a、DOM 節點跨層級的移動操作特別少,可以忽略不計。
b、擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。
c、對於同一層級的一組子節點,它們可以通過唯一 id 進行區分。

具體的比較如下:
1、tree diff,DOM 節點跨層級的移動操作少到可以忽略不計,針對這一現象,React 通過 updateDepth 對 Virtual DOM 樹進行層級控制,只會對同一個父節點下的所有子節點。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。若有節點跨層級的移動,性能會受到影響
2、component diff,如果是同一類型的組件,按照原策略繼續比較 virtual DOM tree。如果不是,則將該組件判斷爲 dirty component,從而替換整個組件下的所有子節點。對於同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許用戶通過 shouldComponentUpdate() 來判斷該組件是否需要進行 diff。
3、element diff,當節點處於同一層級時,默認情況下,當遞歸一個 DOM 節點的子節點時,React 只需同時遍歷所有的孩子節點並更改不同點,如在列表組件追加幾個item時,性能不錯。但是當如下

<ul>
  <li>1</li>
  <li>2</li>
</ul>
<ul>
  <li>3</li>
  <li>1</li>
  <li>2</li>
</ul>

React 將會改變每一個子節點而沒有意識到需要保留 

  • 1和 2
  • 兩個子樹。這很低效。爲了解決這個問題,React 支持一個 key 屬性(attributes)。當子節點有了 key ,React 使用這個 key 去比較原來的樹的子節點和之後樹的子節點。例如,添加一個 key 到我們上面那個低效的例子中可以使樹的轉換變高效

     

    <ul>
      <li key="2015">1</li>
      <li key="2016">2</li>
    </ul>
    <ul>
      <li key="2014">3</li>
      <li key="2015">1</li>
      <li key="2016">2</li>
    </ul>

    現在 React 知道有'2014' key 的元素是新的, key爲'2015' 和'2016'的兩個元素僅僅只是被移動而已,效率變高很多。要注意key必須具備唯一性。若將數組中的索引作爲 key ,如果存在重新排序時,性能將會很差,應該避免這種情況。

20 高階組件(HOC)

高階組件是重用組件邏輯的一項高級技術。高階組件並不是React API的一部分。高階組件源自於React生態。具體來說,高階組件是一個函數,能夠接受一個組件並返回一個新的組件,例如Redux的connect函數。
HOC存在的問題:
1、組件的靜態方法不會被傳遞,需要自行傳遞處理
2、refs不會被傳遞,應該避免此,或者用自定義屬性傳遞
3、react-native-fetch-blob的POST請求不成功。
4、js傳到原生端的函數(ios中叫block)只能執行一次,否則崩潰。

21.React Native 中組件的生命週期

如圖,可以把組件生命週期大致分爲三個階段:

  • 第一階段:是組件第一次繪製階段,如圖中的上面虛線框內,在這裏完成了組件的加載和初始化;
  • 第二階段:是組件在運行和交互階段,如圖中左下角虛線框,這個階段組件可以處理用戶交互,或者接收事件更新界面;
  • 第三階段:是組件卸載消亡的階段,如圖中右下角的虛線框中,這裏做一些組件的清理工作。

組件的生命週期詳解

1.創建階段

  • 該階段主要發生在創建組件類的時候,在這個階段中會初始化組件的屬性類型和默認屬性。通常會將固定的內容放在這個過程中進行初始化和賦值。
  • 在 ES6 中統一使用 static 成員來實現。

1

2

3

4

static defaultProps = {

  autoPlay: false,

  maxLoop: 10,

};

2,實例化階段

該階段主要發生在實例化組件類的時候,也就是該組件類被調用的時候觸發。這個階段會觸發一系列的流程,按執行順序如下:

  • constructor:構造函數,這裏主要對組件的一些狀態進行初始化。
  • componentWillMount:準備加載組件,可以在這裏做一些業務初始化操作,或者設置組件狀態。
  • render:生成頁面需要的 DOM 結構,並返回該結構。
  • componentDidMount:組件加載成功並被成功渲染出來後執行。一般會將網絡請求等加載數據的操作放在這裏進行,保證不會出現 UI 上的錯誤。

3,運行(更新)階段

該階段主要發生在用戶操作之後或者父組件有更新的時候,此時會根據用戶的操作行爲進行相應的頁面結構的調整。這個階段也會觸發一系列的流程,按執行順序如下:

  • componentWillReceiveProps:當組件接收到新的 props 時,會觸發該函數。在該函數中,通常可以調用 this.setState 方法來完成對 state 的修改。
  • shouldComponentUpdate:該方法用來攔截新的 props 或 state,然後根據事先設定好的判斷邏輯,做出最後要不要更新組件的決定。
  • componentWillUpdate:當上面的方法攔截返回 true 的時候,就可以在該方法中做一些更新之前的操作。
  • render:根據一系列的 diff 算法,生成需要更新的虛擬 DOM 數據。(注意:在 render 中最好只做數據和模板的組合,不應進行 state 等邏輯的修改,這樣組件結構會更加清晰)
  • componentDidUpdate:該方法在組件的更新已經同步到 DOM 中去後觸發,我們常在該方法中做 DOM 操作。

4,銷燬階段

該階段主要在組件消亡的時候觸發。

  • componentWillUnmount:當組件要被從界面上移除時就會調用。可以在這個函數中做一些相關的清理工作,例如取消計時器、網絡請求等。

二、生命週期函數詳細介紹

1,constructor

(1)函數原型

1

constructor(props)

(2)基本介紹

  • 它是組件的構造函數。它的第一個語句必須是 super(props)。
  • 構造函數將在組件被加載前最先調用,並且僅調用一次。

(3)常見用途

構造函數最大的作用,就是在這裏定義狀態機變量。

2,componentWillMount

(1)函數原型

1

componentWillMount()

(2)基本介紹

  • 在組件的生命週期中,這個函數只會被執行一次。
  • 這個函數無參數並且不需要任何返回值。
  • 它在初始渲染(render 函數被 React Native 框架調用執行)前被執行,當它執行完後, render 函數會馬上被 React Native 框架調用執行。注意:如果在這個函數裏調用 setstate 函數改變了某些狀態機變量的值, React Native 框架不會執行渲染操作,而是等待這個函數執行完成後再執行初始渲染。
  • 如果子組件也有 componentWillMount 函數,它會在父組件的 componentWillMount 函數之後被調用。

(3)常見用途

如果我們需要從本地存儲中讀取數據用於顯示,那麼在這個函數裏進行讀取是一個很好的時機。

3,render

(1)函數原型

1

render()

(2)基本介紹

  • render 是一個組件必須有的方法,用於界面渲染。
  • 這個函數無參數,返回 JSX 或者其他組件來構成 DOM。注意:只能返回一個頂級元素。

4,componentDidMount

(1)函數原型

1

componentDidMount()

(2)基本介紹

  • 在組件的生命週期中,這個函數只會被執行一次。
  • 這個函數無參數並且不需要任何返回值。
  • 它在初始渲染執行完成後會馬上被調用。在組件生命週期的這個時間點之後,開發者可以通過子組件的引用來訪問、操作任何子組件。
  • 如果子組件也有 componentDidMount 函數,它會在父組件的 componentDidMount 函數之前被調用。

(3)常見用途

如果 React Native 應用需要在程序啓動並顯示初始界面後從網絡側獲取數據,那麼把從網絡側獲取數據的代碼放在這個函數裏是一個不錯的選擇。

5,componentWillReceiveProps

(1)函數原型

1

componentWillReceiveProps(nextProps)

(2)基本介紹

  • 組件的初始渲染執行完成後,當組件接收到新的 props 時,這個函數將被調用。
  • 這個函數不需要返回值。接收一個 object 參數, object 裏是新的 props。
  • 如果新的 props 會導致界面重新渲染,這個函數將在渲染前被執行。在這個函數中,老的 props 可以通過 this.props 訪問,新的 props 在傳入的 object 中。
  • 如果在這個函數中通過調用 this.setState 函數改變某些狀態機變量的值, React Native 框架不會執行對這些狀態機變量改變的渲染,而是等 componentWillReceiveProps 函數執行完成後一起渲染。

注意:
當 React Native 初次被渲染時,componentWillReceiveProps 函數並不會被觸發,這種機制是故意設計的。

6,shouldComponentUpdate

(1)函數原型

1

boolean shouldComponentUpdate(nextProps, nextState)

(2)基本介紹

  • 組件的初始渲染執行完成後,當組件接收到新的 state 或者 props 時這個函數將被調用。
  • 該函數接收兩個 object 參數,其中第一個是新的 props,第二個是新的 state。
  • 該函數需要返回一個布爾值,告訴 React Native 框架針對這次改變,是否需要重新渲染本組件。默認返回 true。如果此函數返回 false,React Native 將不會重新渲染本組件,相應的,該組件的 componentWillUpdate 和 componentDidUpdate 函數也不會被調用。

(3)常見用途

  • 這個函數常常用來阻止不必要的重新渲染,提高 React Native 應用程序性能。
  • 比如我們可以在該函數中比較新老版本的 state 和 props,判斷是否需要進行重新渲染。下面是一個簡單的使用樣例:

1

2

3

4

shouldComponentUpdate(nextProps, nextState) {

  if(this.state.inputedNum.length < 3) return false;

  return true;

}

7,componentWillUpdate

(1)函數原型

1

componentWillUpdate(nextProps, nextState)

(2)基本介紹

  • 組件的初始渲染執行完成後, React Native 框架在重新渲染該組件前會調用這個函數。
  • 該函數不需要返回值,接收兩個 object 參數,其中第一個是新的 props,第二個是新的 state。
  • 我們可以在這個函數中爲即將發生的重新渲染做一些準備工作,但不能在這個函數中通過 this.setState 再次改變狀態機變量的值。如果需要改變,則在 componentWillReceiveProps 函數中進行改變。

8,componentDidUpdate

(1)函數原型

1

componentDidUpdate(prevProps, prevState)

(2)基本介紹

  • 組件的初始渲染執行完成後,React Native 框架在重新渲染該組件完成後會調用這個函數。
  • 該函數不需要返回值,接收兩個 object 參數,其中第一個是渲染前的 props,第二個是渲染前的 state。

9,componentWillUnmount

(1)函數原型

1

componentWillUnmount()

(2)基本介紹

  • 在組件被卸載前,這個函數將被執行。
  • 這個函數沒有參數,也沒不需要返回值。

(3)常見用途

如果組件申請了某些資源或者訂閱了某些消息,那麼需要在這個函數中釋放資源,取消訂閱。

三、完整的樣例

下面通過一個簡單的文本顯示組件,來演示組件各個環節的運作流程。同時這裏把組件整個生命週期中所有會觸發的方法都列出來了。

原文:React Native - 組件的生命週期詳解(附:各階段調用的方法)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

import React, { Component } from 'react';

import {

  AppRegistry,

  StyleSheet,

  TextInput,

  View,

  Text,

  Clipboard

} from 'react-native';

 

export default class Main extends Component {

 

  //構造函數

  constructor(props) {

    super(props);

    console.log("constructor");

    //初始化狀態值

    this.state = {message: "歡迎訪問 hangge.com"}

  }

 

  //準備加載組件

  componentWillMount() {

    console.log("componentWillMount");

  }

 

  //渲染界面

  render() {

    console.log("render");

    return (

      <View style={styles.container}>

        <Text style={styles.info}>

          {this.state.message}

        </Text>

      </View>

    );

  }

 

  //組件加載成功並渲染出來

  componentDidMount() {

    console.log("componentDidMount");

  }

 

  //組件接收到新的 props 時觸發

  componentWillReceiveProps(nextProps) {

    console.log("componentWillReceiveProps");

  }

 

  //決定是否需要更新組件

  shouldComponentUpdate(nextProps, nextState) {

    console.log("shouldComponentUpdate");

  }

 

  //組件重新渲染前會調用

  componentWillUpdate(nextProps, nextState) {

    console.log("componentWillUpdate");

  }

 

  //組件重新渲染後會調用

  componentDidUpdate(prevProps, prevState) {

    console.log("componentDidUpdate");

  }

 

  //組件被卸載前會調用

  componentWillUnmount() {

    console.log("componentWillUnmount");

  }

}

 

const styles = StyleSheet.create({

  container:{

     flex:1,

     marginTop:40,

     alignItems:'center',

  },

  info:{

    fontSize:20,

  },

});

 

AppRegistry.registerComponent('HelloWorld', () => Main);

控制檯輸出信息如下(由於樣例中我沒有更新狀態,也沒有銷燬組件。所以也就沒有後面兩個階段): 

原文:React Native - 組件的生命週期詳解(附:各階段調用的方法)

22.當你調用setState的時候,發生了什麼事?

當調用 setState 時,React會做的第一件事情是將傳遞給 setState 的對象合併到組件的當前狀態。
 這將啓動一個稱爲和解(reconciliation)的過程。 和解(reconciliation)的最終目標是以最有效的方式,根據這個新的狀態來更新UI。 
爲此,React將構建一個新的 React 元素樹(您可以將其視爲 UI 的對象表示)。
 一旦有了這個樹,爲了弄清 UI 如何響應新的狀態而改變,React 會將這個新樹與上一個元素樹相比較( diff )。 
通過這樣做, React 將會知道發生的確切變化,並且通過了解發生什麼變化,只需在絕對必要的情況下進行更新即可最小化 UI 的佔用空間

23.props和state相同點和不同點

1.不管是props還是state的改變,都會引發render的重新渲染。 2.都能由自身組件的相應初始化函數設定初始值。

不同點 1.初始值來源:state的初始值來自於自身的getInitalState(constructor)函數;props來自於父組件或者自身getDefaultProps(若key相同前者可覆蓋後者)。

2.修改方式:state只能在自身組件中setState,不能由父組件修改;props只能由父組件修改,不能在自身組件修改。

3.對子組件:props是一個父組件傳遞給子組件的數據流,這個數據流可以一直傳遞到子孫組件;state代表的是一個組件內部自身的狀態,只能在自身組件中存在。

24.shouldComponentUpdate 應該做什麼

其實這個問題也是跟reconciliation有關係。 “和解( reconciliation )的最終目標是以最有效的方式,根據新的狀態更新用戶界面”。 
如果我們知道我們的用戶界面(UI)的某一部分不會改變, 那麼沒有理由讓 React 很麻煩地試圖去弄清楚它是否應該渲染。 
通過從 shouldComponentUpdate 返回 false, React 將假定當前組件及其所有子組件將保持與當前組件相同

25.reactJS的props.children.map函數來遍歷會收到異常提示,爲什麼?應該如何遍歷?

this.props.children 的值有三種可能:

1.當前組件沒有子節點,它就是 undefined; 
2.有一個子節點,數據類型是 object ; 
3.有多個子節點,數據類型就是 array 。
 
系統提供React.Children.map()方法安全的遍歷子節點對象

26.加載bundle的機制

要實現RN的腳本熱更新,我們要搞明白RN是如何去加載腳本的。 
在編寫業務邏輯的時候,我們會有許多個js文件,打包的時候RN會將這些個js文件打包成一個叫index.android.bundle(ios的是index.ios.bundle)的文件,所有的js代碼(包括rn源代碼、第三方庫、業務邏輯的代碼)都在這一個文件裏,啓動App時會第一時間加載bundle文件,所以腳本熱更新要做的事情就是替換掉這個bundle文件。

runtime思維導圖

2、多線程相關面試問題

多線程是一個比較輕量級的方法來實現單個應用程序內多個代碼執行路徑, 從技術角度來看,一個線程就是一個需要管理執行代碼的內核級和應用級數據結 構組合。

多線程思維導圖

3、RunLoop相關面試問題

我相信大多數開發者一樣,迷惑於runloop,最初只瞭解可以通過runloop一些監聽事件的通知來做一些事情,優化性能。關於runloop源碼的基礎知識,可以參考下面的思維導圖:

runloop思維導圖

4、設計模式相關面試問題

設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。
使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的;模式使代碼編制真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。

設計模式思維導圖

5、架構/框架相關面試問題

架構/框架思維導圖

6、算法相關面試問題

算法思維導圖

7、第三方庫相關面試問題

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章