iOS面試旗開得勝之答案篇

秦小風發表於2018-03-29

以下問題的答案是之前寫的一篇文章 iOS面試旗開得勝之問題篇 現在把問題的答案整理了一份出來給大家。希望對大家有所幫助。如果整理的答案有問題,請聯絡我。shavekevin@gmail.com

1、屬性readwrite,readonly,assign,retain,copy,nonatomic 各自什麼作用,他們在那種情況下用?

  • readwrite:預設的屬性,可讀可寫,生成setter和getter方法。

  • readonly:只讀,只生成getter方法,也就是說不能修改變數。

  • assign:用於宣告基本資料型別(int、float)僅設定變數,是賦值屬性。

  • retain:持有屬性,setter方法將傳入的引數先保留,再賦值,傳入的引數 引用計數retaincount 會加1

    在堆上開闢一塊空間,用指標a指向,然後將指標a賦值(assign)給指標b,等於是a和b同時指向這塊堆空間,當a不使用這塊堆空間的時候,是否要釋放這塊堆空間?答案是肯定要的,但是這件堆空間被釋放後,b就成了野指標。

    如何避免這樣的問題? 這就引出了引用計數器,當a指標這塊堆空間的時候,引用計數器+1,當b也指向的時候,引用計數器變成了2,當a不再指向這塊堆空間時,release-1,引用計數器為1,當b也不指向這塊堆空間時,release-1,引用計數器為0,呼叫dealloc函式,空間被釋放

    總結:當資料型別為int,float原生型別時,可以使用assign。如果是上面那種情況(物件)就是用retain。

  • copy:是賦值特性,setter方法將傳入物件賦值一份;需要完全一份新的變數時,直接從堆區拿。

    當屬性是 NSString、NSArray、NSDictionary時,既可以用strong 修飾,也可以用copy修飾。當用strong修飾的NSString 指向一個NSMutableString時,如果在不知情的情況下這個NSMutableString的別的引用修改了值,就會出現:一個不可變的字串卻被改變了的情況, 使用copy就不會出現這種情況。

  • nonatomic:非原子性,可以多執行緒訪問,效率高。

  • atomic:原子性,屬性安全級別的表示,同一時刻只有一個執行緒訪問,具有資源的獨佔性,但是效率很低。

  • strong:強引用,引用計數+ 1,ARC下,一個物件如果沒有強引用,系統就會釋放這個物件。

  • weak:弱引用,不會使引用計數+1.當一個指向物件的強引用都被釋放時,這塊空間依舊會被釋放掉。

    使用場景:在ARC下,如果使用XIB 或者SB 來建立控制元件,就使用 weak。純程式碼建立控制元件時,用strong修飾,如果想用weak 修飾,就需要先建立控制元件,然後賦值給用weak修飾的物件。

    查詢了一些資料,發現主要原因是,controller需要擁有它自己的view(這個view是所以子控制元件的父view),因此viewcontroller對view就必須是強引用(strong reference),得用strong修飾view。對於lable,它的父view是view,view需要擁有label,但是controller是不需要擁有label的。如果用strong修飾,在view銷燬的情況下,label還仍然佔有記憶體,因為controller還對它強引用;如果用wak修飾,在view銷燬的時label的記憶體也同時被銷燬,避免了殭屍指標出現。

    用引用計數回答就是:因為Controller並不直接“擁有”控制元件,控制元件由它的父view“擁有”。使用weak關鍵字可以不增加控制元件引用計數,確保控制元件與父view有相同的生命週期。控制元件在被addSubview後,相當於控制元件引用計數+1;父view銷燬後,所有的子view引用計數-1,則可以確保父view銷燬時子view立即銷燬。weak的控制元件在removeFromSuperview後也會立即銷燬,而strong的控制元件不會,因為Controller還保有控制元件強引用。

    總結歸納為:當控制元件的父view銷燬時,如果你還想繼續擁有這個控制元件,就用srtong;如果想保證控制元件和父view擁有相同的生命週期,就用weak。當然在大多數情況下用兩個都是可以的。

    使用weak的時候需要特別注意的是:先將控制元件新增到superview上之後再賦值給self,避免控制元件被過早釋放。

2、Objective-C如何對記憶體管理的,說說你的看法以及你遇到的問題以及解決方法?

