iOS面試題彙總

數據結構

1.數據結構的存儲一般分爲幾種?各有什麼特點

數據結構的存儲一般常用的有兩種 順序存儲結構 和 鏈式存儲結構

順序存儲結構:

比如,數組,1-2-3-4-5-6-7-8-9-10,存儲是按順序的。再比如棧和隊列等

鏈式存儲結構:

比如,數組,1-2-3-4-5-6-7-8-9-10,鏈式存儲就不一樣了 1(地址)-2(地址)-7(地址)-4(地址)-5(地址)-9(地址)-8(地址)-3(地址)-6(地址)-10(地址)。每個數字後面跟着一個地址 而且存儲形式不再是順序

2.集合結構 線性結構 樹形結構 圖形結構

  • 集合結構
    一個集合,就是一個圓圈中有很多個元素,元素與元素之間沒有任何關係 這個很簡單
  • 線性結構
    一個條線上站着很多個人。 這條線不一定是直的。也可以是彎的。也可以是值的 相當於一條線被分成了好幾段的樣子 (發揮你的想象力)。 線性結構是一對一的關係
  • 樹形結構
    做開發的肯定或多或少的知道xml 解析 樹形結構跟他非常類似。也可以想象成一個金字塔。樹形結構是一對多的關係
  • 圖形結構
    這個就比較複雜了。他呢 無窮。無邊 無向(沒有方向)圖形機構 你可以理解爲多對多 類似於我們人的交集關係

3.單向鏈表 雙向鏈表 循環鏈表

  • 單向鏈表
  • 雙向鏈表
  • 循環鏈表

4.數組和鏈表區別

  • 數組
    數組元素在內存上連續春芳,可以通過下表查找元素;插入,刪除需要移動大量元素,比較適用於元素很少的情況
  • 鏈表
    鏈表中的元素在內存中不是順序存儲的,查找慢,插入、刪除只需要對元素指針重新賦值,效率高

堆、棧和隊列

  • 堆是一種經過排序的樹形數據結構,每個節點都有一個值,通常我們所說的堆的數據結構是指二叉樹。所以堆在數據結構中通常可以被看做是一棵樹的數組對象。而且堆需要滿足一下兩個性質:
    1.堆中某個節點的值總是不大於或不小於其父節點的值;
    2.堆總是一棵完全二叉樹。
  • 堆分爲兩種情況,有最大堆和最小堆。將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆,在一個擺放好元素的最小堆中,父結點中的元素一定比子結點的元素要小,但對於左右結點的大小則沒有規定誰大誰小
  • 堆常用來實現優先隊列,堆的存取是隨意的,這就如同我們在圖書館的書架上取書,雖然書的擺放是有順序的,但是我們想取任意一本時不必像棧一樣,先取出前面所有的書,書架這種機制不同於箱子,我們可以直接取出我們想要的書。

  • 棧是限定僅在表尾進行插入和刪除操作的線性表。我們把允許插入和刪除的一端稱爲棧頂,另一端稱爲棧底,不含任何數據元素的棧稱爲空棧。棧的特殊之處在於它限制了這個線性表的插入和刪除位置,它始終只在棧頂進行。
  • 棧是一種具有後進先出的數據結構,又稱爲後進先出的線性表,簡稱 LIFO(Last In First Out)結構。也就是說後存放的先取,先存放的後取,這就類似於我們要在取放在箱子底部的東西(放進去比較早的物體),我們首先要移開壓在它上面的物體(放進去比較晚的物體)。
  • 堆棧中定義了一些操作。兩個最重要的是PUSH和POP。PUSH操作在堆棧的頂部加入一個元素。POP操作相反,在堆棧頂部移去一個元素,並將堆棧的大小減一。
  • 棧的應用—遞歸

隊列

  • 隊列是隻允許在一端進行插入操作、而在另一端進行刪除操作的線性表。允許插入的一端稱爲隊尾,允許刪除的一端稱爲隊頭。它是一種特殊的線性表,特殊之處在於它只允許在表的前端進行刪除操作,而在表的後端進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。
  • 隊列是一種先進先出的數據結構,又稱爲先進先出的線性表,簡稱 FIFO(First In First Out)結構。也就是說先放的先取,後放的後取,就如同行李過安檢的時候,先放進去的行李在另一端總是先出來,後放入的行李會在最後面出來。

輸入一棵二叉樹的根結點,求該樹的深度?

二叉樹的節點定義如下:

struct BinaryTreeNode
{
	int m_nValue ;
	BinaryTreeNode* m_pLeft;
	BinarvTreeNode* m_pRight ;
}
  • 如果一棵樹只有一個結點,它的深度爲1。
  • 如果根結點只有左子樹而沒有右子樹,那麼樹的深度應該是其左子樹的深度加1;同樣如果根結點只有右子樹而沒有左子樹,那麼樹的深度應該是其右子樹的深度加1。
  • 如果既有右子樹又有左子樹,那該樹的深度就是其左、右子樹深度的較大值再加1。
