App开发流程之使用分类(Category)和忽略编译警告(Warning)

时间:2023-05-26 11:39:02

Category使得开发过程中,减少了继承的使用,避免子类层级的膨胀。合理使用,可以在不侵入原类代码的基础上,写出漂亮的扩展内容。我更习惯称之为“分类”。

Category和Extension类似,都是对原类的扩展,区别是前者需要提供Category的名称,并且不直接支持属性;后者为匿名,多存在于类的实现文件,观感上实现属性、变量、方法的私有效果。

主要记录分类使用过程中常涉及的内容:

1.关联对象的使用

分类虽然不直接支持属性,但是可以利用关联对象的方法,达到属性的正常使用效果。

添加常用的刷新类库MJRefresh:https://github.com/CoderMJLee/MJRefresh

为了避免原代码被侵入,采用了分类方案,给UIScrollView添加新的属性和方法。新建了一个分类UIScrollView+RefreshControl,在.h文件中声明了几个属性:

/**
* 头部刷新控件,可以自行设置hidden属性
*/
@property (nonatomic, strong, readonly) UIView *refreshHeader; /**
* 底部刷新控件,可以自行设置hidden属性
*/
@property (nonatomic, strong, readonly) UIView *refreshFooter; /**
* 分页数据中,请求的当前页数,考虑到网络请求失败,请自行管理;添加刷新后,默认为1
*/
@property (nonatomic, assign ) NSUInteger refreshPageNum; /**
* 分页数据中,每页请求的数量;添加刷新后,默认为10
*/
@property (nonatomic, assign ) NSUInteger refreshCountPerPage;

在.m文件中关联属性相关对象:

