上週公眾號釋出的以下文章:
- iOS 圖形處理概論
- 再談 __bridge, __bridge_transfer, __bridge_retained(內有彩蛋)
- Flutter 的編譯模式
- 這些流行程式語言你會哪些?
- 簡報 · 第 5 期
本期知識小集的主要內容包括:
- 再談陣列、集合、字典與 hash、isEqual 方法的關聯
- 使用 Keychain 儲存登入態需要注意的一個坑
- 給 UIView 新增陰影
- 比較三種網路框架上傳圖片過程中的不同點?
- 通過 runtime 控制導航欄的 hidden 屬性
- 關於 IAP 丟單的處理
再談陣列、集合、字典與 hash、isEqual 方法的關聯
作者: halohily
我們或多或少了解,Objective-C 中的 NSArray、NSSet、NSDictionary 與 NSObject 及其子類物件的 hash、isEqual 方法有許多聯絡,這篇小集講一下其中的一些細節。
NSArray 允許新增重複元素,新增元素時不查重,所以不呼叫上述兩個方法。在移除元素時,會對當前陣列內的元素進行遍歷,每個元素的 isEqual 方法都會被呼叫(使用 remove 方法傳入的元素作為引數),所有返回真值的元素都被移除。在字典中,不涉及 hash 方法。
NSSet 不允許新增重複元素,所以新增新元素時,該元素的 hash 方法會被呼叫。若集合中不存在與此元素 hash 值相同的元素,則它直接被加入集合,不呼叫 isEqual 方法;若存在,則呼叫集合內的對應元素的 isEqual 方法,返回真值則判等,不加入,處理結束。若返回 false,則判定集合內不存在該元素,將其加入。
從集合中移除元素時,首先呼叫它的 hash 方法。若集合中存在與其 hash 值相等的元素,則呼叫該元素的 isEqual 方法,若真值則判等,進行移除;若不存在,則會依次呼叫集合中每個元素的 isEqual 方法,只要找到一個返回真值的元素,就進行移除,並結束整個過程。(所以這樣會有其他滿足 isEqual 方法但卻被漏掉未被移除的元素)。呼叫 contains 方法時,過程類似。
因此,若某自定義物件會被加入到集合或作為字典的 key 時,需要同時重寫 isEqual 方法和 hash 方法。這樣,若集合中某元素存在,則呼叫它的 contains 和 remove 方法時,可以在 O(1) 完成查詢。否則,查詢它的時間複雜度提升為 O(n)。
值得注意的是,NSDictionary 的鍵和值都是物件型別即可。但是被設為鍵的物件需要遵守 NSCopying 協議。
使用 Keychain 儲存登入態需要注意的一個坑
作者: KANGZUBIN
今天要討論的這個問題你可能永遠都不會遇到,而且絕大部分情況下你很難在開發中事先預料到它未來可能會發生,但是一旦不幸發生了,可能就是一個很嚴重的線上問題,慘痛教訓。
我們通常會在 Keychain(鑰匙串)中儲存一些密碼、使用者登入態等敏感資料,一是可以提高儲存資料的安全性;二是當使用者解除安裝 App 後重新安裝,可以自動登入保留上次的登入態;三是同一開發者賬號下的不同 App,如果是採用同一套賬戶體系,就可以通過 Keychain Groups 共享登入態。
我們的 App 之前都是隻把使用者的登入態儲存在 Keychain 中,並在 App 啟動時去讀取它,這一直也都沒什麼問題。前一段時間我們的 App 由於業務合規的原因稽核被拒,按照蘋果的要求不得不把 App 從公司的 A 開發者賬號轉讓到 B 開發者賬號下(公司旗下有很多不同主體的開發者賬號),轉讓過程很順利,但發版後短時間內收到大面積的使用者反饋說,更新新版本後提示“登入失效,需要重新登入”。
原因很容易就可以猜到,App 從 A 轉讓到 B,就無法讀取儲存在 A 賬號下的 Keychain 資料了,使用者更新版本覆蓋安裝後,開啟 App 也就無法獲取之前的登入態了。
而且對於這種已經發生的問題,我們似乎也沒有什麼有效的補救措施,臨時加急再發一版似乎也解決不了問題,因為之前的 Keychain 資料就是讀取不到了,總不能再把 App 轉讓回去吧,?
那麼如何未雨綢繆預防以後再發生這種因為轉讓 App 導致儲存在 Keychain 中的登入態丟失讀取不到呢?(雖然出現轉讓 App 的概率非常低)
我們在新版本中採用了一種相容的方法:把使用者的登入態同時加密儲存在本地快取(Sandbox)和 Keychain 中,在 App 啟動時,優先從 Keychain 中讀取,如果 Keychain 中取不到,就從本地快取中取(然後再把本地快取的同步到 Keychain 中,因為即使 App 轉讓了,使用者更新版本覆蓋安裝後 Sandbox 中的資料是不會變的),如果兩處都取不到,就認為未登入。
你有沒有更好的解決方案?歡迎留言討論。
另外,有很多人通過 Keychain 來儲存裝置唯一標示符,也需要注意這個問題。
關於 Keychain 如何使用,可以參考蘋果官方文件:GenericKeychain,而關於 Keychain 濫用問題的討論,可以看 V2EX 的這個帖子。
給 UIView 新增陰影
作者: Lefe_x
給 UIView 新增陰影看似簡單,如果操作不當也可能會浪費你一些時間。有時候明明新增了陰影可是在 UI 上卻沒顯示出來,尤其涉及到 cell 複用的情況。這裡總結幾條陰影不顯示的原因:
- 是否設定了 masksToBounds 為 YES,設定為 masksToBounds=YES,陰影不顯示;
- 設定陰影時 view 的 frame 是否為 CGRectZero,如果是,即使設定陰影后修改 frame 不為 CGRectZero 時,也不會顯示陰影;
- 使用自動佈局時往往會遇到 frame 為 CGRectZero 時設定陰影無效,這時可以使用
layoutIfNeeded
方法;
通過 layer 設定陰影
// 陰影的顏色
self.imageView.layer.shadowColor = [UIColor blackColor].CGColor;
self.imageView.layer.shadowOpacity = 0.8;
// 陰影的圓角
self.imageView.layer.shadowRadius = 1;
// 陰影偏離的位置 (100, 50) x 方向偏離 100,y 偏離 50 正向,如果是負數正好為相反的方向
self.imageView.layer.shadowOffset = CGSizeMake(3, 4);
複製程式碼
通過 shadowPath 設定陰影
通過這種方式設定的陰影可以自定義陰影的形狀,它會使用在 layer 上設定的屬性,比如 shadowRadius。
UIEdgeInsets edges = UIEdgeInsetsMake(15, 10, 15, 10);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(-edges.left, -edges.top, CGRectGetWidth(self.imageView.frame) + edges.left + edges.right, CGRectGetHeight(self.imageView.frame) + edges.top + edges.bottom)];
self.imageView.layer.shadowPath = path.CGPath;
複製程式碼
比較三種網路框架上傳圖片過程中的不同點?
作者: 陳滿iOS
AFNetworking 上傳圖片的步驟是利用圖片設定到 request 的 HTTPBodyStream 中去,然後利用帶有圖片的 request 新建 task 上傳。HYNetworking 內部實現上傳圖片的時候,其實就是採用 AFNetworking 關於上傳圖片的 API,都是 AFNetworking 裡面一個 API。XMNetworking 上傳圖片請求也是基於 AFNetworking 上傳進行的封裝,不過比 HYNetworking 更加隱晦而已,另外它封裝了上次圖片陣列的方法。
AFNetworking
- 壓縮轉換:UIImage 例項物件通過 UIImageJPEGRepresentation (壓縮)轉換為 NSData,下面稱之為 imageData。
- 資訊整合:將 imageData 與檔名 fileName,檔案路徑 name,型別名 mimeType 整合成圖片模型(AFHTTPBodyPart)的一個物件 bodyPart 中去。
- 新增圖片模型:將上面新建好的圖片模型物件 bodyPart,向圖片輸入流(AFMultipartBodyStream)的物件 bodyStream 的陣列屬性(HTTPBodyParts)新增。
- 設定 request 的 HTTPBodyStream 屬性為 bodyStream:封裝為 requestByFinalizingMultipartFormData
- 將圖片模型物件 formData 用 AFNetwork 的 POST 請求與 uploadTaskWithStreamedRequest 方法進行上傳。
HYBNetworking
- 壓縮轉換:UIImage 例項物件通過 UIImageJPEGRepresentation 壓縮轉換為 NSData,下面稱之為 imageData。
- 資訊整合:利用 AFNetwork的appendPartWithFileData,將 imageData 與檔名 fileName,檔案路徑 name,型別名 mimeType 整合成圖片模型(AFStreamingMultipartFormData)的一個物件 formData 中去。
- 將圖片模型物件 formData 用 AFNetwork 的 POST 請求與 uploadTaskWithStreamedRequest 方法進行上傳。
XMNetworking
- 壓縮轉換:UIImage 例項物件通過 UIImageJPEGRepresentation 壓縮轉換為 NSData,下面稱之為 imageData。
- 資訊整合:利用 AFNetwork的appendPartWithFileData,將 imageData 與檔名 fileName,檔案路徑 name,型別名 mimeType 整合成圖片模型(XMUploadFormData)的一個物件 formData 中去。
- 新增圖片模型:向管理器的圖片模型陣列 uploadFormDatas 新增上面新建好的圖片模型物件 formData。
- 遍歷圖片模型陣列,獲得圖片模型,利用 AFNetwork 的 POST 請求與 uploadTaskWithStreamedRequest 方法進行上傳。
【總結】
可見,上面三種框架都是基於 AFNetworking 進行的封裝,實質的流程還是一樣的。上傳圖片的流程圖如下所示。
通過 runtime 控制導航欄的 hidden 屬性
在專案中,有時候會遇到某些個頁面需要隱藏導航欄,一般情況下,我們會在 viewWillAppear 和 viewDidDisappear 去設定 navigationBar 的 hidden 屬性,如圖一。
這裡介紹另一種方法,通過 runtime 去控制。
建立 vc 的 category,宣告 Bool 型別的 hideNavigationBar 屬性。主要思路是:重寫 initialize 方法,並且自定義一個方法通過 runtime 去替代系統的 viewWillAppear,在該自定義方法裡,就是去設定 navigationBar 的 hidden 屬性。具體程式碼如圖二。
程式碼中用到的幾個 runtime 方法簡單說明下:
//獲取類中的方法實現
class_getMethodImplementation
//替換Method
method_exchangeImplementations
//獲取類中的某個例項方法
class_getInstanceMethod
//關聯物件,相當於 setValue:forKey
objc_setAssociatedObject
複製程式碼
關於使用方法,由於 hideNavigationBar 預設是 NO,所以只在需要隱藏導航欄的頁面呼叫即可,程式碼如圖三。
關於 IAP 丟單的處理
作者: 高老師很忙
做 IAP(In-App Purchase)功能都有可能遇到丟單的問題,丟單是使用者已經付款,但是因為某種原因客戶端沒有辦法處理後續的操作,比如說根本沒有收到蘋果支付成功的回撥,或者在與伺服器驗證票據過程中斷網等等。如果處理不好,很容易擊潰使用者的對產品的信用度。
蘋果的推薦做法是合理使用 transaction,在 AppDelegate.m 的 application:didFinishLaunchingWithOptions:
方法裡新增 [[SKPaymentQueue defaultQueue] addTransactionObserver:xxx]
;如果還沒有呼叫 [[SKPaymentQueue defaultQueue] finishTransaction:xxx]
,那麼在你下次啟動 App 的時候,就會在 paymentQueue:updatedTransactions:
回撥裡收到未完成的 transaction,然後繼續進行處理,所以需要你在合適的時機去呼叫 finishTransaction 方法,比如說整個支付流程已經完成(包括已經成功驗證票據)的時候,這個可以根據你的業務情況來確定呼叫時機,這樣可以大大降低丟單的概率。不要忘了 removeTransactionObserver 哦!
如果覺得處理丟單的時間有點久,可以根據實際情況把相關資訊存到本地(如果後續處理流程有需要業務資訊的,這種情況是必須要存本地的),在切換到有網或者切換使用者或者你覺得合適的實際去處理後續流程,這樣也可以雙保險,可以把損失降到最低。
關注我們
歡迎關注我們的公眾號:iOS-Tips,也歡迎加入我們的群組討論問題。可以公眾號留言 ios
、flutter
、web
、pwa
、小程式
等關鍵詞獲取入群方式。