阅读MBProgressHUD有感

这段时间,把公司新版本做完后,开始加强自己的学习,自己看一些比较好的第三方库源码,从简单做起,一口一口吃好饭。
先从UI方面的第三方控件看起,最有名的就是MBProgressHUD
阅读优秀的第三方源码的优势,就是可以看到别人的代码规范,从而更好的了解自己和大神之间的距离。

代码规范

首先是外部变量的书写
//MBProgressHUD.h
extern CGFloat const MBProgressMaxOffset;
//MBProgressHUD.m
CGFloat const MBProgressMaxOffset = 1000000.f;
接下来是block的书写
typedef void (^MBProgressHUDCompletionBlock)(); 
/** 
 * Displays a simple HUD window containing a progress indicator and two optional labels for short messages.
 *
 * This is a simple drop-in class for displaying a progress HUD view similar to Apple's private UIProgressHUD class.
 * The MBProgressHUD window spans over the entire space given to it by the initWithFrame: constructor and catches all
 * user input on this region, thereby preventing the user operations on components below the view.
 *
 * @note To still allow touches to pass through the HUD, you can set hud.userInteractionEnabled = NO.
 * @attention MBProgressHUD is a UI class and should therefore only be accessed on the main thread.
 */
从函数头部的注释来阅读可以得出
这里说明了MBProgressHUD是UI控件,所以必须在主线程执行

写注释的时候,先@note(备注),再@param(参数),然后是@return(返回值),最后是@see(提示函数)
/**
 * Creates a new HUD, adds it to provided view and shows it. The counterpart to this method is hideHUDForView:animated:.
 *
 * @note This method sets removeFromSuperViewOnHide. The HUD will automatically be removed from the view hierarchy when hidden.
 *
 * @param view The view that the HUD will be added to
 * @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use
 * animations while appearing.
 * @return A reference to the created HUD.
 *
 * @see hideHUDForView:animated:
 * @see animationType
 */
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;

/**
 * Finds the top-most HUD subview and returns it. 
 *
 * @param view The view that is going to be searched.
 * @return A reference to the last HUD subview discovered.
 */
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;

如果是用instanceType就不用用nullable
但是如果是使用MBProgressHUD就可以用nullable

解析代码

整个文件中 公开的方法有
/**
 * 在某个view上添加HUD并显示
 *
 * 注意:显示之前,先去掉在当前view上显示的HUD。这个做法很严谨,我们将这个方案抽象出来:如果一个模型是这样的:我们需要将A加入到B中,但是需求上B里面只允许只有一个A。那么每次将A添加到B之前,都要先判断当前的b里面是否有A,如果有,则移除。
 */
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated; 
/**
 * 找到某个view上最上层的HUD并隐藏它。
 * 如果返回值是YES的话,就表明HUD被找到而且被移除了。
 */
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
/**
 * 在某个view上找到最上层的HUD并返回它。
 * 返回值可以是空,所以返回值的关键字为:nullable
 */
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
/**
 * 一个HUD的便利构造函数,用某个view来初始化HUD:这个view的bounds就是HUD的bounds
 */
- (instancetype)initWithView:(UIView *)view;
/** 
 * 显示HUD,有无动画。
 */
- (void)showAnimated:(BOOL)animated;
/** 
 * 隐藏HUD,有无动画。
 */
- (void)hideAnimated:(BOOL)animated;
/** 
 * 隐藏HUD,有无动画,且可以设置延时
 */
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

比较重要的部分:
三个NSSTimer
@property (nonatomic, weak) NSTimer *graceTimer; 
@property (nonatomic, weak) NSTimer *minShowTimer;
@property (nonatomic, weak) NSTimer *hideDelayTimer;
和一个CADisplayLink 
@property (nonatomic, weak) CADisplayLink *progressObjectDisplayLink;

代码执行

MBProgress整体函数调用的流程

刚刚在上文提及,整个文件中,比较复杂的就是上面三个timer的关系

@property (nonatomic, weak) NSTimer *graceTimer; //执行一次:在show方法触发后到HUD真正显示之前,前提是设定了graceTime,默认为0
@property (nonatomic, weak) NSTimer *minShowTimer;//执行一次:在HUD显示后到HUD被隐藏之前
@property (nonatomic, weak) NSTimer *hideDelayTimer;//执行一次:在HUD被隐藏的方法触发后到真正隐藏之前

graceTimer / minShowTimer (NSTimer)

graceTimer和minShowTimer的使用场所

- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.minShowTimer invalidate];
    self.useAnimation = animated;
    self.finished = NO;
    // If the grace time is set, postpone the HUD display
    if (self.graceTime > 0.0) {
        NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 
    // ... otherwise show the HUD immediately
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.graceTimer invalidate];
    self.useAnimation = animated;
    self.finished = YES;
    // If the minShow time is set, calculate how long the HUD was shown,
    // and postpone the hiding operation if necessary
    if (self.minShowTime > 0.0 && self.showStarted) {
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
        if (interv < self.minShowTime) {
            NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
            self.minShowTimer = timer;
            return;
        } 
    }
    // ... otherwise hide the HUD immediately
    [self hideUsingAnimation:self.useAnimation];
}

