iOS 面向切面編程的實現(xiàn)與實戰(zhàn)案例

一、簡介

一良价、所謂的 aop 編程(面向切面編程)寝殴,其原理也就是在不更改正常的業(yè)務(wù)處理流程的前提下,通過生成一個動態(tài)代理類明垢,從而實現(xiàn)對目標(biāo)對象嵌入附加的操作蚣常。在 iOS 中,要想實現(xiàn)相似的效果也很簡單痊银,利用 OC 的動態(tài)性抵蚊,通過 Method Swizzling 改變目標(biāo)函數(shù)的 selector 所指向的實現(xiàn),然后在新的實現(xiàn)中實現(xiàn)附加的操作溯革,完成之后再回到原來的處理邏輯贞绳。
二、在一個類沒有實現(xiàn)源碼的情況下致稀,如果你要改變一個類的實現(xiàn)方法冈闭,你可以選擇重繼承該類,然后重寫方法抖单,或者使用Category類別名暴力搶先的方式萎攒。但是這兩種方式,都需要我們在使用的時候改變我們的編程方式矛绘,或者繼承該類耍休,或者需要引入Category。下面推出的一種方式货矮,不需要我們修改我們編寫邏輯的代碼羊精,就能實現(xiàn)函數(shù)的Hook功能,那就是RunTime中的Method Swizzling—交換方法的實現(xiàn)囚玫。

二喧锦、實現(xiàn)原理

在Object-C中每一個Method都是由一個SEL(方法名的散列值)和一個方法實現(xiàn)的指針(IMP)組成,他們在類實例化得過程中劫灶,SEL和IMP一一對應(yīng)組成我們需要的完整的Method裸违。

struct method_t {
    SEL name;//方法名的散列值
    const char *types;//方法的描述
    IMP imp;//方法真實實現(xiàn)的指針
};

如果我們不做任何處理,SEL和IMP都是一一對應(yīng)的本昏。

如果我們使用Method Swizzling交換Method2和Method3的實現(xiàn)的時候供汛,我們只需要在運行時把IMP2和IMP3的指向地址做個交換就可以了。其實我們調(diào)用的就是RunTime中的

 */
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
     __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

進(jìn)入它的源碼涌穆,可以查看它就是按照以上思路把方法指針做了交換怔昨,來做到在運行時把方法進(jìn)行交換。

下面就是它實現(xiàn)的關(guān)鍵源碼宿稀。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    if (ignoreSelector(m1->name)  ||  ignoreSelector(m2->name)) {
        // Ignored methods stay ignored. Now they're both ignored.
        m1->imp = (IMP)&_objc_ignored_method;
        m2->imp = (IMP)&_objc_ignored_method;
        return;
    }

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

方法就換之后趁舀,SEL和IMP的對應(yīng)關(guān)系就如下所示了。

三祝沸、核心代碼

void methodExchange(const char *className, const char *originalMethodName, const char *replacementMethodName, IMP imp) {
    Class cls = objc_getClass(className);//得到指定類的類定義
    SEL oriSEL = sel_getUid(originalMethodName);//把originalMethodName注冊到RunTime系統(tǒng)中
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);//獲取實例方法
    struct objc_method_description *desc = method_getDescription(oriMethod);//獲得指定方法的描述
    if (desc->types) {
        SEL buSel = sel_registerName(replacementMethodName);//把replacementMethodName注冊到RunTime系統(tǒng)中
        
        if (class_addMethod(cls, buSel, imp, desc->types)) {//通過運行時矮烹,把方法動態(tài)添加到類中
            Method buMethod  = class_getInstanceMethod(cls, buSel);//獲取實例方法
            method_exchangeImplementations(oriMethod, buMethod);//交換方法
        }
    }
}

第一個參數(shù)為:需要交換方法的類的名稱越庇。

第二個參數(shù)為:原始方法名。

第三個參數(shù)為:交換方法名奉狈。