​ Objective-C使用引用計數來管理記憶體,物件有個計數器,用以表示當前有多少個事物想令此物件繼續存活下去。

  • MRC 手動記憶體計數 (Reference Counted)

    retain 遞增保留計數

    release 遞減保留計數

    物件被建立出來,物件的保留計數至少為1,若想令某物件繼續存活,則呼叫retain方法。要是不想令其繼續存活,就呼叫release或autorelease。當保留計數歸零時,物件就回收了(deallocated),系統會將其佔用的記憶體標記為“可重用”。(拖物件保留計數為1 的時候 呼叫release或autorelease,不會讓計數為0,會直接釋放,因為這樣可以省一步操作)

    ​注意:物件建立出來之後,並不是說物件此時的保留計數必定是1.在alloc或initWith方法的實現程式碼中,也許還有其他物件也保留了此物件,所以,其保留計數可能會大於1.絕不應該說保留計數一定是某個值,只能說你執行的操作是遞增了引用計數或遞減了引用計數。

    autoreleasepool(自動釋放池)

    呼叫autorelease時,物件的引用計數不會馬上遞減,而是先物件放進自動釋放池,通常是在下一次“事件迴圈”時遞減。

    因為自動釋放池中的釋放操作要等到下一次事件迴圈時才會執行,所以NSLog語句在使用str物件前不需要手工保留。但是,假如要持有此物件的話(比如將其設定給例項變數),那就需要保留。

  • ARC 自動記憶體計數(Garbage Collection)

    使用ARC時,引用計數實際還是要執行的,只不過保留與釋放操作現在是由ARC自動新增。

    這種方式和java類似,在你的程式的執行過程中。始終有一個高人在背後準確地幫你收拾垃圾,你不用考慮它什麼時候開始工作,怎樣工作。你只需要明白,我申請了一段記憶體空間,當我不再使用從而這段記憶體成為垃圾的時候,我就徹底的把它忘記掉,反正那個高人會幫我收拾垃圾。遺憾的是,那個高人需要消耗一定的資源。

    ARC在呼叫這些方法時(retain、release、autorelease、dealloc),並不通過普通的Objective-C訊息派發機制,而是直接呼叫其底層C語言版本。這樣做效能更好,因為保留及釋放操作需要頻繁執行,所以直接呼叫底層函式能節省很多CPU週期。比方說,ARC會呼叫與retain等價的底層函式objc_retain。這也是不能覆寫retain、release、autorelease的原因。

3、記憶體管理的幾條原則時什麼?按照預設法則.哪些關鍵字生成的物件需要手動釋放?在和property結合的時候如何有效的避免記憶體洩露?

  • 誰申請,誰釋放  遵循Cocoa Touch的使用原則;  記憶體管理主要要避免“過早釋放”和“記憶體洩漏”,對於“過早釋放”需要注意@property設定特性時,一定要用對特性關鍵字,對於“記憶體洩漏”,一定要申請了要負責釋放,要細心。 關鍵字alloc 或new 生成的物件需要手動釋放;  設定正確的property屬性,對於retain需要在合適的地方釋放,

  • 使用new、alloc或copy方法建立一個物件時,該物件引用計數器為1。如果不需要使用該物件,可以向其傳送release或autorelease訊息,在其使用完畢時被銷燬。

    ​ 如果通過其他方法獲取一個物件,則可以假設這個物件引用計數為1,並且被設定為autorelease,不需要對該物件進行清理,如果確實需要retain這個物件,則需要使用完畢後release。

    ​ 如果retain了某個物件,需要release或autorelease該物件,保持retain方法和release方法使用次數相等。

    ​ 使用new、alloc、copy關鍵字生成的物件和retain了的物件需要手動釋放。設定為autorelease的物件不需要手動釋放,會直接進入自動釋放池。

4、MVC設計模式是什麼? 你還熟悉什麼設計模式?他們和MVC有什麼不同的地方?

  • MVC設計模式是什麼

    MVC全名是Model View Controller,是模型(model)-檢視(view)-控制器(controller)的縮寫,一種軟體設計典範,用一種業務邏輯、資料、介面顯示分離的方法組織程式碼,將業務邏輯聚集到一個部件裡面,在改進和個性化定製介面及使用者互動的同時,不需要重新編寫業務邏輯。MVC被獨特的發展起來用於對映傳統的輸入、處理和輸出功能在一個邏輯的圖形化使用者介面的結構中。

    Model(模型)是應用程式中用於處理應用程式資料邏輯的部分。   通常模型物件負責在資料庫中存取資料。

    View(檢視)是應用程式中處理資料顯示的部分。   通常檢視是依據模型資料建立的。

    Controller(控制器)是應用程式中處理使用者互動的部分。   通常控制器負責從檢視讀取資料,控制使用者輸入,並向模型傳送資料。

  • 你還熟悉什麼設計模式?

    • 代理模式

      解決什麼場景:當一個類的某些功能需要被別人來實現,但是既不明確是些什麼功能,又不明確誰來實現這些功能的時候,委託代理模式就可以派上用場。

      在cocoa框架中的Delegate模式中,委託人往往是框架中的物件(檢視中的控制元件、表檢視神馬的),代理人往往是檢視控制器物件。

      自定義一個delegate模式

    @interface A:UIView
    id transparendValueDelegate;
    @property(nomatic, retain) id transparendValueDelegate;

    @end

    @implementation A
    @synthesize transparendValueDelegate

    -(void)Call
    { 
    NSString* value = @"你好";
    [transparendValueDelegate transparendValue: value];
    }

    @end
    @interface B:UIView

    NSString* value;
    @end

    @implementation B
    -(void)transparendValue:(NSString*)fromValue
    {
    value = fromValue;
    NSLog(@"%@ ,我是B",value); 
    }
    @end
