iOS摸魚週報 第四十四期

語言: CN / TW / HK

本期概要

  • 話題:Apple 將推出 Tap to Pay 功能
  • Tips:解決 iOS 15 上 APP 莫名其妙地退出登入
  • 面試模組:Dealloc 使用注意事項及解析
  • 優秀部落格:ARM64 彙編入門及應用
  • 學習資料:Github: How to Cook
  • 開發工具:檔案搜尋應用:EasyFind

本期話題

@zhangferry:Apple 將在 iPhone 上推出 Tap to Pay 功能,即可以通過簡單的操作行為 -- 輕觸,完成在商戶端的付款過程。該功能通過 NFC 實現,非常安全,支援 Apple Pay、非接觸式信用卡、借記卡以及其他數字錢包,這意味著 iPhone 將具備類似 POS 的功能,客戶可以直接在商戶的 iPhone 上刷信用卡進行消費。該功能僅 iPhone XS 及之後的機型支援。

Stripe 將成為第一個在 iPhone 上向其商業客戶提供 Tap to Pay 的支付平臺。其他支付平臺和應用程式將在今年晚些時候推出。

Apple empowers businesses to accept contactless payments through Tap to Pay on iPhone

開發Tips

整理編輯:FBY展菲

解決 iOS 15 上 APP 莫名其妙地退出登入

復現問題

在 iOS 15 正式版推出後, 我們開始收到使用者的反饋:在開啟我們的App (Cookpad) 時,使用者莫名其妙地被強制退出帳號並返回到登入頁。非常令人驚訝的是,我們在測試 iOS 15 beta 版的時候並沒有發現這個問題。

我們沒有視訊,也沒有具體的步驟來重現這個問題,所以我努力嘗試以各種方式啟動應用程式,希望能親手重現它。我試著重新安裝應用程式,我試著在有網路連線和沒有網路連線的情況下啟動,我試著強制退出,經過 30 分鐘的努力,我放棄了,我開始回覆使用者說我沒找到具體問題。

直到我再次解鎖手機,沒有做任何操作,就啟動了 Cookpad,我發現 APP 就像我們的使用者所反饋的那樣,直接退出到了登入介面!

在那之後,我無法準確的復現該問題,但似乎與暫停使用手機一段時間後再次使用它有關。

縮小問題範圍

我擔心從 Xcode 重新安裝應用程式可能會影響問題的復現,所以我首先檢查程式碼並試圖縮小問題的範圍。根據我們的實現,我想出了三個懷疑的原因。

  • 1、UserDefaults 中的資料被清除。
  • 2、一個意外的 API 呼叫返回 HTTP 401 並觸發退出登入。
  • 3、Keychain 丟擲了一個錯誤。

我能夠排除前兩個懷疑的原因,這要歸功於我在自己重現該問題後觀察到的一些微妙行為。

  • 登入介面沒有要求我選擇地區 —— 這表明 UserDefaults 中的資料沒有問題,因為我們的 "已顯示地區選擇 "偏好設定仍然生效。
  • 主使用者介面沒有顯示,即使是短暫的也沒有 —— 這表明沒有嘗試進行網路請求,所以 API 是問題原因可能還為時過早。

這就把Keychain留給了我們,指引我進入下一個問題。是什麼發生了改變以及為什麼它如此難以復現?

尋找根本原因

我的除錯介面很有用,但它缺少了一些有助於回答所有問題的重要資訊:時間

我知道在 AppDelegate.application(_:didFinishLaunchingWithOptions:) 之前,“受保護的資料” 是不可用的,但它仍然沒有意義,因為為了重現這個問題,我正在執行以下操作:

1、啟動應用程式 2、簡單使用 3、強制退出應用 4、鎖定我的裝置並將其放置約 30 分鐘 5、解鎖裝置 6、再次啟動應用

每當我在第 6 步中再次啟動應用程式時,我 100% 確定裝置已解鎖,因此我堅信我應該能夠從 AppDelegate.init() 中的 Keychain 讀取資料。

直到我看了所有這些步驟的時間,事情才開始變得有點意義。