int TreeDepth(TreeNode* pRoot)
{
    if(pRoot == nullptr)
        return 0;
    int left = TreeDepth(pRoot->left);
    int right = TreeDepth(pRoot->right);

    return (left>right) ? (left+1) : (right+1);
}

輸入一課二叉樹的根結點,判斷該樹是不是平衡二叉樹?

  • 重複遍歷結點
    先求出根結點的左右子樹的深度,然後判斷它們的深度相差不超過1,如果否,則不是一棵二叉樹;如果是,再用同樣的方法分別判斷左子樹和右子樹是否爲平衡二叉樹,如果都是,則這就是一棵平衡二叉樹
  • 遍歷一遍結點
    遍歷結點的同時記錄下該結點的深度,避免重複訪問

方法1

struct TreeNode{
    int val;
    TreeNode* left;
    TreeNode* right;
};
 
int TreeDepth(TreeNode* pRoot){
    if(pRoot==NULL)
        return 0;
    int left=TreeDepth(pRoot->left);
    int right=TreeDepth(pRoot->right);
    return left>right?(left+1):(right+1);
}
 
bool IsBalanced(TreeNode* pRoot){
    if(pRoot==NULL)
        return true;
    int left=TreeDepth(pRoot->left);
    int right=TreeDepth(pRoot->right);
    int diff=left-right;
    if(diff>1 || diff<-1)
        return false;
    return IsBalanced(pRoot->left) && IsBalanced(pRoot->right);
}

方法2

bool IsBalanced_1(TreeNode* pRoot,int& depth){
    if(pRoot==NULL){
        depth=0;
        return true;
    }
    int left,right;
    int diff;
    if(IsBalanced_1(pRoot->left,left) && IsBalanced_1(pRoot->right,right)){
        diff=left-right;
        if(diff<=1 || diff>=-1){
            depth=left>right?left+1:right+1;
            return true;
        }
    }
    return false;
}
 
bool IsBalancedTree(TreeNode* pRoot){
    int depth=0;
    return IsBalanced_1(pRoot,depth);
} 

Foundation(基礎)

1.nil NIL NSNULL區別

  • nil、NIL 可以說是等價的,都代表內存中一塊空地址。
  • NSNULL 代表一個指向 nil 的對象。

2.如何實現一個線程安全的 NSMutableArray?

NSMutableArray是線程不安全的,當有多個線程同時對數組進行操作的時候可能導致崩潰或數據錯誤

  • 線程鎖:使用線程鎖對數組讀寫時進行加鎖
  • 派發隊列:在《Effective Objective-C 2.0…》書中第41條:多用派發隊列,少用同步鎖中指出:使用“串行同步隊列”(serial synchronization queue),將讀取操作及寫入操作都安排在同一個隊列裏,即可保證數據同步。而通過併發隊列,結合GCD的柵欄塊(barrier)來不僅實現數據同步線程安全,還比串行同步隊列方式更高效。

3.atomic修飾符是絕對安全嗎,爲什麼?

不是,所謂的安全只是侷限於 Setter、Getter 的訪問器方法而言的,你對它做 Release 的操作是不會受影響的。這個時候就容易崩潰了。

4.實現 isEqual 和 hash 方法時要注意什麼?

  • hash
    對關鍵屬性的hash值進行位或運算作爲hash值
  • isEqual
    ==運算符判斷是否是同一對象, 因爲同一對象必然完全相同
    判斷是否是同一類型, 這樣不僅可以提高判等的效率, 還可以避免隱式類型轉換帶來的潛在風險
    判斷對象是否是nil, 做參數有效性檢查
    各個屬性分別使用默認判等方法進行判斷
    返回所有屬性判等的與結果

5.id 和 instanceType 有什麼區別?

  • 相同點
    instancetype 和 id 都是萬能指針,指向對象。
  • 不同點
    1.id 在編譯的時候不能判斷對象的真實類型,instancetype 在編譯的時候可以判斷對象的真實類型。
    2…id 可以用來定義變量,可以作爲返回值類型,可以作爲形參類型;instancetype 只能作爲返回值類型。

self和super的區別

  • self調用自己方法,super調用父類方法
  • self是類,super是預編譯指令
  • [self class] 和 [super class] 輸出是一樣的
  • self和super底層實現原理
    1.當使用 self 調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;
    而當使用 super 時,則從父類的方法列表中開始找,然後調用父類的這個方法sd
    2.當使用 self 調用時,會使用 objc_msgSend 函數:id objc_msgSend(id theReceiver, SEL theSelector, …)
    第一個參數是消息接收者,第二個參數是調用的具體類方法的 selector,後面是 selector 方法的可變參數。以 [self setName:] 爲例,編譯器會替換成調用 objc_msgSend 的函數調用,其中 theReceiver 是 self,theSelector 是 @selector(setName:),這個 selector 是從當前 self 的 class 的方法列表開始找的 setName,當找到後把對應的 selector 傳遞過去。
    3.當使用 super 調用時,會使用 objc_msgSendSuper 函數:id objc_msgSendSuper(struct objc_super *super, SEL op, …)
    第一個參數是個objc_super的結構體,第二個參數還是類似上面的類方法的selector
    struct objc_super {
    id receiver;
    Class superClass;
    };