複製程式碼

使用時:

    A* a = [[A alloc] init];
    B* b = [[B alloc] init];
    a. transparendValueDelegate = b;//設定A代理委託物件為B
    [a Call];
    
複製程式碼

這樣就會輸出:

**你好,我是B**

 委託模式關鍵就在於一個“**被”**字。這個B是很被動的,隨時就會被你A Call一下。
複製程式碼
  • 觀察者模式

    觀察者模式本質上時一種釋出-訂閱模型,用以消除具有不同行為的物件之間的耦合,通過這一模式,不同物件可以協同工作,同時它們也可以被複用於其他地方Observer從Subject訂閱通知,ConcreteObserver實現重現ObServer並將其過載其update方法。一旦SubJect的例項需要通知Observer任何新的變更,Subject會傳送update訊息來通知儲存在其內部類中所註冊的Observer、在ConcreteObserverupdate方法的實際實現中,Subject的內部狀態可被取得並進行後續處理。

    通知

    在Cocoa Touch框架中NSNotificationCenterNSNotification物件實現了一對多的模型。通過NSNotificationCenter可以讓物件之間進行通訊,即便這些物件之間並不認識。

    KVO

    KVO是Cocoa提供的一種稱為鍵值觀察的機制,物件可以通過它得到其他物件特定屬性的變更通知。而這個機制是基於NSKeyValueObserving非正式些,Cocoa通過這個協議為所有遵循協議的物件提供了一種自動化的屬性監聽的功能。

    雖然通知KVO都可以對觀察者進行實現,但是他們之間還是略有不同的,由上面的例子我們可以看出通知是由一箇中心物件為所有觀察者提供變更通知,主要是廣義上關注程式事件,而KVO則是被觀察的物件直接想觀察者傳送通知,主要是繫結於特定物件屬性的值。

  • 單例模式

    單例設計模式確保對於一個給定的類只有一個例項存在,這個例項有一個全域性唯一的訪問點。它通常採用懶載入的方式在第一次用到例項的時候再去建立它。

    注意:蘋果大量使用了此模式。例如:[NSUserDefaults standardUserDefaults], [UIApplication sharedApplication], [UIScreen mainScreen], [NSFileManager defaultManager],所有的這些方法都返回一個單例物件。

    有一些情況下,只有一個例項顯得非常合理。舉例來說,你不需要有多個Logger的例項,除非你想去寫多個日誌檔案。或者一個全域性的配置處理類:實現執行緒安全的方式訪問共享例項是容易的,比如一個配置檔案,有好多個類同時修改這個檔案。

  • 工廠模式

    正式的解釋是:在基類中定義建立物件的一個介面,讓子類決定例項化哪個類。工廠方法讓一個類的例項化延遲到子類中進行。工廠方法要解決的問題是物件的建立時機,它提供了一種擴充套件的策略,很好地符合了開放封閉原則。工廠方法也叫做虛構造器(Virtual Constructor)。

    通過工廠方法建立工廠物件,然後在工廠類中定義建立基類的子類物件的方法並通過外部傳入的條件判斷去建立哪一個子類物件,不過由於OC是執行時語言,所以工廠類雖然提供了建立子類物件的方法,但是在編譯時期並不能確定物件型別,編譯時期建立的子類物件型別是基類型別,真正的型別在執行時期由子類來確定,也即此時確定為子類型別。

    優點

    極大地優化了程式碼,如果需要100個子類物件,不用再一直呼叫alloc方法去建立,而是直接通過其工廠類的一句程式碼即可實現,提高了對程式碼的複用性。同時,也可以將大量的操作放到工廠類中去處理,業務類中只負責去呼叫建立需要的物件即可。

    缺點

    因為它的實現條件之一必須存在繼承關係,所以模式中工廠類集中了所有的建立邏輯,形成一個龐大的全能類,當所有的類不是繼承自同一個父類的時候擴充套件比較困難。