再次仔細檢視時間戳: - main.swift — 11:38:47 - AppDelegate.init() — 11:38:47 - AppDelegate.application(_:didFinishLaunchingWithOptions:) — 12:03:04 - ViewController.viewDidAppear(_:) — 12:03:04

在我真正解鎖手機並點選應用圖示之前的 25 分鐘,應用程式本身就已經啟動了!

現在,我實際上從未想過有這麼大的延遲,實際上是 @_saagarjha 建議我檢查時間戳,之後,他指給我看這條推特。

推特翻譯: 有趣的 iOS 15 優化。Duet 現在試圖先發制人地 "預熱" 第三方應用程式,在你點選一個應用程式圖示前幾分鐘,通過 dyld 和預主靜態初始化器執行它們。然後,該應用程式被暫停,隨後的 "啟動" 似乎更快。

現在一切都說得通了。我們最初沒有測試到它,因為我們很可能沒有給 iOS 15 beta 版足夠的時間來 "學習" 我們的使用習慣,所以這個問題只在現實生活的場景中再現,即裝置認為我很快就要啟動應用程式。我仍然不知道這種預測是如何形成的,但我只想把它歸結為 "Siri 智慧",然後就到此為止了。

結論

從 iOS 15 開始,系統可能決定在使用者實際嘗試開啟你的應用程式之前對其進行 "預熱",這可能會增加受保護的資料在你認為應該無法使用的時候的被訪問概率。

通過等待 application(_:didFinishLaunchingWithOptions:) 委託回撥來避免App 受此影響,如果可以的話,留意 UIApplication.isProtectedDataAvailable(或對應委託的回撥/通知)並相應處理。

我們仍然發現了極少數的非致命問題,在 application(_:didFinishLaunchingWithOptions:) 中屬性 isProtectedDataAvailable 值為 false,我們現在除了推遲從鑰匙串讀取資料之外,沒有其它好方法,因為它是系統原因導致,不值得進行進一步研究。

參考:解決 iOS 15 上 APP 莫名其妙地退出登入 - Swift社群

面試解析

整理編輯:Hello World

Dealloc 使用注意事項及解析

關於 Dealloc 的相關面試題以及應用, 週報裡已經有所提及。例如 三十八期:dealloc 在哪個執行緒執行四十二期:OOM 治理 FBAllocationTracker 實現原理,可以結合今天的使用注意事項一起學習。

避免在 dealloc 中使用屬性訪問

在很多資料中,都明確指出,應該儘量避免在 dealloc 中通過屬性訪問,而是用成員變數替代。

在初始化方法和 dealloc 方法中,總是應該直接通過例項變數來讀寫資料。- 《Effective Objective-C 2.0》第七條

Always use accessor methods. Except in initializer methods and dealloc. - WWDC 2012 Session 413 - Migrating to Modern Objective-C

The only places you shouldn’t use accessor methods to set an instance variable are in initializer methods and dealloc. - Practical Memory Management

除了可以提升訪問效率,也可以防止發生 crash。有文章介紹 crash 的原因是:析構過程中,類結構不再完整,當使用 accessor 時,實際是向當前例項傳送訊息,此時可能會存在 crash。

筆者對這裡也不是很理解,根據 debug 分析析構過程實際是優先呼叫了例項覆寫的 dealloc 後,才依次處理 superclass 的 dealloccxx_destructAssociatedWeak ReferenceSide Table等結構的,最後執行 free,所以不應該發生結構破壞導致的 crash,希望有了解的同學指教一下

筆者個人的理解是:Apple 做這種要求的原因是不想讓子類影響父類的構造和析構過程。例如以下程式碼,子類通過覆寫了 Associated方法, 會影響到父類的 dealloc 過程。

```objectivec @interface HWObject : NSObject @property(nonatomic) NSString* info; @end

@implementation HWObject - (void)dealloc { self.info = nil; } - (void)setInfo:(NSString *)info { if (info) { _info = info; NSLog(@"%@",[NSString stringWithString:info]); } } @end

@interface HWSubObject : HWObject @property (nonatomic) NSString* debugInfo; @end

@implementation HWSubObject - (void)setInfo:(NSString *)info { NSLog(@"%@",[NSString stringWithString:self.debugInfo]); } - (void)dealloc { _debugInfo = nil; } - (instancetype)init { if (self = [super init]) { _debugInfo = @"This is SubClass"; } return self; } @end ```