第四個參數(shù)為:交換方法的方法指針卤唉。

具體每一段代碼已經(jīng)在主時鐘說明的非常清楚了,就不多講了仁期。下面進(jìn)入實戰(zhàn)環(huán)節(jié)桑驱。

四、頁面埋點的實現(xiàn)

如果我們要實現(xiàn)頁面埋點的話跛蛋,我們就需要在-(void)viewWillAppear:(BOOL)animated熬的;方法中寫入我們的埋點代碼,這樣其實是非常不優(yōu)雅的赊级,需要我們在每個ViewController中的-(void)viewWillAppear:(BOOL)animated押框;都需要加入類似的埋點代碼。這個時候我們就可以使用Method Swizzling來HOOK住-(void)viewWillAppear:(BOOL)animated理逊;方法來進(jìn)行修改强戴。

我們新建一個UIViewController的分類,在其中進(jìn)行方法的交換 , 關(guān)鍵代碼如下:挡鞍。

@implementation UIViewController (Track)

+ (void)load{//+load會在類初始加載時調(diào)用
    //替換viewWillAppear:方法
    methodExchange("UIViewController", "viewWillAppear:", "hook_viewWillAppear:", (IMP)imp_processViewWillAppear);
}

//實現(xiàn)新的方法
static void imp_processViewWillAppear(id self, SEL cmd, BOOL animated){
    
    //先執(zhí)行原來的方法
    SEL oriSel = sel_getUid("hook_viewWillAppear:");
    void (*hook_viewWillAppear)(id, SEL, BOOL) = (void (*)(id,SEL,BOOL))[UIViewController instanceMethodForSelector:oriSel];//函數(shù)指針
    hook_viewWillAppear(self,cmd,animated);
    
    //添加埋點
    NSLog(@"進(jìn)入: %@", NSStringFromClass([self class]));
}

@end

這樣我們需要修改我們原來的代碼邏輯 就可以實現(xiàn)簡單的埋點功能了骑歹。效果如下:

五、網(wǎng)絡(luò)圖片信息監(jiān)控工具

在我們?nèi)粘i_發(fā)中墨微,網(wǎng)絡(luò)圖片下載顯示工具使用SDWebImage這個開源項目比較多道媚。查看他的源碼發(fā)現(xiàn)它的核心處理代碼其實是下面這段函數(shù)。

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock

我們調(diào)用一下方法就能實現(xiàn)網(wǎng)絡(luò)圖片的正常顯示翘县,但是我們還不能自動加上圖片的基本信息到圖片中顯示最域。

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 150, 150, 300)];
    [imageView sd_setImageWithURL:[NSURL URLWithString:@"http://c.hiphotos.baidu.com/image/pic/item/8d5494eef01f3a296716a9b49a25bc315d607ce9.jpg"]];
    [self.view addSubview:imageView];

這時我們就需要Hook住該方法,修改它的實現(xiàn)锈麸。

#define originalMethod_setImageWithURL "sd_setImageWithURL:placeholderImage:options:progress:completed:"
#define replacementMethod_setImageWithURL "hook_sd_setImageWithURL:placeholderImage:options:progress:completed:"

+ (void)load;
{
    NSLog(@"開啟圖片監(jiān)控");
    methodExchange("UIImageView", originalMethod_setImageWithURL, replacementMethod_setImageWithURL, (IMP)imp_processSetImageWithURL);
}

/**
 *  replacementMethod_processNeoHttpTaskFinish方法的實現(xiàn)
 */