7.@synthesize和@dynamic分別有什麼作用?

  • @property有兩個對應的詞,一個是 @synthesize,一個是 @dynamic。如果 @synthesize和 @dynamic都沒寫,那麼默認的就是@syntheszie var = _var;
  • @synthesize 的語義是如果你沒有手動實現 setter 方法和 getter 方法,那麼編譯器會自動爲你加上這兩個方法。
  • @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶自己實現,不自動生成。(當然對於 readonly 的屬性只需提供 getter 即可)。假如一個屬性被聲明爲 @dynamic var,然後你沒有提供 @setter方法和 @getter 方法,編譯的時候沒問題,但是當程序運行到 instance.var = someVar,由於缺 setter 方法會導致程序崩潰;或者當運行到 someVar = var 時,由於缺 getter 方法同樣會導致崩潰。編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。

8.typeof 和 __typeof,typeof 的區別?

  • __typeof __() 和 __typeof() 是 C語言 的編譯器特定擴展,因爲標準 C 不包含這樣的運算符。 標準 C 要求編譯器用雙下劃線前綴語言擴展(這也是爲什麼你不應該爲自己的函數,變量等做這些)
  • typeof() 與前兩者完全相同的,只不過去掉了下劃線,同時現代的編譯器也可以理解。
  • 所以這三個意思是相同的,但沒有一個是標準C,不同的編譯器會按需選擇符合標準的寫法。

10.struct和class的區別

  • 類: 引用類型(位於棧上面的指針(引用)和位於堆上的實體對象)
  • 結構:值類型(實例直接位於棧中)

UIKit

1.UIView 和 CALayer 是什麼關係?

  • UIView 繼承 UIResponder,而 UIResponder 是響應者對象,可以對iOS 中的事件響應及傳遞,CALayer 沒有繼承自 UIResponder,所以 CALayer 不具備響應處理事件的能力。CALayer 是 QuartzCore 中的類,是一個比較底層的用來繪製內容的類,用來繪製UI
  • UIView 對 CALayer 封裝屬性,對 UIView 設置 frame、center、bounds 等位置信息時,其實都是UIView 對 CALayer 進一層封裝,使得我們可以很方便地設置控件的位置;例如圓角、陰影等屬性, UIView 就沒有進一步封裝,所以我們還是需要去設置 Layer 的屬性來實現功能。
  • UIView 是 CALayer 的代理,UIView 持有一個 CALayer 的屬性,並且是該屬性的代理,用來提供一些 CALayer 行的數據,例如動畫和繪製。

Bounds 和 Frame 的區別?

  • Bounds:一般是相對於自身來說的,是控件的內部尺寸。如果你修改了 Bounds,那麼子控件的相對位置也會發生改變。
  • Frame :是相對於父控件來說的,是控件的外部尺寸。

setNeedsDisplay 和 layoutIfNeeded 兩者是什麼關係?

  • UIView的setNeedsDisplay和setNeedsLayout兩個方法都是異步執行的。而setNeedsDisplay會自動調用drawRect方法,這樣可以拿到UIGraphicsGetCurrentContext進行繪製;而setNeedsLayout會默認調用layoutSubViews,給當前的視圖做了標記;layoutIfNeeded 查找是否有標記,如果有標記及立刻刷新。
  • 只有setNeedsLayout和layoutIfNeeded這二者合起來使用,纔會起到立刻刷新的效果。

4.談談對UIResponder的理解

UIResponder類是專門用來響應用戶的操作處理各種事件的,包括觸摸事件(Touch Events)、運動事件(Motion Events)、遠程控制事件(Remote Control Events)。我們知道UIApplication、UIView、UIViewController這幾個類是直接繼承自UIResponder,所以這些類都可以響應事件。當然我們自定義的繼承自UIView的View以及自定義的繼承自UIViewController的控制器都可以響應事件。

5.loadView的作用?

loadView方法會在每次訪問UIViewController的view(比如controller.view、self.view)而且view爲nil時會被調用,此方法主要用來負責創建UIViewController的view(重寫loadView方法,並且不需要調用[super loadView])
這裏要提一下 [super loadView],[super loadView]做了下面幾件事。

  • 它會先去查找與UIViewController相關聯的xib文件,通過加載xib文件來創建UIViewController的view,如果在初始化UIViewController指定了xib文件名,就會根據傳入的xib文件名加載對應的xib文件,如果沒有明顯地傳xib文件名,就會加載跟UIViewController同名的xib文件
  • 如果沒有找到相關聯的xib文件,就會創建一個空白的UIView,然後賦值給UIViewController的view屬性
    綜上,在需要自定義UIViewController的view時,可以通過重寫loadView方法且不需要調用[super loadView]方法。