- (UIView *)refreshHeader
{
return objc_getAssociatedObject(self, _cmd);
} - (UIView *)refreshFooter
{
return objc_getAssociatedObject(self, _cmd);
} - (NSUInteger)refreshPageNum
{
NSUInteger pageNum = [objc_getAssociatedObject(self, _cmd) integerValue]; return pageNum;
} - (void)setRefreshPageNum:(NSUInteger)refreshPageNum
{
objc_setAssociatedObject(self, @selector(refreshPageNum), @(refreshPageNum), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} - (NSUInteger)refreshCountPerPage
{
NSUInteger countPerPage = [objc_getAssociatedObject(self, _cmd) integerValue]; return countPerPage;
} - (void)setRefreshCountPerPage:(NSUInteger)refreshCountPerPage
{
objc_setAssociatedObject(self, @selector(refreshCountPerPage), @(refreshCountPerPage), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

objc_getAssociatedObject和objc_setAssociatedObject方法分别用于获取和保存关联的对象。_cmd与@selector([方法名])作用类似,都是获取到SEL,不过_cmd表示当前方法的SEL。

因为是关联对象,所以即便是保存int类型,也需要转换为NSNumber对象,并设置为强引用类型。

2.使用method_exchangeImplementations方法,也就是常说的swizzle技术

添加常用的图片加载类库SDWebImage:https://github.com/rs/SDWebImage

但是需要修改缓存图片的路径,缓存路径相关方法在SDImageCache中可以查看。只需要在其init时候,修改名为memCache和diskCachePath的属性。新建了一个分类SDImageCache+CacheHelper.h,然后在实现文件中添加如下代码:

+ (void)load
{
__weak typeof(self) weakSelf = self; static dispatch_once_t once;
dispatch_once(&once, ^{
[weakSelf swizzleOriginalSelector:@selector(init) withNewSelector:@selector(base_init)];
});
} + (void)swizzleOriginalSelector:(SEL)originalSelector withNewSelector:(SEL)newSelector
{
Class selfClass = [self class]; Method originalMethod = class_getInstanceMethod(selfClass, originalSelector);
Method newMethod = class_getInstanceMethod(selfClass, newSelector); IMP originalIMP = method_getImplementation(originalMethod);
IMP newIMP = method_getImplementation(newMethod); //先用新的IMP加到原始SEL中
BOOL addSuccess = class_addMethod(selfClass, originalSelector, newIMP, method_getTypeEncoding(newMethod));
if (addSuccess) {
class_replaceMethod(selfClass, newSelector, originalIMP, method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, newMethod);
}
} - (instancetype)base_init
{
id instance = [self base_init]; [self resetCustomImageCachePath]; return instance;
} /**
* 自定义图片缓存路径
*/
- (void)resetCustomImageCachePath {
//reset the memory cache
NSString *rootDirectory = kAppImageCacheRootDirectory;
NSCache *memCache = (NSCache *)[self valueForKey:@"memCache"];
memCache.name = rootDirectory; //reset the disk cache
NSString *path = [self makeDiskCachePath:rootDirectory];
[self setValue:path forKey:@"diskCachePath"];
}

主要的方法有:

class_getInstanceMethod

method_getImplementation

class_addMethod

class_replaceMethod

method_getTypeEncoding

method_exchangeImplementations

+ (void)load静态方法会在类加载时候,即init前调用,分类的load方法顺序在原类的load方法之后。在这个时候交换init方法,添加修改缓存路径的方法即可达到目的。

- (instancetype)base_init方法中调用了[self base_init],因为与init方法已经交换,所以该行代码其实就调用了原init方法。

3.使用KVC

就是因为KVC技术的存在,所以之前说“在观感上达到私有属性和变量的效果”。自定义的分类,不能直接访问memCache和diskCachePath属性,所以上述代码,使用了NSObject对象的方法:

- (nullable id)valueForKey:(NSString *)key;

- (void)setValue:(nullable id)value forKey:(NSString *)key;

只需要知道属性或者变量名称,即可获取值或者设置值。

4.使用performSelector调用对象方法

KVC可以操作私有属性,针对私有方法,则可以通过对象的如下方法,对其不可见的方法进行调用:

- (id)performSelector:(SEL)aSelector;

- (id)performSelector:(SEL)aSelector withObject:(id)object;

- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

还可以使用如下方法,先判断是否能相应某个指定方法:

- (BOOL)respondsToSelector:(SEL)aSelector;

添加常用类库SVProgressHUD:https://github.com/SVProgressHUD/SVProgressHUD

准备为其增加分类方法,实现显示过场时的加载动画效果。新建分类SVProgressHUD+Extension,增加方法如下:

+ (void)showAnimationImages:(NSArray<UIImage *> *)images animationDuration:(NSTimeInterval)animationDuration status:(NSString *)status
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//写在该范围内的代码,都不会被编译器提示上述类型的警告
SVProgressHUD *sharedProgressHUD = (SVProgressHUD *)[SVProgressHUD performSelector:@selector(sharedView)];
__weak SVProgressHUD *weakInstance = sharedProgressHUD; [[NSOperationQueue mainQueue] addOperationWithBlock:^{
__strong SVProgressHUD *strongInstance = weakInstance;
if(strongInstance){
// Update / Check view hierarchy to ensure the HUD is visible
// [strongSelf updateViewHierarchy]; [strongInstance performSelector:@selector(updateViewHierarchy)]; // Reset progress and cancel any running animation
// strongSelf.progress = SVProgressHUDUndefinedProgress;
// [strongSelf cancelRingLayerAnimation];
// [strongSelf cancelIndefiniteAnimatedViewAnimation];
[strongInstance setValue:@(-) forKey:@"progress"];
[strongInstance performSelector:@selector(cancelRingLayerAnimation)];
[strongInstance performSelector:@selector(cancelIndefiniteAnimatedViewAnimation)]; // Update imageView
// UIColor *tintColor = strongSelf.foregroundColorForStyle;
// UIImage *tintedImage = image;
// if([strongSelf.imageView respondsToSelector:@selector(setTintColor:)]) {
// if (tintedImage.renderingMode != UIImageRenderingModeAlwaysTemplate) {
// tintedImage = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
// }
// strongSelf.imageView.tintColor = tintColor;
// } else {
// tintedImage = [strongSelf image:image withTintColor:tintColor];
// }
// strongSelf.imageView.image = tintedImage;
// strongSelf.imageView.hidden = NO;
UIImageView *imageView = (UIImageView *)[strongInstance valueForKey:@"imageView"];
[imageView setImage:images[]];
[imageView setAnimationImages:images];
[imageView setAnimationDuration:animationDuration];
imageView.size = images[].size;
imageView.hidden = NO;
[imageView startAnimating]; // Update text
// strongSelf.statusLabel.text = status;
UILabel *statusLabel = (UILabel *)[strongInstance valueForKey:@"statusLabel"];
statusLabel.text = status; // Show
// [strongSelf showStatus:status];
[strongInstance performSelector:@selector(showStatus:) withObject:status]; // An image will dismissed automatically. Therefore we start a timer
// which then will call dismiss after the predefined duration
// strongSelf.fadeOutTimer = [NSTimer timerWithTimeInterval:duration target:strongSelf selector:@selector(dismiss) userInfo:nil repeats:NO];
// [[NSRunLoop mainRunLoop] addTimer:strongSelf.fadeOutTimer forMode:NSRunLoopCommonModes];
NSTimer *timer = [NSTimer timerWithTimeInterval: target:strongInstance selector:@selector(dismiss) userInfo:nil repeats:NO];
[strongInstance setValue:timer forKey:@"fadeOutTimer"];
}
}];
#pragma clang diagnostic pop
}

可以看到,使用了上述手段,在不侵入原代码的情况下,实现了新增方法效果。

5.即是标题中描述的:忽略编译警告

先记录忽略代码片段中的编译警告

注意上述代码中,增加了如下内容:

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//写在该范围内的代码,都不会被编译器提示上述类型的警告
#pragma clang diagnostic pop

因为使用performSelector方法时候,不可见的方法名,会被提示“Undeclared selector”的警告。使用上述代码,可以忽略代码片段中指定类型(-Wundeclared-selector)的编译警告。

同理,也可以用于忽略其他类型的编译警告。

但是,关键问题在于:如何获取编译警告的类型Flag,例如-Wundeclared-selector。

先注释上述控制代码,即出现编译警告:

App开发流程之使用分类(Category)和忽略编译警告(Warning)

然后右键其中一个警告,选择Reveal In Log:

App开发流程之使用分类(Category)和忽略编译警告(Warning)

在All Issues中,关注如下内容:

App开发流程之使用分类(Category)和忽略编译警告(Warning)

其中,[-Wundeclared-selector]就是该警告的类型flag。

再顺便记录一下,自定义warning的控制代码,用于提示自己或者同事:#warning This is a custom warning

6.忽略指定文件的编译警告

找出警告类型如上,然后将flag内容修改为类似:-Wno-undeclared-selector,添加到下图中Compiler Flags中:

App开发流程之使用分类(Category)和忽略编译警告(Warning)

这步骤与添加“-fno-objc-arc”的非ARC编译flag一样。

7.忽略整个工程(Target)的编译警告

在上图的Build Settings栏下,找到Other Warning Flags项:

App开发流程之使用分类(Category)和忽略编译警告(Warning)

将之前步骤中找到的警告类型flag,加入Other Warning Flags的值中。

以上记录了分类使用过程中常见的情况。合理使用分类,可以在形式上分离代码;扩展类的属性和方法;减少类继承的复杂层级关系。

示例代码在Base框架中,Base项目已更新:https://github.com/ALongWay/base.git