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

2023-05-16

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:@(-1) 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[0]];
            [imageView setAnimationImages:images];
            [imageView setAnimationDuration:animationDuration];
            imageView.size = images[0].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:100 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。

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

 

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

 

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

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

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

 

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

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

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

 

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

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

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

 

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

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

转载于:https://www.cnblogs.com/ALongWay/p/5956005.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