5、淺複製和深複製的區別?

  • 淺拷貝

    淺拷貝就是對記憶體地址的複製,讓目標物件指標和源物件指向同一片記憶體空間。

    淺拷貝只是對物件的簡單拷貝,讓幾個物件共用一片記憶體,當記憶體銷燬的時候,指向這片記憶體的幾個指標需要重新定義才可以使用,要不然會成為野指標。

    在 iOS 裡面, 使用retain 關鍵字進行引用計數,就是一種更加保險的淺拷貝。他既讓幾個指標共用同一片記憶體空間,又可以在release 由於計數的存在,不會輕易的銷燬記憶體,達到更加簡單使用的目的。

  • 深拷貝

    深拷貝是指拷貝物件的具體內容,而記憶體地址是自主分配的,拷貝結束之後,兩個物件雖然存的值是相同的,但是記憶體地址不一樣,兩個物件也互不影響,互不干涉。

    copy 與 retain 的區別:

    copy 是建立一個新物件,retain 是建立一個指標,引用物件計數加一。 copy屬性標識兩個物件內容相同,新的物件retain count為1, 與舊有物件引用計數無關,舊有物件沒有變化。copy減少物件對上下文的依賴。

    iOS提供了copy和mutableCopy方法,顧名思義,copy就是複製了一個imutable的物件,而mutableCopy就是複製了一個mutable的物件。以下將舉幾個例子來說明。 這裡指的是NSString, NSNumber等等一類的物件。

NSString *string = @”dddd";

NSString *stringCopy = [string copy];

NSMutableString *stringDCopy = [string mutableCopy];

[stringMCopy appendString:@``"!!"``];
複製程式碼

檢視記憶體可以發現,string和stringCopy指向的是同一塊記憶體區域(weak reference),引用計數沒有發生改變。而stringMCopy則是我們所說的真正意義上的複製,系統為其分配了新記憶體,是兩個獨立的字串內容是一樣的。

  • 當然在 ios 中並不是所有的物件都支援copy,mutableCopy,遵守NSCopying協議的類可以傳送copy訊息,遵守NSMutableCopying協議的類才可以傳送mutableCopy訊息。

    copy構造

- (id)copyWithZone:(NSZone *)zone{

    MyObj *copy = [[[self class] allocWithZone :zone] init];

    copy->name = [_name copy];

    copy->imutableStr = [_imutableStr copy];

    copy->age = age;

    return copy;

}
複製程式碼

mutableCopy構造

- (id)mutableCopyWithZone:(NSZone *)zone{

    MyObj *copy = NSCopyObject(self, 0, zone);

    copy->name = [_name mutableCopy];

    copy->age = age;

    return copy;

}
複製程式碼

6、什麼是KVO和KVC?他們的使用場景是什麼?

  • KVC

    KVC,即是指 NSKeyValueCoding,一個非正式的 Protocol,提供一種機制來間接訪問物件的屬性。KVO 就是基於 KVC 實現的關鍵技術之一。

    一個物件擁有某些屬性。比如說,一個 Person 物件有一個 name 和一個 address 屬性。以 KVC 說法,Person 物件分別有一個 value 對應他的 name 和 address 的 key。 key 只是一個字串,它對應的值可以是任意型別的物件。從最基礎的層次上看,KVC 有兩個方法:一個是設定 key 的值,另一個是獲取 key 的值。

    說白了就是通過指定的key獲得想要的值value。而不是通過呼叫Setter、Getter方法訪問。

    • 注意

      (1). key的值必須正確,如果拼寫錯誤,會出現異常

      (2). 當key的值是沒有定義的,valueForUndefinedKey:這個方法會被呼叫,如果你自己寫了這個方法,key的值出錯就會呼叫到這裡來

      (3). 因為類key反覆巢狀,所以有個keyPath的概念,keyPath就是用.號來把一個一個key連結起來,這樣就可以根據這個路徑訪問下去

      (4). NSArray/NSSet等都支援KVC

    • 底層原理

      • 當一個物件呼叫setValue:forKey: 方法時,方法內部會做以下操作:

      1.判斷有沒有指定key的set方法,如果有set方法,就會呼叫set方法,給該屬性賦值 2.如果沒有set方法,判斷有沒有跟key值相同且帶有下劃線的成員屬性(_key).如果有,直接給該成員屬性進行賦值 3.如果沒有成員屬性_key,判斷有沒有跟key相同名稱的屬性.如果有,直接給該屬性進行賦值 4.如果都沒有,就會呼叫 valueforUndefinedKey 和setValue:forUndefinedKey:方法。

    • 使用場景

      1、賦值:setValue:forkey。

      2、字典轉模型:KVC,使用setValuesForKeysWithDictionary:方法,該方法預設根據字典中每個鍵值對,呼叫setValue:forKey方法

      缺點:字典中的鍵值對必須與模型中的鍵值對完全對應,否則程式會崩潰

      3、取值:valueForKey

  • KVO

    Key-Value Observing (KVO) 建立在 KVC 之上,它能夠觀察一個物件的 KVC key path 值的變化。說白了就是你關心的一個值改變了,你就會得到通知。你就可以在你想處理的地方處理這個值。

    1、為物件新增一個觀察者(監聽器)

    2、設定監聽事件

    3、取消監聽

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

