iOS 面向切面程式設計的實現與實戰案例
一、簡介
一、所謂的 aop 程式設計(面向切面程式設計),其原理也就是在不更改正常的業務處理流程的前提下,通過生成一個動態代理類,從而實現對目標物件嵌入附加的操作。在 iOS 中,要想實現相似的效果也很簡單,利用 OC 的動態性,通過 Method Swizzling 改變目標函式的 selector 所指向的實現,然後在新的實現中實現附加的操作,完成之後再回到原來的處理邏輯。
二、在一個類沒有實現原始碼的情況下,如果你要改變一個類的實現方法,你可以選擇重繼承該類,然後重寫方法,或者使用Category類別名暴力搶先的方式。但是這兩種方式,都需要我們在使用的時候改變我們的程式設計方式,或者繼承該類,或者需要引入Category。下面推出的一種方式,不需要我們修改我們編寫邏輯的程式碼,就能實現函式的Hook功能,那就是RunTime中的Method Swizzling—交換方法的實現。
二、實現原理
在Object-C中每一個Method都是由一個SEL(方法名的雜湊值)和一個方法實現的指標(IMP)組成,他們在類例項化得過程中,SEL和IMP一一對應組成我們需要的完整的Method。
struct method_t {
SEL name;//方法名的雜湊值
const char *types;//方法的描述
IMP imp;//方法真實實現的指標
};
如果我們不做任何處理,SEL和IMP都是一一對應的。
如果我們使用Method Swizzling交換Method2和Method3的實現的時候,我們只需要在執行時把IMP2和IMP3的指向地址做個交換就可以了。其實我們呼叫的就是RunTime中的
*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
進入它的原始碼,可以檢視它就是按照以上思路把方法指標做了交換,來做到在執行時把方法進行交換。
下面就是它實現的關鍵原始碼。
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的對應關係就如下所示了。
三、核心程式碼
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系統中
Method oriMethod = class_getInstanceMethod(cls, oriSEL);//獲取例項方法
struct objc_method_description *desc = method_getDescription(oriMethod);//獲得指定方法的描述
if (desc->types) {
SEL buSel = sel_registerName(replacementMethodName);//把replacementMethodName註冊到RunTime系統中
if (class_addMethod(cls, buSel, imp, desc->types)) {//通過執行時,把方法動態新增到類中
Method buMethod = class_getInstanceMethod(cls, buSel);//獲取例項方法
method_exchangeImplementations(oriMethod, buMethod);//交換方法
}
}
}
第一個引數為:需要交換方法的類的名稱。
第二個引數為:原始方法名。
第三個引數為:交換方法名。
第四個引數為:交換方法的方法指標。
具體每一段程式碼已經在主時鐘說明的非常清楚了,就不多講了。下面進入實戰環節。
四、頁面埋點的實現
如果我們要實現頁面埋點的話,我們就需要在-(void)viewWillAppear:(BOOL)animated;方法中寫入我們的埋點程式碼,這樣其實是非常不優雅的,需要我們在每個ViewController中的-(void)viewWillAppear:(BOOL)animated;都需要加入類似的埋點程式碼。這個時候我們就可以使用Method Swizzling來HOOK住-(void)viewWillAppear:(BOOL)animated;方法來進行修改。
我們新建一個UIViewController的分類,在其中進行方法的交換 , 關鍵程式碼如下:。
@implementation UIViewController (Track)
+ (void)load{//+load會在類初始載入時呼叫
//替換viewWillAppear:方法
methodExchange("UIViewController", "viewWillAppear:", "hook_viewWillAppear:", (IMP)imp_processViewWillAppear);
}
//實現新的方法
static void imp_processViewWillAppear(id self, SEL cmd, BOOL animated){
//先執行原來的方法
SEL oriSel = sel_getUid("hook_viewWillAppear:");
void (*hook_viewWillAppear)(id, SEL, BOOL) = (void (*)(id,SEL,BOOL))[UIViewController instanceMethodForSelector:oriSel];//函式指標
hook_viewWillAppear(self,cmd,animated);
//新增埋點
NSLog(@"進入: %@", NSStringFromClass([self class]));
}
@end
這樣我們需要修改我們原來的程式碼邏輯 就可以實現簡單的埋點功能了。效果如下:
五、網路圖片資訊監控工具
在我們日常開發中,網路圖片下載顯示工具使用SDWebImage這個開源專案比較多。檢視他的原始碼發現它的核心處理程式碼其實是下面這段函式。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
我們呼叫一下方法就能實現網路圖片的正常顯示,但是我們還不能自動加上圖片的基本資訊到圖片中顯示。
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住該方法,修改它的實現。
#define originalMethod_setImageWithURL "sd_setImageWithURL:placeholderImage:options:progress:completed:"
#define replacementMethod_setImageWithURL "hook_sd_setImageWithURL:placeholderImage:options:progress:completed:"
+ (void)load;
{
NSLog(@"開啟圖片監控");
methodExchange("UIImageView", originalMethod_setImageWithURL, replacementMethod_setImageWithURL, (IMP)imp_processSetImageWithURL);
}
/**
* replacementMethod_processNeoHttpTaskFinish方法的實現
*/
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);
}
}
實現效果如下:
六、注意點
你要確保Method Swizzling的交換程式碼在APP的執行週期中只被呼叫一次。大部分情況都是在+(void)load方法中被呼叫。或者在APPDelegate中的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中被呼叫。
七、聯絡方式
歡迎加好友、一起交流。
相關文章
- aop面向切面程式設計的實現程式設計
- Spring Boot實戰系列(3)AOP面向切面程式設計Spring Boot程式設計
- JS實現AOP 面向切面程式設計 (裝飾者模式)JS程式設計模式
- .NET Core 實現動態代理做AOP(面向切面程式設計)程式設計
- Spring AOP:面向切面程式設計的核心概念與實際應用Spring程式設計
- AOP 面向切面程式設計程式設計
- AOP(面向切面程式設計)程式設計
- 面向切面程式設計AOP程式設計
- 程式設計思想 面向切面程式設計程式設計
- 面向切面程式設計AspectJ在Android埋點的實踐程式設計Android
- Java 面向切面程式設計AOPJava程式設計
- AOP--面向切面程式設計程式設計
- Python裝飾器與面向切面程式設計Python程式設計
- Java中的面向切面程式設計(AOP)Java程式設計
- 基於SpringBoot AOP面向切面程式設計實現Redis分散式鎖Spring Boot程式設計Redis分散式
- 【spring原始碼學習】spring的AOP面向切面程式設計的實現解析Spring原始碼程式設計
- 設計模式之面向切面程式設計AOP設計模式程式設計
- Android AOP面向切面設計程式設計Android程式設計
- 前端js面向切面程式設計(AOP)前端JS程式設計
- React Native面向切面程式設計React Native程式設計
- Android面向切面程式設計(AOP)Android程式設計
- iOS開發 面向切面程式設計之 Aspects 原始碼解析iOS程式設計原始碼
- 面向切面程式設計之瘋狂的 Aspects程式設計
- 四、Spring-面向切面程式設計Spring程式設計
- 前端解讀面向切面程式設計(AOP)前端程式設計
- Spring之AOP面向切面程式設計Spring程式設計
- 面向切面程式設計和依賴注入程式設計依賴注入
- 好程式設計師web前端分享js實現實戰案例程式設計師Web前端JS
- Autofac實現攔截器和切面程式設計程式設計
- 在Javascript中進行面向切面程式設計JavaScript程式設計
- 01.AOP(AspectOrientatedProgramming面向切面程式設計)程式設計
- 面向橫切面程式設計(Aspect Oriented Programming)。程式設計
- Spring AOP——Spring 中面向切面程式設計Spring程式設計
- Spring AOP(面向切面程式設計)是什麼?Spring程式設計
- 面向切面程式設計 ( Aspect Oriented Programming with Spring )程式設計Spring
- Spring理論基礎-面向切面程式設計Spring程式設計
- 手寫Spring---AOP面向切面程式設計(4)Spring程式設計
- 手寫Spring---AOP面向切面程式設計(3)Spring程式設計