6.使用 drawRect有什麼影響?

drawRect 方法依賴 Core Graphics 框架來進行自定義的繪製 缺點:它處理 touch 事件時每次按鈕被點擊後,都會用 setNeddsDisplay 進行強制重繪;而且不止一次,每次單點事件觸發兩次執行。這樣的話從性能的角度來說,對 CPU 和內存來說都是欠佳的。特別是如果在我們的界面上有多個這樣的UIButton 實例,那就會很糟糕了。這個方法的調用機制也是非常特別. 當你調用 setNeedsDisplay 方法時, UIKit 將會把當前圖層標記爲 dirty,但還是會顯示原來的內容,直到下一次的視圖渲染週期,纔會將標記爲 dirty 的圖層重新建立 Core Graphics 上下文,然後將內存中的數據恢復出來, 再使用 CGContextRef 進行繪製

7.keyWindow 和 delegate的window有何區別

  • delegate.window 程序啓動時設置的window對象。
  • keyWindow 這個屬性保存了[windows]數組中的[UIWindow]對象,該對象最近被髮送了[makeKeyAndVisible]消息
    一般情況下 delegate.window 和 keyWindow 是同一個對象,但不能保證keyWindow就是delegate.window,因爲keyWindow會因爲makeKeyAndVisible而變化,例如,程序中添加了一個懸浮窗口,這個時候keywindow就會變化。

WebView

1.說一下 JS 和 OC 互相調用的幾種方式?

  • js調用oc的三種方式:

根據網頁重定向截取字符串通過url scheme判斷

替換方法.context[@“copyText”]