static void imp_processSetImageWithURL(id self, SEL cmd, NSURL *url, UIImage *placeholder,
                                      SDWebImageOptions options, SDWebImageDownloaderProgressBlock progressBlock,  SDWebImageCompletionBlock completedBlock) {
    //  Run original
    SEL oriSel = sel_getUid(replacementMethod_setImageWithURL);
    
    BOOL (*setImageWithURLMethod)(id, SEL, NSURL *, UIImage *, SDWebImageOptions, SDWebImageDownloaderProgressBlock,  SDWebImageCompletionBlock) =
    (BOOL (*)(id, SEL, NSURL *, UIImage *, SDWebImageOptions, SDWebImageDownloaderProgressBlock,  SDWebImageCompletionBlock))[UIImageView instanceMethodForSelector : oriSel];
    
    if (Open_Monitor) {
        NSTimeInterval startTime = CFAbsoluteTimeGetCurrent();
        
        BOOL imageIsExsit = NO;
        if ([[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:[[SDWebImageManager sharedManager] cacheKeyForURL:url]]) {
            imageIsExsit = YES;
        }
        __weak typeof(self) weafSelf = self;
        SDWebImageCompletionBlock replaceCompletedBlock = ^(UIImage *image, NSError *error, SDImageCacheType cacheType,NSURL *imageURL) {
            NSTimeInterval endTime = CFAbsoluteTimeGetCurrent();
            
            NSData *data = UIImageJPEGRepresentation(image, 1.0);
            if (!data && data.length <= 0) {
                data = UIImagePNGRepresentation(image);
            }
            
            NSString *string = [NSString stringWithFormat:@"url:  %@ \nsize:  %.2fX%.2f(px) \ndownTime:  %fs \ndownSize:  %luK", [url absoluteString], image.size.width, image.size.height, endTime - startTime, [data length] / 1024];
            
            
             ((UIImageView *)weafSelf).image = [ImageMonitorService drawText:string inImage:((UIImageView *)weafSelf).image atPoint:CGPointZero];

            if (completedBlock) {
                completedBlock(((UIImageView *)weafSelf).image, error, cacheType,imageURL);
            }
        };
        setImageWithURLMethod(self,  cmd, url, placeholder, options, progressBlock,  replaceCompletedBlock);
    }
    else {
        setImageWithURLMethod(self,  cmd, url, placeholder, options, progressBlock,  completedBlock);
    }
}

實現(xiàn)效果如下:

六镀脂、注意點

你要確保Method Swizzling的交換代碼在APP的運行周期中只被調(diào)用一次。大部分情況都是在+(void)load方法中被調(diào)用忘伞”〕幔或者在APPDelegate中的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中被調(diào)用。

七氓奈、聯(lián)系方式

新浪微博
github
簡書首頁

歡迎加好友翘魄、一起交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舀奶,一起剝皮案震驚了整個濱河市暑竟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌育勺,老刑警劉巖但荤,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罗岖,死亡現(xiàn)場離奇詭異,居然都是意外死亡腹躁,警方通過查閱死者的電腦和手機(jī)呀闻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來潜慎,“玉大人,你說我怎么就攤上這事蓖康☆盱牛” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵蒜焊,是天一觀的道長倒信。 經(jīng)常有香客問我,道長泳梆,這世上最難降的妖魔是什么鳖悠? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮优妙,結(jié)果婚禮上乘综,老公的妹妹穿的比我還像新娘。我一直安慰自己套硼,他們只是感情好卡辰,可當(dāng)我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邪意,像睡著了一般九妈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上雾鬼,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天萌朱,我揣著相機(jī)與錄音,去河邊找鬼策菜。 笑死晶疼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的又憨。 我是一名探鬼主播冒晰,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼竟块!你這毒婦竟也來了壶运?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤浪秘,失蹤者是張志新(化名)和其女友劉穎蒋情,沒想到半個月后埠况,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡棵癣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年辕翰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狈谊。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡喜命,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出河劝,到底是詐尸還是另有隱情壁榕,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布赎瞎,位于F島的核電站牌里,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏务甥。R本人自食惡果不足惜牡辽,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望敞临。 院中可真熱鬧态辛,春花似錦、人聲如沸挺尿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽票髓。三九已至攀涵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洽沟,已是汗流浹背以故。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留裆操,地道東北人怒详。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像踪区,于是被迫代替她去往敵國和親昆烁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容