- (void)handleGraceTimer:(NSTimer *)theTimer {
    // Show the HUD only if the task is still running
    if (!self.hasFinished) {
        [self showUsingAnimation:self.useAnimation];
    }
}

- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}

从上面可以看出graceTimer他的实际含义就是
如果开发者设置了graceTime,那么MBProgressHUD不会立即显现,而是在graceTime之后才调用showUsingAnimation来展示出MBProgressHUD

从上面可以看出minShowTimer他的实际含义就是
如果开发者设置了minShowTimer,那么MBProgressHUD会在调用hide方法之前,判断当前的时间,如果当前的时间小于minShowTime,那么就会取消hide操作,开启延时,直到展示的时间达到minShowTime的时候,才调用hide操作来隐藏MBProgressHUD

hideDelayTimer(NSTimer)

hideDelayTimer 的使用场所

- (void)showUsingAnimation:(BOOL)animated {
    // Cancel any previous animations
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    // Cancel any scheduled hideDelayed: calls
    [self.hideDelayTimer invalidate];

    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    // Needed in case we hide and re-show with the same NSProgress object attached.
    [self setNSProgressDisplayLinkEnabled:YES];

    if (animated) {
        [self animateIn:YES withType:self.animationType completion:NULL];
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        self.bezelView.alpha = self.opacity;
#pragma clang diagnostic pop
        self.backgroundView.alpha = 1.f;
    }
}

- (void)done {
    // Cancel any scheduled hideDelayed: calls
    [self.hideDelayTimer invalidate];
    [self setNSProgressDisplayLinkEnabled:NO];

    if (self.hasFinished) {
        self.alpha = 0.0f;
        if (self.removeFromSuperViewOnHide) {
            [self removeFromSuperview];
        }
    }
    MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
    if (completionBlock) {
        completionBlock();
    }
    id delegate = self.delegate;
    if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
        [delegate performSelector:@selector(hudWasHidden:) withObject:self];
    }
}

//done函数方法的调用
- (void)hideUsingAnimation:(BOOL)animated {
    if (animated && self.showStarted) {
        self.showStarted = nil;
        [self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
            [self done];
        }];
    } else {
        self.showStarted = nil;
        self.bezelView.alpha = 0.f;
        self.backgroundView.alpha = 1.f;
        [self done];
    }
}

- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay {
    NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.hideDelayTimer = timer;
}

- (void)handleHideTimer:(NSTimer *)timer {
    [self hideAnimated:[timer.userInfo boolValue]];
}

从上面可以看出来,delayHideTimer的处理很聪明,其实整个文件中,核心调用隐藏的只有hideAnimated:方法,所以hideAnimated:(BOOL)animated afterDelay的实现策略,就是通过设置一个定时器,在afterDelay后再调用hideAnimated:方法,从而实现在设置延时之后再隐藏的视觉效果,good!

progressObjectDisplayLink(CADisplayLink)

我们先从官方文档了解一下CADisplayLink
A CADisplay​Link object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.
//CADisplayLink是一个定时器,获取应用中绘制屏幕刷新的速率来同步执行操作
Your application initializes a new display link, providing a target object and a selector to be called when the screen is updated. To synchronize your display loop with the display, your application adds it to a run loop using the add​To​Run​Loop:​for​Mode:​ method
//整个应用会初始化一个定时器,提供一个target目标和一个回调的selector,当屏幕更新的时候会回调该方法,我们应该将定时器添加入runLoop当中,使用add​To​Run​Loop:​for​Mode:
Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated. The target can read the display link’s timestamp
property to retrieve the time that the previous frame was displayed. For example, an application that displays movies might use the timestamp to calculate which video frame will be displayed next. An application that performs its own animations might use the timestamp to determine where and how displayed objects appear in the upcoming frame. The duration
property provides the amount of time between frames. You can use this value in your application to calculate the frame rate of the display, the approximate time that the next frame will be displayed, and to adjust the drawing behavior so that the next frame is prepared in time to be displayed.
//当displayLink和runloop连接的时候,回调方法会在屏幕更新的时候调用,同时target会获得对应的时间搓的属性来获得上一个frame展示的时候的时间搓,例如,当一个应用在播放电影的时候,可以使用这个时间搓来计算出接下来要展示哪一个电影片段,一个包含动画的应用中可以通过使用这个时间搓来确定应该在哪里,以什么方式来展示下一个动画片段,duration这个属性则是提供不同frame改变之间的所间隔的时间,你可以使用这个值帮助你计算出你的应用中,frame变化的比率,以及下一个frame出现的大致时间,根据这些绘制的行为,会让你的下一个frame在准确的时间内展示出来
Your application can disable notifications by setting the paused
property to YES
. Also, if your application cannot provide frames in the time provided, you may want to choose a slower frame rate. An application with a slower but consistent frame rate appears smoother to the user than an application that skips frames. You can define the number of frames per second by setting the preferred​Frames​Per​Second
property.
//你的应用可以停止消息提醒,当你设置pause属性为yes,此外,如果你的应用程序无法提供所提供的时间内的帧,您可能需要选择较慢的帧速率。使用较慢但一致的帧速率的应用程序比跳过帧的应用程序更适合用户使用。您可以通过设置preferred​Frames​Per​Second属性来定义每秒帧数。
When your application finishes with a display link, it should call invalidate
to remove it from all run loops and to disassociate it from the target.
//当你的应用使用完display link,请使用invalidate去移除runloop 并且将其从对象中取消关联
CADisplay​Link should not be subclassed.