iOS面試旗開得勝之答案篇

  • 主要分為三大步
    • 第一步:尋找該屬性有沒有setsetter方法?有,就直接賦值
    • 第二步:尋找有沒有該屬性的成員屬性?有,就直接賦值
    • 第三步:尋找有沒有該屬性帶下劃線的成員屬性?有,就直接賦值

7、通知和協議有哪些不同之處?

  • 通知

    需要有一個通知中心:NSNotificationCenter,自定義通知的話需要給一個名字,然後監聽。

    • 優點: 通知的傳送者和接受者都不需要知道對方。可以指定接收通知的具體方法。通知名可以是任何字串。
    • 缺點: 較鍵值觀察(KVO)需要多點程式碼,在刪掉前必須移除監聽者。
  • 協議

    通過setDelegate來設定代理物件,最典型的例子是常用的 TableView.

    • 優點:支援它的類有詳盡和具體資訊。
    • 缺點:該類必須支援委託。某一時間只能有一個委託連線到某一物件。

8、在iOS應用有哪些方式儲存本地資料?他們都應用在哪些場景?

  • 沙盒

    • Documents:儲存使用者產生的資料,iTunes同步裝置的時候會備份該目錄。使用者產生的資料就是指使用者在使用當前app的時候儲存的一些資料,比如儲存app中的圖片、儲存下載的檔案等。
    • Library:這個目錄下有2個資料夾,一個是Caches、一個是Preferences,Caches主要儲存快取資料,比如SDWebImage把快取的圖片就存放到該目錄下。當程式退出後,改目錄儲存的檔案一直存在。 PreferencesXcode6之前儲存的是偏好設定,比如NSUserDefaults儲存的檔案。但是Xcode6以上就儲存到/Users/使用者名稱/Library/ Developer/CoreSimulator/Devices/模擬器UDID/data/Library/Preferences/資料夾下。
    • tmp:儲存程式中的臨時資料,當程式退出後系統會自動刪除tmp中所有的檔案。
  • NSUserDefaults

    • NSUserDefaults 是個單例物件,在整個程式的生命週期中都只有一個例項。

    • NSUserDefaults 儲存的資料型別:NSNumber, 基本資料型別(int,NSInter,float,double,CGFlat......), NSString, NSData, NSArray, NSDictionary, NSURL。

    • NSUserDefaults:一般儲存配置資訊,比如使用者名稱、密碼、是否儲存使用者名稱和密碼、是否離線下載等一些配置條件資訊。

    • 用法

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    //儲存值(key值同名的時候會覆蓋的)  

    [defaults setObject:@"使用者名稱" forKey:kUsernameKey];

    //立即儲存

    [defaults synchronize];

    //取值

    NSString *username = [defaults objectForKey:kUsernameKey];
複製程式碼
  • 儲存的一些方法
    //儲存NSInteger
    [defaults setInteger:(NSInteger) forKey:(nonnull NSString *)];
    //儲存BOOL
    [defaults setBool:(BOOL) forKey:(nonnull NSString *)];
    //儲存NSURL
    [defaults setURL:(nullable NSURL *) forKey:(nonnull NSString *)];
    //儲存float
    [defaults setFloat:(float) forKey:(nonnull NSString *)];
    //儲存double
    [defaults setDouble:(double) forKey:(nonnull NSString *)];
複製程式碼
  • 取值方法
    [defaults integerForKey:(nonnull NSString *)];
    [defaults boolForKey:(nonnull NSString *)];
    [defaults URLForKey:(nonnull NSString *)];
    [defaults floatForKey:(nonnull NSString *)];
    [defaults doubleForKey:(nonnull NSString *)];
複製程式碼
  • 刪除方法
    [defaults removeObjectForKey:(nonnull NSString *)];
複製程式碼
  • 歸檔(序列化)

    • 一般儲存自定義的物件,但是隻有遵守NSCoding的類才能只用歸檔。

    • 準守NSCoding協議必須要實現兩個require方法

      • (void)encodeWithCoder:(NSCoder *)aCoder //歸檔會觸發
      • - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder //解歸檔會觸發
    • Coding 類具體實現:

      @interface Coding : NSObject<NSCoding>
      @property (nonatomic, copy) NSString *name;
      @property (nonatomic, assign) NSInteger age;
      複製程式碼
   
   #import "Coding.h"
    #import <objc/runtime.h>
    @implementation Coding
     /**
     *  根據類動畫獲取類的所有屬性,不要忘記匯入#import <objc/runtime.h>
     *
     *  @param cls <#cls description#>
     *
     *  @return <#return value description#>
     */
    - (NSArray *)perperiesWithClass:(Class)cls
    {

        NSMutableArray *perperies = [NSMutableArray array];

        unsigned int outCount;
        //動態獲取屬性
        objc_property_t *properties = class_copyPropertyList(cls, &outCount);

        //遍歷person類的所有屬性
        for (int i = 0; i < outCount; i++)
        {
            objc_property_t property = properties[i];
            const char *name = property_getName(property);

            NSString *s = [[NSString alloc] initWithUTF8String:name];

            [perperies addObject:s];

        }

        return perperies;
    }

    /**
     *  歸檔會觸發
     *
     *  @param aCoder <#aCoder description#>
     */
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
        for (NSString *perperty in [self perperiesWithClass:[self class]])
        {
            [aCoder encodeObject:perperty forKey:perperty];
        }
    }

    /**
     *  解歸檔會觸發
     *
     *  @param aDecoder <#aDecoder description#>
     *
     *  @return <#return value description#>
     */
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        if (self = [super init])
        {
            for (NSString *perperty in [self perperiesWithClass:[self class]])
            {
                [self setValue:[aDecoder decodeObjectForKey:perperty] forKey:perperty];;
            }

        }

        return self;
    }

    @end