造成 crash 的原因是 HWSubObject:dealloc() 中釋放了變數 debugInfo,然後呼叫 HWObject:dealloc() ,該函式使用 Associated 設定 info ,由於子類覆寫了 setInfo,所以執行子類 setInfo。該函式內使用了已經被釋放的變數 debugInfo正如上面說的, 子類通過重寫 Associated,最終影響到了父類的析構過程。

dealloc 是什麼時候釋放變數的

其實在 dealloc 中無需開發處理成員變數, 當系統呼叫 dealloc時會自動呼叫解構函式(.cxx_destruct)釋放變數,參考原始碼呼叫鏈:[NSObject dealloc] => _objc_rootDealloc => rootDealloc => object_dispose => objc_destructInstance => object_cxxDestruct => object_cxxDestructFromClass

cpp static void object_cxxDestructFromClass(id obj, Class cls) { // 遍歷 self & superclass // SEL_cxx_destruct 是在 map_images 時在 Sel_init 中賦值的, 其實就是 .cxx_destruct 函式 dtor = (void(*)(id)) lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct); // 執行 (*dtor)(obj); } } }

沿著 superClass 鏈通過 lookupMethodInClassAndLoadCache去查詢 SEL_cxx_destruct函式,查詢到呼叫。SEL_cxx_destructobjc 在初始化呼叫 map_images 時,在 Sel_init 中賦值的,值就是 .cxx_destruct

cxx_destruct 就是用於釋放變數的,當類中新增了變數後,會自動插入該函式,這裡可以通過 LLDB watchpoint 監聽例項的屬性值變化, 然後檢視堆疊資訊驗證。

避免在 dealloc 中使用 __weak

objective-c - (void)dealloc { __weak typeof(self) weakSelf = self; }

當在 dealloc中使用了 __weak 後會直接 crash,報錯資訊為:Cannot form weak reference to instance (0x2813c4d90) of class xxx. It is possible that this object was over-released, or is in the process of deallocation. 報錯原因是 runtime 在儲存弱引用計數過程中判斷了當前物件是否正在析構中, 如果正在析構則丟擲異常

核心原始碼如下:

```cpp id weak_register_no_lock(weak_table_t weak_table, id referent_id, id referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions) { // ... 省略 if (deallocating) { if (deallocatingOptions == CrashIfDeallocating) { _objc_fatal("Cannot form weak reference to instance (%p) of " "class %s. It is possible that this object was " "over-released, or is in the process of deallocation.", (void*)referent, object_getClassName((id)referent)); } // ... 省略 }

```

避免在 dealloc 中使用 GCD

例如一個經常在子執行緒中使用的類,內部需要使用 NSTimer 定時器,定時器由於需要加到 NSRunloop 中,為了簡單,這裡加到了主執行緒, 而定時器有一個特殊性:定時器的釋放和建立必須在同一個執行緒,所以釋放也需要在主執行緒,示例程式碼如下(以上程式碼僅作為示例程式碼,並非實際開發使用):

```objectivec - (void)dealloc { [self invalidateTimer]; }

  • (void)fireTimer { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ if (!weakSelf.timer) { weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"TestDeallocModel timer:%p", timer); }]; [[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSRunLoopCommonModes]; } }); }

  • (void)invalidateTimer { dispatch_async(dispatch_get_main_queue(), ^{ // crash 位置 if (self.timer) { NSLog(@"TestDeallocModel invalidateTimer:%p model:%p", self->_timer, self); [self.timer invalidate]; self.timer = nil; } }); }

  • (vodi)main { dispatch_async(dispatch_get_global_queue(0, 0), ^{ HWSubObject *obj = [[HWSubObject alloc] init]; [obj fireTimer]; }); } ```

程式碼會在invalidateTimer::if (self.timer) 處發生 crash, 報錯為 EXC_BAD_ACCESS。原因很簡單,因為 dealloc最終會呼叫 free()釋放記憶體空間,而後 GCD再訪問到 self 時已經是野指標,所以報錯。

可以使用 performSelector代替 GCD實現, 確保執行緒操作先於 dealloc 完成。

總結:面試中對於記憶體管理和 dealloc 相關的考察應該不會很複雜,建議熟讀一次原始碼,瞭解 dealloc 的呼叫時機以及整個釋放流程,然後理解注意事項,基本可以一次性解決 dealloc 的相關面試題。

優秀部落格

整理編輯:皮拉夫大王在此

本期優秀部落格的主題為:ARM64 彙編入門及應用。彙編程式碼對於我們大多數開發者來說是既熟悉又陌生的領域,在日常開發過程中我們經常會遇到彙編,所以很熟悉。但是我們遇到彙編後,大多數人可能並不瞭解彙編程式碼做了什麼,也不知道能利用匯編程式碼解決什麼問題而常常選擇忽略,因此彙編程式碼又是陌生的。本期部落格我搜集了 3 套匯編系列教程,跟大家一道進入 ARM64 的彙編世界。

閱讀學習後我將獲得什麼?

完整閱讀三套學習教程後,我們可以閱讀一些邏輯簡單的彙編程式碼,更重要的是多了一種針對疑難 bug 的排查手段。

需要基礎嗎?

我對彙編掌握的並不多,在閱讀和學期過程期間發現那些需要思考和理解的內容,作者們都介紹的很好。

1、[C in ASM(ARM64)] -- 來自知乎:知兵

@皮拉夫大王:推薦先閱讀此係列文章。作者從語法角度解釋原始碼與彙編的關係,例如陣列相關的彙編程式碼是什麼樣子?結構體相關的彙編程式碼又是什麼樣子。閱讀後我們可以對棧有一定的理解,並且能夠閱讀不太複雜的彙編程式碼,並能結合指令集說明將一些人工原始碼翻譯成彙編程式碼。

2、iOS彙編入門教程 -- 來自掘金:Soulghost

@皮拉夫大王:頁師傅出品經典教程。相對前一系列文章來說,更多地從 iOS 開發者的角度去看到和應用匯編,例如如何利用匯編程式碼分析 NSClassFromString 的實現。文章整體的深度也有所加深,如果讀者有一定的彙編基礎,可以從該系列文章開始閱讀。

3、深入iOS系統底層系列文章目錄 -- 來自掘金:歐陽大哥2013

@皮拉夫大王:非常全面且深入的底層相關文章集合。有了前兩篇文章的鋪墊,可以閱讀該系列文章做下拓展。另外作者還在 深入iOS系統底層之crash解決方法 文章中一步步帶領我們利用匯編程式碼排查野指標問題。作為初學者我們可以快速感受到收益。

學習資料

整理編輯:Mimosa

程式設計師做飯指南

地址:http://github.com/Anduin2017/HowToCook

一個由社群驅動和維護的做飯指南。在這裡你可以學習到各色菜式是如何製作的,以及一些廚房的使用常識和知識。比較有意思的是,該倉庫裡的菜譜大都對製作過程中的細節和用量描述準確,比如菜譜中有 不允許使用不精準描述的詞彙,例如:適量、少量、中量、適當。 等非常嚴格準確的要求,對幾乎每個菜譜都做到了簡潔準確,非常有意思,也非常歡迎大家貢獻它~

工具推薦

整理編輯:CoderStar

EasyFind

地址:http://easyfind.en.softonic.com/mac

軟體狀態:免費

軟體介紹

小而強大的檔案搜尋應用,媲美 windows 下的 Everything

EasyFind

關於我們

iOS 摸魚週報,主要分享開發過程中遇到的經驗教訓、優質的部落格、高質量的學習資料、實用的開發工具等。週報倉庫在這裡:http://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的內容推薦可以通過 issue 的方式進行提交。另外也可以申請成為我們的常駐編輯,一起維護這份週報。另可關注公眾號:iOS成長之路,後臺點選進群交流,聯絡我們,獲取更多內容。

往期推薦

iOS摸魚週報 第四十三期

iOS摸魚週報 第四十二期

iOS摸魚週報 第四十一期

iOS摸魚週報 第四十期