CADisplayLink的使用场所有
- (void)setProgressObjectDisplayLink:(CADisplayLink *)progressObjectDisplayLink {
    if (progressObjectDisplayLink != _progressObjectDisplayLink) {
        [_progressObjectDisplayLink invalidate];
        
        _progressObjectDisplayLink = progressObjectDisplayLink;
        
        [_progressObjectDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    }
}

- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    // We're using CADisplayLink, because NSProgress can change very quickly and observing it may starve the main thread,
    // so we're refreshing the progress only every frame draw
    if (enabled && self.progressObject) {
        // Only create if not already active.
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        self.progressObjectDisplayLink = nil;
    }
}

- (void)updateProgressFromProgressObject {
    self.progress = self.progressObject.fractionCompleted;
}

调用setNSProgressDisplayLinkEnabled的场所
- (void)showUsingAnimation:(BOOL)animated {
    // Cancel any previous animations
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    // Cancel any scheduled hideDelayed: calls
    [self.hideDelayTimer invalidate];

    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    // Needed in case we hide and re-show with the same NSProgress object attached.
    [self setNSProgressDisplayLinkEnabled:YES];

    if (animated) {
        [self animateIn:YES withType:self.animationType completion:NULL];
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        self.bezelView.alpha = self.opacity;
#pragma clang diagnostic pop
        self.backgroundView.alpha = 1.f;
    }
}

- (void)done {
    // Cancel any scheduled hideDelayed: calls
    [self.hideDelayTimer invalidate];
    [self setNSProgressDisplayLinkEnabled:NO];

    if (self.hasFinished) {
        self.alpha = 0.0f;
        if (self.removeFromSuperViewOnHide) {
            [self removeFromSuperview];
        }
    }
    MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
    if (completionBlock) {
        completionBlock();
    }
    id delegate = self.delegate;
    if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
        [delegate performSelector:@selector(hudWasHidden:) withObject:self];
    }
}

- (void)setProgressObject:(NSProgress *)progressObject {
    if (progressObject != _progressObject) {
        _progressObject = progressObject;
        [self setNSProgressDisplayLinkEnabled:YES];
    }
}

/**
 * The NSProgress object feeding the progress information to the progress indicator.
 */
@property (strong, nonatomic, nullable) NSProgress *progressObject;

/**
 * The progress of the progress indicator, from 0.0 to 1.0. Defaults to 0.0.
 */
@property (assign, nonatomic) float progress;

所以从上面可以看出来,当开发者设置progressObject的时候,就自动会在动画展示的时候调用 [self setNSProgressDisplayLinkEnabled:YES]; 从而启动定时器,因此会在每次屏幕更新的时候,调用一次updateProgressFromProgressObject来获取当前的fractionCompleted,从而更新progress,在外面可以使用kvo来监听progress来做一些用户自己想要做的操作。

NSTimer和CADisplayLink

下面结合NSTimer来介绍 CADisplayLink,与NSTimer不同的地方有:

1、原理不同
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息,  CADisplayLink类对应的selector就会被调用一次。 
NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。
2、周期设置方式不同
iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector 默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置, CADisplayLink的selector每秒调用次数=60/ frameInterval。比如当 frameInterval设为2,每秒调用就变成30次。因此, CADisplayLink 周期的设置方式略显不便。
NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多。
3、精确度不同
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在忙于别的调用,触发时间就会推迟到下一个runloop周期。更有甚者,在OS X v10.9以后为了尽量避免在NSTimer触发时间到了而去中断当前处理的任务,NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间范围。
4、使用场合
从原理上不难看出, CADisplayLink 使用场合相对专一, 适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。

可以去阅读这篇文章重点讲CADisplayLink

总结

总的而言,MBProgressHUD总体难度不大,但是学到很多东西:
1.代码规范
2.思路很聪明,所有暴露的方法,最终都是调用同一个私有方法,在处理是否延时的时候依然使用这个思路,同样的方法执行,加一个定时器来区分罢了。
3.使用CADisplayLink
4.使用NSAssert

你可能感兴趣的:(阅读MBProgressHUD有感)