複製程式碼
  • 歸檔具體使用
    Coding *coding1 = [[Coding alloc] init];
     coding1.name = @"小明";
     coding1.age = 12;

     Coding *coding2 = [[Coding alloc] init];
     coding1.name = @"小王";
     coding1.age = 20;

     NSArray *array = @[coding1, coding2];

     //儲存物件轉化為二進位制資料(一定是可變物件)
     NSMutableData *data = [NSMutableData data];

     //1.初始化
     NSKeyedArchiver *archivier = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
     //2.歸檔
     [archivier encodeObject:array forKey:@"key"];

     //3.完成歸檔
     [archivier finishEncoding];

     //4.儲存
     [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"data"];
複製程式碼
  • 解歸檔的具體使用:
    //1.獲取儲存的資料
     NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"data"];

     //2.初始化解歸檔物件
     NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

     //3.解歸檔
     NSArray *persons = [unarchiver decodeObjectForKey:@"key"];

     //4.完成解歸檔
     [unarchiver finishDecoding];
複製程式碼
  • plist檔案儲存

    • 一般在iOS用plist儲存,plist本身就是XML檔案,名字字尾為.plist

    • plist主要儲存的資料型別為NSStringNSNumberNSDataNSArrayNSDictionary

    • 具體實現:

    //把字典寫入到plist檔案,比如檔案path為:~/Documents/data.plist
    [dictionary writeToFile:path atomically:YES];
    //把陣列寫入到plist檔案中
    [array writeToFile:path atomically:YES];
複製程式碼
  • 讀取資料
    NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:(nonnull NSString *)]];
    NSDictionary *dictionary =  [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:(nullable NSString *) ofType:(nullable NSString *)]];
複製程式碼
    NSArray *array = [NSArray arrayWithContentsOfURL:[NSURL fileURLWithPath:(nonnull NSString *)]];
    NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:(nullable NSString *) ofType:(nullable NSString *)]];
複製程式碼
  • 資料庫

    • iOS用的sqlite3, 使用sqlite3需要配置庫檔案libsqlite3.tbd或者匯入libsqlite3.0.tbd,這兩個庫匯入任何一個都可以

    • 儲存大量資料可以優先考慮用資料庫,sql語句對查詢操作有優化作用,所以從查詢速度或者插入效率都是很高的。

    • sqlite使用步驟:

      • 指定資料庫路徑。

      • 建立sqlite3物件並且開啟資料庫。

      • 建立表。

      • 對資料庫操作,包括增刪改查。

      • 關閉資料庫。

    • 具體實現:

      • 資料庫路徑

        //返回資料庫路徑,儲存到Cache目錄下
        -(NSString *)databasePath
        {
          NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
        
          return [path stringByAppendingPathComponent:@"contacts.db"];
        }
        複製程式碼
      • 建立sqlite3物件並且開啟資料庫,如果資料庫開啟成功,就建立表。

      //資料庫物件
        sqlite3 *contactDB;
        
        const char *path = [[self databasePath] UTF8String];

       if (sqlite3_open(path, &contactDB) == SQLITE_OK)
       {
            char *errMsg;
            const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS(ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT,PHONE TEXT)";
           //執行語句
          if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) != SQLITE_OK)
            {
                    //建立表失敗
             }
       }
       else 
        {
                //開啟資料庫失敗
        }
        sqlite3_close(contactDB);
