郑州七彩网站建设公司怎么样,网站建设参数,国外专业做集装箱别墅网站,枣强县住房和城乡建设局网站来源#xff1a;伯乐在线 - Tsui YuenHong 链接#xff1a;http://ios.jobbole.com/90422/ 点击 → 申请加入伯乐在线专栏作者 新增实践部分#xff1a;偏方 Hook 进某些方法来添加功能 Category – 简介 Category#xff08;类别#xff09;是 Objective-C 2.0 添加的新特… 来源伯乐在线 - Tsui YuenHong 链接http://ios.jobbole.com/90422/ 点击 → 申请加入伯乐在线专栏作者 新增实践部分偏方 Hook 进某些方法来添加功能 Category – 简介 Category类别是 Objective-C 2.0 添加的新特性十年前的新特性 ?。其作用可以扩展已有的类 而不必通过子类化已有类甚至也不必知道已有类的源码还有就是分散代码使已有类的体积大大减少也利于分工合作。 在苹果开源项目中我们可以下载相关的源码来查看 category 的资料。 在 AFNetworking 和 SDWebImage 中也大量用到 category 来扩展已有类和分散代码。 关于 category 的定义可以在 objc-runtime-new.h 中找到。由其定义可以看出 category 可以正常实现功能有添加实例方法、类方法、协议、实例属性。( 在后面的实践中发现类属性也是可以添加的 ) struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta) { if (isMeta) return nil; // classProperties; else return instanceProperties; } }; 随便说一句本文并不主要注重 category 的实现细节和工作原理。关于细节的方面可以看相关文章 深入理解Objective-CCategory(上) 深入理解Objective-CCategory(下) 和 结合 category 工作原理分析 OC2.0 中的 runtime 。 Category – 能做什么 首先我们先来创建一个 Person 类以及 Person 类的 category可以看得出 category 的文件名就是 已有类名自定义名。 // Person.h interface Person : NSObject property (nonatomic, copy) NSString *name; (void)run; - (void)talk; end // Person.m implementation Person // 原实例方法 - (void)talk{ NSLog(\n我是原实例方法\n我是%,self.name); } // 原类方法 (void)run{ NSLog(\n我是原类方法\n我是跑得很快的的香港记者); } end // PersonOtherSkills.h interface Person (OtherSkills){ //⚠️ instance variables may not be placed in categories //int i; //NSString *str; } // 添加实例属性 property (nonatomic, copy) NSString *otherName; // 添加类属性 property (class, nonatomic, copy) NSString *clsStr; // 重写已有类方法 (void)run; - (void)talk; // 为已有类添加方法 - (void)logInstProp; (void)logClsProp; // PersonOtherSkills.m static NSString *_clsStr nil; static NSString *_otherName nil; implementation Person (OtherSkills) dynamic otherName; // 重写类方法 (void)run{ // 警告⚠️ Category is implementing a method which will also be implemented by its primary class NSLog(\n我是重写方法\n我是跑得很快的的香港记者); } // 重写实例方法 - (void)talk{ // 警告⚠️ Category is implementing a method which will also be implemented by its primary class NSLog(\n我是重写方法\n我是会谈笑风生的%,self.otherName); } // 输出实例属性 - (void)logInstProp{ NSLog(\n输出实例属性\n我是会谈笑风生的%,self.otherName); } // 输出类属性 (void)logClsProp{ NSLog(\n输出类属性\n我是会谈笑风生的%,self.clsStr); } (NSString *)clsStr{ return _clsStr; } (void)setClsStr:(NSString *)clsStr{ _clsStr clsStr; } - (NSString *)otherName{ return _otherName; } - (void)setOtherName:(NSString *)otherName{ _otherName otherName; } 创建完代码之后下面我们来看看 category 到底能干什么。 顺便一提我是在网上看到很多文章说 category 不能添加属性这是说法是不对的如 PersonOtherSkills.h 中就添加了一个 otherName 的属性。正确的说法应该是 category 不能添加实例变量否则编译器会报错 instance variables may not be placed in categories。正常情况下因为 category 不能添加实例变量也会导致属性的 setter getter 方法不能正常工作。 当然可以利用 Runtime 为 category 动态关联属性最后会介绍两种使 category 属性正常工作的方法 category 可以为已有类添加实例属性。 如 PersonOtherSkills.h 中就添加了一个 otherName 的属性。可以出来能正常工作。 // 运行代码 Person *p1 [[Person alloc] init]; // 实例属性 p1.otherName 小花; [p1 logInstProp]; p1.otherName 小明; [p1 logInstProp]; // 输出结果 2016-09-11 09:45:09.935 category[37281:1509791] 输出实例属性 我是会谈笑风生的小花 2016-09-11 09:45:09.936 category[37281:1509791] 输出实例属性 我是会谈笑风生的小明 category 可以为已有类添加类属性。 虽然category_t 中是没有定义 clssProperties但是根据实际操作却显示 category 的确可以为已有类添加类属性并且成功执行。 // 运行代码 Person.clsStr 小东; [Person logClsProp]; // 输出结果 2016-09-11 09:45:09.936 category[37281:1509791] 输出类属性 我是会谈笑风生的小东 category 可以为已有类添加实例方法和类方法。 在上面的两个例子中已经体现了 category 可以为已有类添加实例方法和类方法。这里将讨论加入 category 重写了已有类的方法会怎么样在创建的代码中我们已经重写了 run 和 talk 方法那这时我们来调用看看。 // 运行代码 // 调用类方法 [Person run]; // 调用实例方法 Person *p1 [[Person alloc] init]; [p1 talk]; // 输出结果 2016-09-11 11:22:05.817 category[37733:1562534] 我是重写方法 我是跑得很快的的香港记者 2016-09-11 11:22:05.817 category[37733:1562534] 我是重写方法 我是会谈笑风生的(null) 可以看得出来这时候无论是已有类中的类方法和实例方法都可以被 category 替换到其中的重写方法即使我现在是没有导入 PersonOtherSkills.h 。这就带来一个很严重的问题如果在 category 中不小心重写了已有类的方法将导致原方法无法正常执行。所以使用 category 添加方法时候请注意是否和已有类重名了正如 《 Effective Objective-C 2.0 》 中的第 25 条所建议的 在给第三方类添加 category 时添加方法时记得加上你的专有前缀 然而因为 category 重写方法是并不是替换掉原方法而是往已有类中继续添加方法所以还是有机会去调用到原方法。这里利用 class_copyMethodList 获取 Person 类的全部类方法和实例方法。 // 获取 Person 的方法列表 unsigned int personMCount; // 获取实例方法 //Method *personMList class_copyMethodList([Person class], personMCount); // 获取类方法 Method *personMList class_copyMethodList(object_getClass([Person class]), personMCount); NSMutableArray *mArr [NSMutableArray array]; // 这里是倒序获取所以 mArr 第一个方法对应的是 Person 类中最后一个方法 for (int i personMCount - 1; i 0; i--) { SEL sel NULL; IMP imp NULL; Method method personMList[i]; NSString *methodName [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSUTF8StringEncoding]; [mArr addObject:methodName]; if ([run isEqualToString:methodName]) { imp method_getImplementation(method); sel method_getName(method); ((void (*)(id, SEL))imp)(p1, sel); // 这里的 sel 有什么用呢 ?! //break; } } free(personMList); 其中输出的类方法和实例方法分别如下显示原方法的确可以被调用。 不过我这里有个疑问使用 imp 时第二个参数 sel 到底有什么用呢? 2016-09-11 11:52:44.795 category[37893:1582677] 我是原类方法 我是跑得很快的的香港记者 2016-09-11 11:52:44.796 category[37893:1582677] 我是重写方法 我是跑得很快的的香港记者 2016-09-11 11:52:44.796 category[37893:1582677] ( run, // 原方法 run, // 重写方法 setClsStr:, logClsProp, clsStr ) 2016-09-11 11:54:14.545 category[37927:1584029] 我是原实例方法 我是(null) 2016-09-11 11:54:14.545 category[37927:1584029] 我是重写方法 我是会谈笑风生的(null) 2016-09-11 11:54:14.545 category[37927:1584029] ( setName:, name, .cxx_destruct, setOtherName:, logInstProp, tanxiaofengsheng, otherName, talk, //原方法 talk //重写方法 category 可以为已有类添加协议。 这里先添加一个新的 category负责处理他谈笑风生的行为和写个协议让他上电视。 // PersonDelegate.h #import Person.h // 添加协议 protocol PersonDelegate - (void)showInTV; end interface Person (Delegate) // 添加 delegate property (nonatomic, weak) id delegate; - (void)tanxiaofengsheng; end // PersonDelegate.m #import PersonDelegate.h #import implementation Person (Delegate) - (id)delegate{ return objc_getAssociatedObject(self, selector(delegate)); } - (void)setDelegate:(id)delegate{ objc_setAssociatedObject(self, selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN); } - (void)tanxiaofengsheng{ for (int i 0 ; i 在相应的代理里面添加 showInTV 的方法 // 运行代码 Person *p1 [[Person alloc] init]; p1.delegate self; // 开始谈笑风生了 [p1 tanxiaofengsheng]; // ShowInTV 方法的实现 - (void)showInTV{ UIImageView *imageView [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)]; imageView.image [UIImage imageNamed:naive.jpg]; [self.view addSubview:imageView]; } 这样就利用 category 为已有类添加了协议。 关于 category 的基本应用就介绍到这里了。下面就来分享一下 category 的实践中的使用。 Category – 实践 偏方Hook 进某些方法来添加功能 一般来说为原方法添加功能都是利用 Runtime 来 Method Swizzling。不过这里也有个奇淫技巧来实现同样的功能例如我要在所有 VC 的 - (void)viewDidLoad 里面打印一个句话就可以用 category 重写已有类的方法因为 category 重写方法不是通过替换原方法来实现的而是在原方法列表又增添一个新的同名方法这就创造了机会给我们重新调用原方法了。 // 待 Hook 类 // ViewController.m // 待替换方法 无参 - (void)viewDidLoad { [super viewDidLoad]; [self testForHook:Hello World]; NSLog(执行原方法); } // 待替换方法 有参 - (void)testForHook:(NSString *)str1{ NSLog(%,str1); } // category 实现方法 // ViewControllerHookOriginMethod.m // category 重写原方法 - (void)viewDidLoad { NSLog(HOOK SUCCESS! \n--%-- DidLoad !,[self class]); IMP imp [self getOriginMethod:viewDidLoad]; ((void (*)(id, SEL))imp)(self, selector(viewDidLoad)); } // category 重写原方法 - (void)testForHook:(NSString *)str1{ NSLog(HOOK SUCCESS \n--%s-- 执行,_cmd); IMP imp [self getOriginMethod:testForHook:]; ((void (*)(id, SEL, ...))imp)(self, selector(testForHook:), str1); } // 获取原方法的 IMP - (IMP)getOriginMethod:(NSString *)originMethod{ // 获取 Person 的方法列表 unsigned int methodCount; // 获取实例方法 Method *VCMethodList class_copyMethodList([self class], methodCount); IMP imp NULL; // 这里是倒序获取所以 mArr 第一个方法对应的是 Person 类中最后一个方法 for (int i methodCount - 1; i 0; i--) { Method method VCMethodList[i]; NSString *methodName [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSUTF8StringEncoding]; if ([originMethod isEqualToString:methodName]) { imp method_getImplementation(method); break; } } free(VCMethodList); return imp; } // 执行代码 // ViewController.m - (void)viewDidLoad { [super viewDidLoad]; [self testForHook:Hello World]; NSLog(执行原方法); } // 输出结果 2016-09-12 23:00:15.887 category[63655:2375379] HOOK SUCCESS! --ViewController-- DidLoad ! 2016-09-12 23:00:15.888 category[63655:2375379] HOOK SUCCESS --testForHook:-- 执行 2016-09-12 23:00:15.889 category[63655:2375379] Hello World 2016-09-12 23:00:15.889 category[63655:2375379] 执行原方法 查看输出结果可以看得出来我们的 Hook 掉 viewDidLoad 来实现打印成功了。 UIButton 实现点击事件可以“传参”。 一般创建UIButton的时候都会使用 addTarget ...这个方法来为button添加点击事件不过这个方法有个不好的地方就是无法传自己想要的参数。例如下面代码中声明了str我的意图是点击button就使控制台或者屏幕显示str的内容。如果按照这样来写的我想到的解决办法就是将str设置为属性或者成员变量不过这样都是比较麻烦而且不直观的代码分散。 NSString *str hi; UIButton *button [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 150, 100)]; button.backgroundColor [UIColor redColor]; [button addTarget:self action:selector(click:) forControlEvents:UIControlEventTouchDown]; [self.view addSubview:button]; // 点击事件 - (void)click:(UIButton *)button{ ... } 我想到较好的解决办法应该在创建button就为它设置具体的点击响应事件。实现方法就是为 UIButton 添加 block 属性或者添加可传入 block 的方法。具体代码如下 // UIButtonCategory.h #import typedef void(^ActionHandlerBlock)(void); interface UIButton (Category) // 点击响应的 block property (nonatomic, copy) ActionHandlerBlock actionHandlerBlock; // 设置 UIButton 的点击事件 - (void)kk_addActionHandler: (ActionHandlerBlock )actionHandlerBlock ForControlEvents:(UIControlEvents )controlEvents; end // UIButtonCategory.m #import UIButtonCategory.h #import static const void *kk_actionHandlerBlock kk_actionHandlerBlock; implementation UIButton (Category) - (void)kk_addActionHandler:(ActionHandlerBlock)actionHandler ForControlEvents:(UIControlEvents)controlEvents{ // 关联 actionHandler objc_setAssociatedObject(self, kk_actionHandlerBlock, actionHandler, OBJC_ASSOCIATION_COPY_NONATOMIC); // 设置点击事件 [self addTarget:self action:selector(handleAction) forControlEvents:controlEvents]; } // 处理点击事件 - (void)handleAction{ ActionHandlerBlock actionHandlerBlock objc_getAssociatedObject(self, kk_actionHandlerBlock); if (actionHandlerBlock) { actionHandlerBlock(); } } - (ActionHandlerBlock)actionHandlerBlock{ return objc_getAssociatedObject(self, selector(actionHandlerBlock)); } - (void)setActionHandlerBlock:(ActionHandlerBlock)actionHandlerBlock{ objc_setAssociatedObject(self, selector(actionHandlerBlock), actionHandlerBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } end 那现在我们来看看调用的结果例如我现在想要的点击事件是 button 颜色随机变换。 UIButton *button [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 150, 100)]; button.backgroundColor [UIColor redColor]; [self.view addSubview:button]; // 1. 通过实例方法传入 block 来修改 UIButton *button2 [[UIButton alloc] initWithFrame:CGRectMake(100, 400, 150, 100)]; button2.backgroundColor [UIColor redColor]; [button2 kk_addActionHandler:^{ button.backgroundColor [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0]; } ForControlEvents:UIControlEventTouchDown]; [self.view addSubview:button2]; // 2. 通过修改 block 属性来修改 UIButton *button3 [[UIButton alloc] initWithFrame:CGRectMake(100, 550, 150, 100)]; button3.backgroundColor [UIColor redColor]; button3.actionHandlerBlock ^{ button.backgroundColor [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0]; }; [button3 addTarget:self action:selector(click:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button3]; // 响应事件 - (void)click:(UIButton *)button{ if (button.actionHandlerBlock) { button.actionHandlerBlock(); } } 显然方法1和方法2在这个例子中实现的效果是相同的。不过在不同场合这两个方法适用的范围也不同。 直接调用实例方法传入 block 会使代码更加简洁和集中但不适合 block 需要传值的情景。 相反设置 block 属性要在 selector() 中的方法中调用 block比较麻烦不过在需要的情况下可以传入合适的参数。 p.s. 以后会继续补充实践部分。 最后说一下两种使 category 属性正常工作的方法 因为 category 不能创建实例变量那就直接使用静态变量如最开始为 ohterName 和clsStr 属性设置 setter getter的做法。 使用objc_setAssociatedObject其中 key 的选择有以下几种个人比较喜欢第四种。 static char *key1; // SDWebImage AFNetworking 中的做法比较简单而且 key1 肯定唯一。key 取 key1 static const char * const key2 key2; // 网上看到的做法指针不可变指向内容不可变但是这种情况必须在赋值确保 key2 指向内容的值是唯一。key 取 key2。 static const void *key3 key3; // 最取巧的方法指向自己是为了不创建额外空间而 const 修饰可以确保无法修改 key3 指向的内容。key 取 key3。 key 取 selector(属性名)最方便输入有提示只要你确保属性名添加上合适的前缀就不会出问题。