注入對象:遵守協議JSExport,設置context[@

  • oc調用js代碼兩種方式

通過webVIew調用 webView stringByEvaluatingJavaScriptFromString: 調用

通過JSContext調用[context evaluateScript:];

2.在使用 WKWedView 時遇到過哪些問題?

白屏問題,Cookie 問題,在WKWebView上直接使用NSURLProtocol無法攔截請求,在WKWebView 上通過loadRequ發起的post請求body數據被丟失,截屏問題等

消息傳遞的方式

1.KVC實現原理

  • KVC,鍵-值編碼,使用字符串直接訪問對象的屬性。
  • 底層實現,當一個對象調用setValue方法時,方法內部會做以下操作:
    1.檢查是否存在相應key的set方法,如果存在,就調用set方法
    2.如果set方法不存在,就會查找與key相同名稱並且帶下劃線的成員屬性,如果有,則直接給成員屬性賦值
    3.如果沒有找到_key,就會查找相同名稱的屬性key,如果有就直接賦值
    4.如果還沒找到,則調用valueForUndefinedKey:和setValue:forUndefinedKey:方法

2.KVO的實現原理

  • KVO-鍵值觀察機制,原理如下:
    1.當給A類添加KVO的時候,runtime動態的生成了一個子類NSKVONotifying_A,讓A類的isa指針指向NSKVONotifying_A類,重寫class方法,隱藏對象真實類信息
    2.重寫監聽屬性的setter方法,在setter方法內部調用了Foundation的_NSSetObjectValueAndNotify 函數
  1. ()_NSSetObjectValueAndNotify函數內部
    • 首先會調用 willChangeValueForKey
    • 然後給屬性賦值
    • 最後調用 didChangeValueForKey
    • 最後調用 observer 的 observeValueForKeyPath 去告訴監聽器屬性值發生了改變
      4.重寫了dealloc做一些 KVO 內存釋放

3.如何手動觸發KVO方法

  • 手動調用willChangeValueForKey和didChangeValueForKey方法
  • 鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey: 和 didChangeValueForKey。在一個被觀察屬性發生改變之前, willChangeValueForKey: 一定會被調用,這就 會記錄舊的值。而當改變發生後, didChangeValueForKey 會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用。如果可以手動實現這些調用,就可以實現“手動觸發”了 有人可能會問只調用didChangeValueForKey方法可以觸發KVO方法,其實是不能的,因爲willChangeValueForKey: 記錄舊的值,如果不記錄舊的值,那就沒有改變一說了

4.通知和代理有什麼區別

  • 通知是觀察者模式,適合一對多的場景
  • 代理模式適合一對一的反向傳值
  • 通知耦合度低,代理的耦合度高

5.block和delegate的區別

  • delegate運行成本低,block的運行成本高
  • block出棧需要將使用的數據從棧內存拷貝到堆內存,當然對象的話就是加計數,使用完或者block置nil後才消除。delegate只是保存了一個對象指針,直接回調,沒有額外消耗。就像C的函數指針,只多做了一個查表動作。
  • delegate更適用於多個回調方法(3個以上),block則適用於1,2個回調時。

6.爲什麼Block用copy關鍵字

Block在沒有使用外部變量時,內存存在全局區,然而,當Block在使用外部變量的時候,內存是存在於棧區,當Block copy之後,是存在堆區的。存在於棧區的特點是對象隨時有可能被銷燬,一旦銷燬在調用的時候,就會造成系統的崩潰。所以Block要用copy關鍵字。

組件化

1.組件化有什麼好處?

  • 業務分層、解耦,使代碼變得可維護;
  • 有效的拆分、組織日益龐大的工程代碼,使工程目錄變得可維護;
  • 便於各業務功能拆分、抽離,實現真正的功能複用;
  • 業務隔離,跨團隊開發代碼控制和版本風險控制的實現;
  • 模塊化對代碼的封裝性、合理性都有一定的要求,提升開發同學的設計能力;
    +在維護好各級組件的情況下,隨意組合滿足不同客戶需求;(只需要將之前的多個業務組件模塊在新的主App中進行組裝即可快速迭代出下一個全新App)

2.你是如何組件化解耦的?

  • 分層
    1.基礎功能組件:按功能分庫,不涉及產品業務需求,跟庫Library類似,通過良好的接口拱上層業務組件調用;不寫入產品定製邏輯,通過擴展接口完成定製;
    2.基礎UI組件:各個業務模塊依賴使用,但需要保持好定製擴展的設計
    3.業務組件:業務功能間相對獨立,相互間沒有Model共享的依賴;業務之間的頁面調用只能通過UIBus進行跳轉;業務之間的邏輯Action調用只能通過服務提供;
  • 中間件:target-action,url-block,protocol-class

3.爲什麼CTMediator方案優於基於Router的方案?

Router的缺點:

  • 在組件化的實施過程中,註冊URL並不是充分必要條件。組件是不需要向組件管理器註冊URL的,註冊了URL之後,會造成不必要的內存常駐。註冊URL的目的其實是一個服務發現的過程,在iOS領域中,服務發現的方式是不需要通過主動註冊的,使用runtime就可以了。另外,註冊部分的代碼的維護是一個相對麻煩的事情,每一次支持新調用時,都要去維護一次註冊列表。如果有調用被棄用了,是經常會忘記刪項目的。runtime由於不存在註冊過程,那就也不會產生維護的操作,維護成本就降低了。 由於通過runtime做到了服務的自動發現,拓展調用接口的任務就僅在於各自的模塊,任何一次新接口添加,新業務添加,都不必去主工程做操作,十分透明。
  • 在iOS領域裏,一定是組件化的中間件爲openURL提供服務,而不是openURL方式爲組件化提供服務。如果在給App實施組件化方案的過程中是基於openURL的方案的話,有一個致命缺陷:非常規對象(不能被字符串化到URL中的對象,例如UIImage)無法參與本地組件間調度。 在本地調用中使用URL的方式其實是不必要的,如果業務工程師在本地間調度時需要給出URL,那麼就不可避免要提供params,在調用時要提供哪些params是業務工程師很容易懵逼的地方。
  • 爲了支持傳遞非常規參數,蘑菇街的方案採用了protocol,這個會侵入業務。由於業務中的某個對象需要被調用,因此必須要符合某個可被調用的protocol,然而這個protocol又不存在於當前業務領域,於是當前業務就不得不依賴public Protocol。這對於將來的業務遷移是有非常大的影響的。
    CTMediator的優點:
  • 調用時,區分了本地應用調用和遠程應用調用。本地應用調用爲遠程應用調用提供服務。
  • 組件僅通過Action暴露可調用接口,模塊與模塊之間的接口被固化在了Target-Action這一層,避免了實施組件化的改造過程中,對Business的侵入,同時也提高了組件化接口的可維護性。
  • 方便傳遞各種類型的參數。

4.基於CTMediator的組件化方案,有哪些核心組成?

  • CTMediator中間件:集成就可以了
  • 模塊Target_%@:模塊的實現及提供對外的方法調用Action_methodName,需要傳參數時,都統一以NSDictionary的形式傳入
  • CTMediator+%@擴展:擴展裏聲明瞭模塊業務的對外接口,參數明確,這樣外部調用者可以很容易理解如何調用接口。

內存管理

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

  • 什麼情況使用 weak 關鍵字?
    在 ARC 中,在有可能出現循環引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性
    自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性爲什麼可以被設置成weak?》
  • 不同點:
    weak 此特質表明該屬性定義了一種“非擁有關係” (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。 而 assign 的“設置方法”只會執行鍼對“純量類型” (scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。
    assign 可以用非 OC 對象,而 weak 必須用於 OC 對象

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

  • 若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分爲可變版本與不可變版本,那麼就要同時實現 NSCopying 與 NSMutableCopying 協議。
    具體步驟:
    需聲明該類遵從 NSCopying 協議
    實現 NSCopying 協議。該協議只有一個方法:
- (id)copyWithZone:(NSZone *)zone;
1

注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。

  • 重寫帶 copy 關鍵字的 setter,例如:
- (void)setName:(NSString *)name {
    //[_name release];
    _name = [name copy];
}

3.深拷貝與淺拷貝

淺拷貝只是對指針的拷貝,拷貝後兩個指針指向同一個內存空間,深拷貝不但對指針進行拷貝,而且對指針指向的內容進行拷貝,經深拷貝後的指針是指向兩個不同地址的指針。
當對象中存在指針成員時,除了在複製對象時需要考慮自定義拷貝構造函數,還應該考慮以下兩種情形:

  • 當函數的參數爲對象時,實參傳遞給形參的實際上是實參的一個拷貝對象,系統自動通過拷貝構造函數實現;
  • 當函數的返回值爲一個對象時,該對象實際上是函數內對象的一個拷貝,用於返回函數調用處。
    copy方法:如果是非可擴展類對象,則是淺拷貝。如果是可擴展類對象,則是深拷貝。
    mutableCopy方法:無論是可擴展類對象還是不可擴展類對象,都是深拷貝。

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

  • @property 的本質是實例變量(ivar)+存取方法(access method = getter + setter),即 @property = ivar + getter + setter;
    “屬性” (property)作爲 Objective-C 的一項特性,主要的作用就在於封裝對象中的數據。 Objective-C 對象通常會把其所需要的數據保存爲各種實例變量。實例變量一般通過“存取方法”(access method)來訪問。其中,“獲取方法” (getter)用於讀取變量值,而“設置方法” (setter)用於寫入變量值。
  • ivar、getter、setter 是自動合成這個類中的
    完成屬性定義後,編譯器會自動編寫訪問這些屬性所需的方法,此過程叫做“自動合成”(autosynthesis)。需要強調的是,這個過程由編譯 器在編譯期執行,所以編輯器裏看不到這些“合成方法”(synthesized method)的源代碼。除了生成方法代碼 getter、setter 之外,編譯器還要自動向類中添加適當類型的實例變量,並且在屬性名前面加下劃線,以此作爲實例變量的名字。在前例中,會生成兩個實例變量,其名稱分別爲 _firstName 與 _lastName。也可以在類的實現代碼裏通過 @synthesize 語法來指定實例變量的名字.

@protocol和category中如何使用@property

  • 在 protocol 中使用 property 只會生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協議的對象能實現該屬性
  • category 使用 @property 也是隻會生成 setter 和 getter 方法的聲明,如果我們真的需要給 category 增加屬性的實現,需要藉助於運行時的兩個函數:objc_setAssociatedObject和objc_getAssociatedObject

6.簡要說一下@autoreleasePool的數據結構??

簡單說是雙向鏈表,每張鏈表頭尾相接,有 parent、child指針
每創建一個池子,會在首部創建一個 哨兵 對象,作爲標記
最外層池子的頂端會有一個next指針。當鏈表容量滿了,就會在鏈表的頂端,並指向下一張表。

BAD_ACCESS在什麼情況下出現?

訪問了懸垂指針,比如對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。 死循環

8.使用CADisplayLink、NSTimer有什麼注意點?

CADisplayLink、NSTimer會造成循環引用,可以使用YYWeakProxy或者爲CADisplayLink、NSTimer添加block方法解決循環引用

9.iOS內存分區情況

  • 棧區(Stack)
    由編譯器自動分配釋放,存放函數的參數,局部變量的值等
    棧是向低地址擴展的數據結構,是一塊連續的內存區域
  • 堆區(Heap)
    由程序員分配釋放
    是向高地址擴展的數據結構,是不連續的內存區域
  • 全局區
    全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域
    程序結束後由系統釋放
  • 常量區
    常量字符串就是放在這裏的
    程序結束後由系統釋放
    代碼區
    存放函數體的二進制代碼
  • 注:
    在 iOS 中,堆區的內存是應用程序共享的,堆中的內存分配是系統負責的
    系統使用一個鏈表來維護所有已經分配的內存空間(系統僅僅記錄,並不管理具體的內容)
    變量使用結束後,需要釋放內存,OC 中是判斷引用計數是否爲 0,如果是就說明沒有任何變量使用該空間,那麼系統將其回收
    當一個 app 啓動後,代碼區、常量區、全局區大小就已經固定,因此指向這些區的指針不會產生崩潰性的錯誤。而堆區和棧區是時時刻刻變化的(堆的創建銷燬,棧的彈入彈出),所以當使用一個指針指向這個區裏面的內存時,一定要注意內存是否已經被釋放,否則會產生程序崩潰(也即是野指針報錯)

10.循環引用

循環引用的實質:多個對象相互之間有強引用,不能釋放讓系統回收。
如何解決循環引用?

  • 1.避免產生循環引用,通常是將 strong 引用改爲 weak 引用。 比如在修飾屬性時用weak 在block內調用對象方法時,使用其弱引用,這裏可以使用兩個宏
    #define WS(weakSelf) __weak __typeof(&*self)weakSelf = self; // 弱引用
    #define ST(strongSelf) __strong __typeof(&*self)strongSelf = weakSelf; //使用這個要先聲明weakSelf 還可以使用__block來修飾變量 在MRC下,__block不會增加其引用計數,避免了循環引用 在ARC下,__block修飾對象會被強引用,無法避免循環引用,需要手動解除。

  • 2.在合適時機去手動斷開循環引用。 通常我們使用第一種。

  • 代理(delegate)循環引用屬於相互循環引用
    delegate 是iOS中開發中比較常遇到的循環引用,一般在聲明delegate的時候都要使用弱引用 weak,或者assign,當然怎麼選擇使用assign還是weak,MRC的話只能用assign,在ARC的情況下最好使用weak,因爲weak修飾的變量在釋放後自動指向nil,防止野指針存在

  • NSTimer循環引用屬於相互循環使用
    在控制器內,創建NSTimer作爲其屬性,由於定時器創建後也會強引用該控制器對象,那麼該對象和定時器就相互循環引用了。 如何解決呢? 這裏我們可以使用手動斷開循環引用: 如果是不重複定時器,在回調方法裏將定時器invalidate並置爲nil即可。 如果是重複定時器,在合適的位置將其invalidate並置爲nil即可

  • 3.block循環引用
    一個簡單的例子:

@property (copy, nonatomic) dispatch_block_t myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock {
    self.myBlock = ^() {
        NSLog(@"%@",self.blockString);
    };
}

由於block會對block中的對象進行持有操作,就相當於持有了其中的對象,而如果此時block中的對象又持有了該block,則會造成循環引用。 解決方案就是使用__weak修飾self即可

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {
        NSLog(@"%@",weakSelf.blockString);
 };

並不是所有block都會造成循環引用。 只有被強引用了的block纔會產生循環引用 而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]這些系統方法等 或者block並不是其屬性而是臨時變量,即棧block

[self testWithBlock:^{
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();
}

還有一種場景,在block執行開始時self對象還未被釋放,而執行過程中,self被釋放了,由於是用weak修飾的,那麼weakSelf也被釋放了,此時在block裏訪問weakSelf時,就可能會發生錯誤(向nil對象發消息並不會崩潰,但也沒任何效果)。 對於這種場景,應該在block中對 對象使用__strong修飾,使得在block期間對 對象持有,block執行結束後,解除其持有。

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {

        __strong __typeof(self) strongSelf = weakSelf;

        [strongSelf test];
 };
<<<<<<< HEAD

數據存儲

1.iOS 開發中數據持久性有哪幾種?

iOS本地數據保存有多種方式,比如NSUserDefaults、歸檔、文件保存、數據庫、CoreData、KeyChain(鑰匙串)等多種方式。其中KeyChain(鑰匙串)是保存到沙盒範圍以外的地方,也就是與沙盒無關。

2.FMDB數據結構變化升級

  • 使用columnExists:inTableWithName方法判斷數據表中是否存在字段
  • 如果不存在,則添加, 如:向bbb表中添加aaa字段 -> ALTER TABLE bbb ADD ‘aaa’ TEXT

多線程

1.進程與線程

  • 進程:
    1.進程是一個具有一定獨立功能的程序關於某次數據集合的一次運行活動,它是操作系統分配資源的基本單元.
    2.進程是指在系統中正在運行的一個應用程序,就是一段程序的執行過程,我們可以理解爲手機上的一個app.
    3.每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內,擁有獨立運行所需的全部資源

  • 線程
    1.程序執行流的最小單元,線程是進程中的一個實體.
    2.一個進程要想執行任務,必須至少有一條線程.應用程序啓動的時候,系統會默認開啓一條線程,也就是主線程

  • 進程和線程的關係
    1.線程是進程的執行單元,進程的所有任務都在線程中執行
    2.線程是 CPU 分配資源和調度的最小單位
    3.一個程序可以對應多個進程(多進程),一個進程中可有多個線程,但至少要有一條線程
    4.同一個進程內的線程共享進程資源

2.什麼是多線程?

  • 多線程的實現原理:事實上,同一時間內單核的CPU只能執行一個線程,多線程是CPU快速的在多個線程之間進行切換(調度),造成了多個線程同時執行的假象。
  • 如果是多核CPU就真的可以同時處理多個線程了。
  • 多線程的目的是爲了同步完成多項任務,通過提高系統的資源利用率來提高系統的效率。

3.多線程的優點和缺點

  • 優點:
    能適當提高程序的執行效率
    能適當提高資源利用率(CPU、內存利用率)

  • 缺點:
    開啓線程需要佔用一定的內存空間(默認情況下,主線程佔用1M,子線程佔用512KB),如果開啓大量的線程,會佔用大量的內存空間,降低程序的性能
    線程越多,CPU在調度線程上的開銷就越大
    程序設計更加複雜:比如線程之間的通信、多線程的數據共享

4.多線程的 並行 和 併發 有什麼區別?

  • 並行:充分利用計算機的多核,在多個線程上同步進行
  • 併發:在一條線程上通過快速切換,讓人感覺在同步進行

5.iOS中實現多線程的幾種方案,各自有什麼特點?

  • NSThread 面向對象的,需要程序員手動創建線程,但不需要手動銷燬。子線程間通信很難。
  • GCD c語言,充分利用了設備的多核,自動管理線程生命週期。比NSOperation效率更高。
  • NSOperation 基於gcd封裝,更加面向對象,比gcd多了一些功能。

6.多個網絡請求完成後執行下一步

  • 使用GCD的dispatch_group_t
    創建一個dispatch_group_t
    每次網絡請求前先dispatch_group_enter,請求回調後再dispatch_group_leave,enter和leave必須配合使用,有幾次enter就要有幾次leave,否則group會一直存在。
    當所有enter的block都leave後,會執行dispatch_group_notify的block。
NSString *str = @"http://xxxx.com/";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];

dispatch_group_t downloadGroup = dispatch_group_create();
for (int i=0; i<10; i++) {
    dispatch_group_enter(downloadGroup);
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%d---%d",i,i);
        dispatch_group_leave(downloadGroup);
    }];
    [task resume];
}

dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
    NSLog(@"end");
});
  • 使用GCD的信號量dispatch_semaphore_t
    dispatch_semaphore信號量爲基於計數器的一種多線程同步機制。如果semaphore計數大於等於1,計數-1,返回,程序繼續運行。如果計數爲0,則等待。dispatch_semaphore_signal(semaphore)爲計數+1操作,dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)爲設置等待時間,這裏設置的等待時間是一直等待。
    創建semaphore爲0,等待,等10個網絡請求都完成了,dispatch_semaphore_signal(semaphore)爲計數+1,然後計數-1返回
NSString *str = @"http://xxxx.com/";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%d---%d",i,i);
        count++;
        if (count==10) {
            dispatch_semaphore_signal(sem);
            count = 0;
        }
    }];
    [task resume];
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

7.多個網絡請求順序執行後執行下一步

  • 使用信號量semaphore
    每一次遍歷,都讓其dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER),這個時候線程會等待,阻塞當前線程,直到dispatch_semaphore_signal(sem)調用之後
NSString *str = @"http://www.xxx.com";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSLog(@"%d---%d",i,i);
        dispatch_semaphore_signal(sem);
    }];
    
    [task resume];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

8.異步操作兩組數據時, 執行完第一組之後, 才能執行第二組

  • 這裏使用dispatch_barrier_async柵欄方法即可實現
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    NSLog(@"第一次任務的主線程爲: %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    NSLog(@"第二次任務的主線程爲: %@", [NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"第一次任務, 第二次任務執行完畢, 繼續執行");
});