複製程式碼
  • 程式碼解釋:

    • sqlite3_open:開啟指定路徑的資料庫,如果資料庫不存在,就會建立一個新的資料庫。
    • SQLITE_OK 是一個常量,表示開啟資料庫成功。
    • contactDB 就是資料庫物件。
    • sqlite3_exec就是執行sql語句方法。
    • sqlite3_close關閉資料庫,一般暫時不用資料庫的時候手動關閉,防止資源浪費。
  • 儲存資料到資料庫

      //是一個抽象型別,是一個控制程式碼,在使用過程中一般以它的指標進行操作
      sqlite3_stmt *statement;

      //資料庫路徑 
      const char *path = [[self databasePath] UTF8String];

      //使用的時候開啟資料庫
      if (sqlite3_open(path, &contactDB) == SQLITE_OK)
      {
          NSString *insertSQL = [NSString stringWithFormat:@"INSERT INTO CONTACTS (name,address,phone) VALUES(\"%@\",\"%@\",\"%@\")",name.text,address.text,phone.text];

          const char *insert_stmt = [insertSQL UTF8String];
         // 這個函式將sql文字轉換成一個準備語句(prepared statement)物件,同時返回這個物件的指標。這個介面需要一個資料庫連線指標以及一個要準備的包含SQL語句的文字。它實際上並不執行這個SQL語句,它僅僅為執行準備這個sql語句
          sqlite3_prepare_v2(contactDB, insert_stmt, -1, &statement, NULL);
          //執行這個sql
          if (sqlite3_step(statement) == SQLITE_DONE)
          {
              //TODO:已儲存到資料庫;
          }
          else
          {
              //TODO:儲存失敗
          }
          //銷燬statement物件
          sqlite3_finalize(statement);
          //關閉資料庫
          sqlite3_close(contactDB);
      }
複製程式碼
  • 查詢操作
      //資料庫路徑
      const char *path = [[self databasePath] UTF8String];
      //查詢結果集物件控制程式碼
      sqlite3_stmt *statement;

      //開啟資料庫
      if (sqlite3_open(path, &contactDB) == SQLITE_OK)
      {
          //查詢的sql語句
          NSString *querySQL = [NSString stringWithFormat:@"SELECT address,phone from contacts where name=\"%@\"",name.text];
          const char *query_stmt = [querySQL UTF8String];

          //執行查詢sql語句
          if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK) 
          {
              //遍歷每條資料
              if (sqlite3_step(statement) == SQLITE_ROW) 
              {
                  //獲取每條資料的欄位。
                  NSString *addressField = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, 0)];
                  address.text = addressField;

                  NSString *phoneField = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, 1    )];
                  phone.text = phoneField;

                  //TODO:已查到結果
              }
              else
              {
                  //TODO:未查到結果
              }
              sqlite3_finalize(statement);
          }

          sqlite3_close(contactDB);
      }
複製程式碼
  • CoreData

    • CoreData提供了一種“物件-關係對映”的功能,能將OC物件轉化成資料,儲存Sqlite中。

    • CoreData的好處就是能夠合理管理記憶體,避免sql語句的麻煩(不用寫sql語句)。

    • CoreData構成

      • NSManagedObjectContext:被管理的資料上下文,主要作用:插入、查詢、刪除。
      • NSManagedObjectModel:資料庫所有的表結構和資料結構,包含各個實體的定義的資訊。主要作用就是新增實體、實體屬性,建立屬性之間的關係。
      • NSPersistentStoreCoordinator持久化儲存助理物件,相當於資料庫的聯結器。主要作用就是設定儲存的名字、位置、儲存方式。
      • NSFetchRequest相當於select語句。查詢封裝物件。
      • NSEntityDescription實體結構物件,相當於表格結構。
      • 字尾為xxx.xcdatamodeld檔案,編譯後為xxx.momd的檔案。
    • 儲存資料

    - (NSManagedObjectContext *)context
    {
     AppDelegate *app = [UIApplication sharedApplication].delegate;

     return app.managedObjectContext;
    }
複製程式碼
    //建立Person物件
    /*
    insertNewObjectForEntityForName:就是建立的實體名字。
    inManagedObjectContext:上下文,appDelegate裡面已經建立完成。
    */
     Person *person = [NSEntityDescription
                       insertNewObjectForEntityForName:@"Person"
                       inManagedObjectContext:[self context]];

     //賦值
     [person setValue:@"小王" forKey:@"name"];
     [person setValue:@(35) forKey:@"age"];

     //儲存
     if (![[self context] save:nil])
     {
        //TODO:儲存失敗
     }
複製程式碼
  • 查詢
    //建立查詢物件
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];

    #if 0
        //條件查詢
        //NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<=35"];
        //查詢名字帶有王的
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like[cd]'*王*'"];
    //設定查詢條件
        request.predicate = predicate;
    #endif

        //排序
        NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
     //設定排序條件
        request.sortDescriptors = @[sort];

        //執行查詢
        NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];

       //遍歷查詢結果
        for (Person *p in objectArray)
        {
            NSLog(@"%@ - %@",[p valueForKey:@"name"],[p valueForKey:@"age"]);
        }
