1. 程式人生 > >iOS 如何自動移除KVO觀察者

iOS 如何自動移除KVO觀察者

來源:簡書 - changsanjiang

連結:http://www.jianshu.com/p/11bb1dcfb07e(點選尾部閱讀原文前往)

問題

我們都知道, 使用KVO模式, 對某個屬性進行監聽時, Observer 需要在必要的時刻進行移除, 否則 App 必然會 Crash. 這個問題有點煩人, 因為偶爾會忘記寫移除 Observer 的程式碼...

我一直想要這樣一個效果:

只管監聽, 並處理監聽方法. 不去分心, 管何時移除 Observer , 讓其能夠適時自動處理.

所幸, 它能夠實現, 先預覽一下:

@interface NSObject (SJObserverHelper)

- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface SJObserverHelper : NSObject

@property (nonatomic, unsafe_unretained) id target;

@property (nonatomic, unsafe_unretained) id observer;

@property (nonatomic, strong) NSString *keyPath;

@property (nonatomic, weak) SJObserverHelper *factor;

@end

@implementation SJObserverHelper

- (void)dealloc {

    if ( _factor ) {

        [_target removeObserver:_observer forKeyPath:_keyPath];

    }

}

@end

@implementation NSObject (ObserverHelper)

- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {

    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];

    SJObserverHelper *helper = [SJObserverHelper new];

    SJObserverHelper *sub = [SJObserverHelper new];

    sub.target = helper.target = self;

    sub.observer = helper.observer = observer;

    sub.keyPath = helper.keyPath = keyPath;

    helper.factor = sub;

    sub.factor = helper;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [observer hash]].UTF8String;

    objc_setAssociatedObject(self, helpeKey, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    objc_setAssociatedObject(observer, helpeKey, sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

@end

專案原始碼

下面來說說一步一步的實現吧:

初步思路實現:

我們都知道, 物件被釋放之前, 會呼叫dealloc方法, 其持有的例項變數也會被釋放.

我就這樣想, 在監聽註冊時, 為self和Observer關聯個臨時物件, 當兩者在釋放例項變數時, 我藉助這個時機, 在臨時物件的dealloc方法中, 移除Observer就行了.

想法很好, 可總不能每個類裡都加一個臨時物件的屬性吧. 那如何在不改變原有類的情況下, 為其關聯一個臨時物件呢?

關聯屬性

不改變原有類, 這時候肯定是要用Category了, 系統框架裡面有很多的分類, 並且有很多的關聯屬性, 如下圖 UIView 標頭檔案第180行:

0?wx_fmt=png

ex.png

依照上圖, 我們先看一個示例, 為NSObject的新增一個Category, 並添加了一個property, 在.m中實現了它的setter和getter方法.

#import <objc/message.h>

@interface NSObject (Associate)

@property (nonatomic, strong) id tmpObj;

@end

@implementation NSObject (Associate)

static const char *testKey = "TestKey";

- (void)setTmpObj:(id)tmpObj {

    // objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

    objc_setAssociatedObject(self, testKey, tmpObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (id)tmpObj {

    // objc_getAssociatedObject(id object, const void *key)

    return objc_getAssociatedObject(self, testKey);

}

@end

很明確, objc_setAssociatedObject 便是關聯屬性的setter方法, 而objc_getAssociatedObject便是關聯屬性的getter方法. 最需要關注的就是setter方法, 因為我們要用來新增關聯屬性物件.

初步思路探索

初步嘗試:

既然屬性可以隨時使用objc_setAssociatedObject關聯了, 那我就嘗試先為self關聯一個臨時物件, 在其dealloc中, 將Observer移除.

@interface SJObserverHelper : NSObject

@property (nonatomic, weak) id target;

@property (nonatomic, weak) id observer;

@property (nonatomic, strong) NSString *keyPath;

@end

@implementation SJObserverHelper

- (void)dealloc {

    [_target removeObserver:_observer forKeyPath:_keyPath];

}

@end

- (void)addObserver {

    NSString *keyPath = @"name";

    [_xiaoM addObserver:_observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];

    SJObserverHelper *helper_obj = [SJObserverHelper new];

    helper_obj.target = _xiaoM;

    helper_obj.observer = _observer;

    helper_obj.keyPath = keyPath;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;

    // 關聯

    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

於是, 美滋滋的運行了一下程式, 當將_xiaoM 置為 nil 時, 砰 App Crash......

reason: 'An instance 0x12cd1c370 of class Person was deallocated while key value observers were still registered with it.

分析: 臨時物件的dealloc, 確確實實的跑了. 為什麼會還有registered? 於是我嘗試在臨時物件的dealloc中, 列印例項變數target, 發現其為nil. 好吧, 這就是Crash問題原因!

嘗試 unsafe_unretained

通過上面操作, 我們知道self在被釋放之前, 會先釋放其持有的關聯屬性, self並未完全釋放, 可在臨時物件中target卻成了nil. 同時self還是有效的, 那如何保持不為nil呢?

我們看看OC中的兩個修飾符weak與unsafe_unretained:

  • weak: 持有者不會對目標進行retain, 當目標銷燬時, 持有者的例項變數會被置空

  • unsafe_unretained: 持有者不會對目標進行retain, 當目標釋放後, 持有者的例項變數還會依然指向之前的記憶體空間(野指標)

由上, unsafe_unretained很好的解決了我們的問題. 於是我做了如下修改:

@interface SJObserverHelper : NSObject

@property (nonatomic, unsafe_unretained) id target;

@property (nonatomic, unsafe_unretained) id observer;

@property (nonatomic, strong) NSString *keyPath;

@end

再次執行程式, 還行, 觀察者移除了.

最終實現

還存在的問題

目前, 我們只是實現了, 如何在self釋放的時候, 移除自己身上的Observer.

但如果Observer提前釋放了呢?

而新增關聯屬性, 兩者還不能同時持有臨時物件, 否則臨時物件也不會及時的釋放.

好吧, 既然一個不行, 那就各自關聯一個:

- (void)addObserver {

    ..... 

    SJObserverHelper *helper_obj = [SJObserverHelper new];

    SJObserverHelper *sub_obj = [SJObserverHelper new];

    sub_obj.target = helper_obj.target = _xiaoM;

    sub_obj.observer = helper_obj.observer = _observer;

    sub_obj.keyPath = helper_obj.keyPath = keyPath;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;

    // 關聯

    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 關聯

    objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

如上, 仔細想想, 存在一個很明顯的問題, 兩個關聯屬性釋放的同時, 進行了兩次觀察移除的操作. 為避免這個問題, 我又做了如下修改:

@interface SJObserverHelper : NSObject

@property (nonatomic, unsafe_unretained) id target;

@property (nonatomic, unsafe_unretained) id observer;

@property (nonatomic, strong) NSString *keyPath;

@property (nonatomic, weak) SJObserverHelper *factor;  // 1. 新增一個 weak 變數

@end

@implementation SJObserverHelper

- (void)dealloc {

    if ( _factor ) {

        [_target removeObserver:_observer forKeyPath:_keyPath];

    }

}

@end

- (void)addObserver {

    ..... 

    SJObserverHelper *helper_obj = [SJObserverHelper new];

    SJObserverHelper *sub_obj = [SJObserverHelper new];

    sub_obj.target = helper_obj.target = _xiaoM;

    sub_obj.observer = helper_obj.observer = _observer;

    sub_obj.keyPath = helper_obj.keyPath = keyPath;

    // 2. 互相 weak 引用

    helper_obj.factor = sub_obj;  

    sub_obj.factor = helper_obj;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;

    // 關聯

    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 關聯

    objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

在之前的操作中, 我們知道, weak 修飾的變數, 在目標釋放時,持有者的例項變數都會自動置為nil, 因此如上dealloc方法中, 我們只需要判斷weak引用的例項變數factor是否為空即可.

抽取

以上操作, 我們就可以解決偶爾忘記寫移除Observer的程式碼了. 現在只需要把實現抽取出來, 做成一個通用的工具方法:

我新建了一個NSObject的Category, 並添加了一個方法, 如下:

0?wx_fmt=png

ex1.png

然後將上述的實現進行了整合放到了.m中:

0?wx_fmt=png

ex2.png

到此, 以後只需要呼叫- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;這個方法即可, 移除就交給臨時變數自己搞定.

結語: 能夠看到這裡, 老鐵是真愛了, 可以幫小弟去點個Star.

Over...

0?wx_fmt=png

相關推薦

iOS 自動KVO觀察

nonatomic format 時機 value set observe fork tor @property 對NSObject寫一個分類: #import <Foundation/Foundation.h> @interface NSObject (F

iOS 如何自動KVO觀察

來源:簡書 - changsanjiang連結:http://www.jianshu.com/p

KVO 觀察身份 removeObserver:

移除觀察者身份 可以傳送一條指定觀察方物件和鍵路徑的 removeObserver:forKeyPath: 訊息至被觀察的物件來移除一個鍵-值觀察者(當我們達到目的時) [child removeObserver:self forKeyPath:@"key"];

iOS開發子檢視控制器

OC語法中沒有直接移除子檢視的方法,但是可以通過查詢子檢視來移除 for(UIView *view in [self.view subviews]) { [view removefromsuperview] } 來點直接的:實際上是用了NSArray的mak

一個可以監控U盤接入、自動拷貝檔案到U盤、自動U盤的小程式

一個可以監控U盤接入、自動拷貝檔案到U盤、自動移除U盤的小程式 1,支援自動拖拽檔案,並獲取檔案路徑。 支援檔案拖拽: DragAcceptFiles(hWnd, T

如何用Powershell指令碼實現自動離職使用者的所屬組並儲存日誌

最近工作收到一個需求,需要對離職人員的賬號清理所屬組並記錄下日誌,所以研究了下如何使用Powershell來實現此功能: #查詢出相應的OU下面的離職賬號$users = get-aduser -Filter * -SearchBase "OU=xxxx,DC=it581,DC=com" | foreach

如何用Powershell腳本實現自動離職用戶的所屬組並保存日誌

保存日誌 like -a sea hbase data tin action xxx 最近工作收到一個需求,需要對離職人員的賬號清理所屬組並記錄下日誌,所以研究了下如何使用Powershell來實現此功能: #查詢出相應的OU下面的離職賬號$users = get-adus

IOS UITableView cell上之前的檢視

UITableView reload使用時需要先移除cell上所有的檢視,再去載入新的UI到Cell上。 下面是移除cell上的UI方法: - (void)tableView:(UITableView*)tableView willDisplayCell:(UITabl

iOS開發所有子檢視

<span style="font-size:18px;"><span style="background-color: rgb(255, 255, 255);">iOS開發中

KVO的風險

ans 項目 targe lan 刪除 判斷 移除 nbsp 分享圖片 為之前項目添加一個功能用到了一個開源庫XMTextView,然後運行報錯提示: 顯示沒有註冊一個叫font的觀察者,所以閃退。但是我的UITextView沒有添加觀察者呀,怎麽會刪除呢? 原來是

一個KVO 實現WKWebView加載進度條的例子 (註意最後移觀察

svi anim red all ini 一個 lap eat its // // OpenWebViewController.m // Treasure // // Created by 藍藍色信子 on 16/7/29. // Copyright ? 2016

iOS-知識梳理(觀察模式-KVO、NSNotification的實現原理.KVC原理)

觀察者模式的定義:一個目標物件管理所有依賴於它的觀察者物件,並在它自身的狀態改變時主動通知觀察者物件。這個主動通知通常是通過呼叫各觀察者物件所提供的介面方法來實現的。觀察者模式較完美地將目標物件與觀察者物件解耦。 KVO基於runtime實現,當你觀察一個物件的時候,一個新類被動態建立繼承於

iOS-知識梳理(觀察模式-KVO、NSNotification的實現原理)

觀察者模式的定義:一個目標物件管理所有依賴於它的觀察者物件,並在它自身的狀態改變時主動通知觀察者物件。這個主動通知通常是通過呼叫各觀察者物件所提供的介面方法來實現的。觀察者模式較完美地將目標物件與觀察者物件解耦。 KVO基於runtime實現,當你觀察一個物件的時候,一個新

觀察模式實際應用:監聽線程,意外退出線程後自動重啟

lee text 實時 之間 最終 ren tap instance and 摘要:  觀察者模式,定義對象之間的一種一對多的依賴關系,當對象的狀態發生改變時,所有依賴於它的對象都得到通知並且被自動更新。觀察者模式在JDK中有現成的實現,java.util.Obsera

powershell 自動從組中禁用的賬號

所有 pre identity str com cat each 特定 confirm 1.特定組中移除禁用賬號 $group= "testgroup" $members = Get-ADGroupMember -Identity $group foreach ($mem

驗證碼自動填充項目中觀察模式分析

chan span esc 好處 lint ner 解析 驗證 一對一 觀察者模式 觀察者模式定義了對象之間的一對多依賴,當一個對象狀態發生改變時,其依賴者便會收到通知並做相應的更新。其原則是:為交互對象之間松耦合。以松耦合方式在一系列對象之間溝通狀態,我們

iOS 一行程式碼處理子檢視(、隱藏等)

無需遍歷迴圈,一行程式碼處理子檢視邏輯 移除所有子檢視: [view.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; 顯示、隱藏所有子檢視 //隱藏所有子檢視 [view.subviews

iOS swift cookie創建存儲

storage ssa 創建 form hive ios delet view on() 保存網絡請求的cookies,並存儲到UserDefaults中 //保存COOKIES static func saveCookies(for urlStr: Str

ios觀察模式

什麼是觀察者模式?我們先打個比方,這就像你訂報紙。比如你想知道美國最近發生了些什麼新聞。你可能會訂閱一份美國週刊 。然後美國一旦有了新的故事,美國週刊就發一刊,並郵寄給你。當你收到這份報刊,然後你就能夠了解美國最新的動態。其實這就是觀察者模式,A對B的變化感興趣,就註冊為B的觀察者,當B發生變化

iOS逆向之動態分析(騰訊視訊廣告)非會員

前言: iOS逆向分析之動態分析,我開始思考怎樣把原理講的深入淺出,怎樣把故事講的有趣生動,於是本來寫好的動態分析又操了重來,那麼今天我準備帶著問題來講動態分析,先丟擲我們這次逆向的目標,騰訊視訊廣告移除,以此為例講解動態分析。 首先我們進入視訊播放頁,點選最近的熱片《戰