dispatch_async(queue, ^{
    NSLog(@"第三次任務的主線程爲: %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    NSLog(@"第四次任務的主線程爲: %@", [NSThread currentThread]);
});

9.多線程中的死鎖?

死鎖是由於多個線程(進程)在執行過程中,因爲爭奪資源而造成的互相等待現象,你可以理解爲卡主了。產生死鎖的必要條件有四個:

  • 互斥條件 : 指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
  • 請求和保持條件 : 指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
  • 不可剝奪條件 : 指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
  • 環路等待條件 : 指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。
    最常見的就是 同步函數 + 主隊列 的組合,本質是隊列阻塞。
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2");
});

NSLog(@"1");
// 什麼也不會打印,直接報錯

10.GCD執行原理?

  • GCD有一個底層線程池,這個池中存放的是一個個的線程。之所以稱爲“池”,很容易理解出這個“池”中的線程是可以重用的,當一段時間後這個線程沒有被調用胡話,這個線程就會被銷燬。注意:開多少條線程是由底層線程池決定的(線程建議控制再3~5條),池是系統自動來維護,不需要我們程序員來維護(看到這句話是不是很開心?) 而我們程序員需要關心的是什麼呢?我們只關心的是向隊列中添加任務,隊列調度即可。
  • 如果隊列中存放的是同步任務,則任務出隊後,底層線程池中會提供一條線程供這個任務執行,任務執行完畢後這條線程再回到線程池。這樣隊列中的任務反覆調度,因爲是同步的,所以當我們用currentThread打印的時候,就是同一條線程。
  • 如果隊列中存放的是異步的任務,(注意異步可以開線程),當任務出隊後,底層線程池會提供一個線程供任務執行,因爲是異步執行,隊列中的任務不需等待當前任務執行完畢就可以調度下一個任務,這時底層線程池中會再次提供一個線程供第二個任務執行,執行完畢後再回到底層線程池中。
  • 這樣就對線程完成一個複用,而不需要每一個任務執行都開啓新的線程,也就從而節約的系統的開銷,提高了效率。在iOS7.0的時候,使用GCD系統通常只能開58條線程,iOS8.0以後,系統可以開啓很多條線程,但是實在開發應用中,建議開啓線程條數:35條最爲合理。

動畫

圖像處理

Runtime

RunLoop

網絡

算法

項目框架

設計模式

數據安全及加密

調試技巧

性能優化

源碼理解

代碼管理

持續集成

逆向(竊取密碼,防護破解,算法加密)

參考:
https://ios.nobady.cn/Multi-thread.html

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