複製程式碼
  • 修改
    //先查詢要修改的物件
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];

        //設定查詢條件
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name='小王' and age = 35"];
        request.predicate = predicate;

        //執行查詢
        NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];

        //遍歷要修改的物件
        for (Person *p in objectArray)
        {
            //修改(修改記憶體資料,沒有同步資料庫)
            [p setValue:@(45) forKey:@"age"];
        }
        //同步資料庫
        [[self context] save:nil];
複製程式碼
  • 刪除
    //查詢要刪除的資料
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];

        //設定查詢條件
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name='小王'"];
        request.predicate = predicate;

        //執行查詢
        NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];

        //遍歷刪除
        for (Person *p in objectArray)
        {
            //刪除記憶體中的資料
            [[self context] deleteObject:p];
         }

        //同步資料庫
        [[self context] save:nil];
複製程式碼
  • 當app更新版本,並且表結構有修改,需要版本升級和資料遷移操作,否則app就是崩掉。

  • KeyChain

    • 鑰匙串(英文: KeyChain)是蘋果公司Mac OS中的密碼管理系統。

    • 一個鑰匙串可以包含多種型別的資料:密碼(包括網站,FTP伺服器,SSH帳戶,網路共享,無線網路,群組軟體,加密磁碟映象等),私鑰,電子證照和加密筆記等。

    • iOS的KeyChain服務提供了一種安全的儲存私密資訊(密碼,序列號,證照等)的方式。每個iOS程式都有一個獨立的KeyChain儲存。從iOS 3.0開始,跨程式分享KeyChain變得可行。

    • 當應用程式被刪除後,儲存到KeyChain裡面的資料不會被刪除,所以KeyChain是儲存到沙盒範圍以外的地方。

    • KeyChain的所有資料也都是以key-value的形式儲存的,這和NSDictionary的儲存方式一樣。

    • 相比於NSUserDefaults來說,KeyChain儲存更為安全,而且KeyChain裡面儲存的資料不會因為app刪除而丟失。

    • 基本使用

      為了使用方便,我們使用github上封裝好的類KeychainItemWrapperSFHFKeychainUtils

      • KeychainItemWrapper是蘋果封裝的類,封裝了操作KeyChain的基本操作,下載地址:github.com/baptistefet…
      // 初始化一個儲存使用者帳號的KeychainItemWrapper 
      KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"Your Apple ID" accessGroup:@"YOUR_APP_ID.com.yourcompany.AppIdentifier"];
      //儲存帳號
      [wrapper setObject:@"<帳號>" forKey:(id)kSecAttrAccount];  
      //儲存密碼
      [wrapper setObject:@"<帳號密碼>" forKey:(id)kSecValueData];

      //從keychain裡取出帳號密碼
      NSString *password = [wrapper objectForKey:(id)kSecValueData];

      //清空設定
      [wrapper resetKeychainItem];
複製程式碼
  • 上面程式碼的setObject: forKey: 裡引數forKey的值應該是Security.framework裡標頭檔案SecItem.h裡定義好的key

  • SFHFKeychainUtils是另外一個第三方庫,這個類比KeychainItemWrapper要簡單很多,提供了更簡單的方法儲存密碼到KeyChain,下載地址:github.com/ldandersen/… 這個庫是mrc,匯入後可能會因為mrc會報錯。

  • SFHFKeychainUtils就3個方法:

      //獲取密碼密碼
      +(NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
      //儲存密碼
      +(BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
      //刪除密碼
      +(BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;

複製程式碼
  • 引數說明

    • username:因為KeyChain儲存也是以鍵值對存在,所以這個可以看作key,根據key取value.
    • forServiceName :這個就是組的名字,可以理解為KeyChain儲存是分組儲存。一般要唯一哦,命名可以使用YOUR_APP_ID.com.yourcompany.AppIdentifier。
  • 如果兩個應用的usernameserviceName引數一樣,那麼這兩個app會共用KeyChain裡面的資料,也就是可以共享密碼。

  • KeyChain還有一個用途,就是替代UDID。UDID已經被廢除了,所以只能用UUID代替,所以我們可以把UUID用KeyChain儲存。

      //建立一個uuid
      NSString *uuidString = [self uuidString];
      //31C75924-1D2E-4AF0-9C67-96D6929B1BD3

      [SFHFKeychainUtils storeUsername:kKeyChainKey andPassword:uuidString forServiceName:kKeyChainGroupKey updateExisting:NO error:nil];

      -(NSString *)uuidString
      {
        //建立一個uuid
        CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
        CFStringRef stringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef);

        NSString *uuidString = (__bridge NSString *)(stringRef);

        CFRelease(uuidRef);

        return uuidString;
      }
複製程式碼

答案整理:少聰:github &&簡書

QQ技術交流群:214541576

微信公眾號:shavekevin

熱愛生活,分享快樂。好記性不如爛筆頭。多寫,多記,多實踐,多思考。

相關文章