读源码MBProgressHUD

前言

都说程序员想提高技术最好的方法就是读源码,学习大神的源码,吸取大神写代码的逻辑思路,甚至连写代码习惯和风格都可以学习。所以我决定读一些常用第三方库的源码,今天带来一篇读MBProgressHUD源码,抛砖引玉,如果写的有问题希望大家见谅并且希望大家能提出来(ps:第一次写。。。。看到最后有福利哦!!!!)


MBProgressHUD

MBProgressHUD是什么?MBProgressHUD是一个显示HUD窗口的第三方库,常用于网络请求或者后台执行任务时,在界面上显示一个表示进度的loading视图或者文本提示的HUD窗口,给用户提示。

好了,现在介绍完了MBProgressHUD,我们来开始读源码吧,这篇主要通过三个部分来解读它:

  1. 核心API
  2. 方法调用流程图
  3. 内部方法解读

1.核心API

  • 类方法
// 创建新的HUD,添加到提供的view上并显示
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
//找到最顶层的HUD并且隐藏,发现并删除则返回YES,否则NO
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
//找到最顶层的HUD并且返回
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
  • 实例方法
//初始化创建HUD,并将HUD的frame设置为view.bounds
- (instancetype)initWithView:(UIView *)view;
//显示HUD
- (void)showAnimated:(BOOL)animated;
//隐藏HUD
- (void)hideAnimated:(BOOL)animated;
//延迟指定时间后隐藏HUD
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;
  • 重要的属性
//show函数执行到显示HUD的时间段,默认为0
@property (assign, nonatomic) NSTimeInterval graceTime;
//显示的最小时间
@property (assign, nonatomic) NSTimeInterval minShowTime;
//HUD显示的类型
@property (assign, nonatomic) MBProgressHUDMode mode;
//HUD显示动画类型
@property (assign, nonatomic) MBProgressHUDAnimation animationType

看完了主要API,接下来我们来看看MBProgressHUD的方法调用流程图:

2.方法调用流程图

MBProgressHUD的提供的接口方法可以大致上分为两类:显示(show)和隐藏(hide)。而且无论是调用显示方法还是隐藏方法,最终都会走到私有方法animateIn:withType: completion:里。下面是方法调用流程图:

方法调用流程图

看完了方法调用流程图,让我们来看看内部方法是怎么实现的吧

3.内部方法解读

我们先来解读show方法:

+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [[self alloc] initWithView:view]; 
// 创建HUD,之后会调用[self initWithFrame:view.bounds],把HUD的frame设置为传进来的view的frame
    hud.removeFromSuperViewOnHide = YES; // 将HUD设置为在隐藏后被移除的状态,应该是一个标记
    [view addSubview:hud]; // 把创建的HUD添加到传进来的view上
    [hud showAnimated:animated];
    return hud;
}

- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.minShowTimer invalidate]; // 取消当前设置的minShowTimer
    self.useAnimation = animated; //设置是否执行animation
    self.finished = NO; // 表示当前任务未完成
    // 如果设置了graceTime,则要推迟HUD的显示
    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;
    } // 立刻显示HUD  
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

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

// 所以的show方法最后都会走到这个方法
- (void)showUsingAnimation:(BOOL)animated {
// Cancel any previous animations 
    // 移除所有的动画
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

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

    // 记录开始的时间,hide时会用到
    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;
    }
}

我们可以看到,无论是类方法的show方法,还是对象方法的show方法,而且无论是触发了graceTimer还是没有触发,最后都会走到showUsingAnimation:方法来让HUD显示出来。

这里肯定会有人会问graceTimer有什么用,我们来解读下,顺带把MBProgressHUD中的三个定时器属性一起介绍下:

补充:

定时器属性

// 执行一次,在show方法调用后到HUD真正显示之前执行,graceTime默认为0
@property (nonatomic, weak) NSTimer *graceTimer;
// 执行一次,在HUD显示后到HUD被隐藏之间执行
@property (nonatomic, weak) NSTimer *minShowTimer;
// 执行一次,在HUD被隐藏方法调用后到HUD真正隐藏之前执行
@property (nonatomic, weak) NSTimer *hideDelayTimer;
  • graceTimer:若设置代理graceTime,那么HUD回在show方法被调用后的graceTime时间后显示。这么做的目的是为了防止如果任务完成很快,消耗的时间太短而造成HUD一闪而过的效果,用户体验很差。如果设置了graceTime,那么如果任务完成的很快,所消耗的时间少于所设置的graceTime,那么HUD则不会显示。
  • minShowTimer:若设置了minShowTime,那么就会在hide方法被调用后判断任务执行时间是不是少于minShowTime,如果任务执行时间少于minShowTime,那么HUD也不会立刻被隐藏,它会等待minShowTime时间之后才消失。这么做也是为了防止HUD出现一闪而过的情况。
  • hideDelayTimer:用来推迟HUD的隐藏。如果设置了delayTime,那么HUD会在hide方法触发之后等待delayTime时间才隐藏。

showUsingAnimation:(BOOL)animated方法里调用了[self setNSProgressDisplayLinkEnabled:YES]方法,这个方法是用来刷新NSProgress

// NSProgress的监听方法
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    // 通过CADisplayLink来刷新progress的变化。为啥不用KVO来监听是因为progress的变化可能很快的,如果使用KVO机制来监听progress的变化会很占用主线程。
    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;
    }
}

我们再来解读hide方法:

+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL
)animated {
    MBProgressHUD *hud = [self HUDForView:view]; // 获取当前view最前的HUD
    if (hud != nil) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hideAnimated:animated];
        return YES;
    }
    return NO;
}

+ (MBProgressHUD *)HUDForView:(UIView *)view {
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator]; // 倒序排序,把最早加入的子view放到最前面
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            return (MBProgressHUD*)subview;
        }
    }
    return nil;
}

- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.graceTimer invalidate];
    self.useAnimation = animated;
    self.finished = YES;
   // 如果设置了HUD最小的显示时间(minShowTime),则需要判断最小显示时间和已经显示的时间大小
    if (self.minShowTime > 0.0 && self.showStarted) {
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
        if (interv < self.minShowTime) {
           // 如果最小显示时间比较大,那么不会立刻触发HUD的隐藏,而是计算出二者的时间差,开启一个timer,经过这个时间差之后再触发隐藏
            
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;
        } 
    }
    // 如果最小显示时间比较小,则立刻触发HUD的隐藏
    [self hideUsingAnimation:self.useAnimation];
}

// minShowTimer的触发方法
- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}
// hide方法最后都会走到这个方法里
- (void)hideUsingAnimation:(BOOL)animated {
    if (animated && self.showStarted) {
       // 隐藏时,将showStared设为nil
        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];
    }
}

解读完hide方法我们可以看到,无论是类方法的hide方法,还是对象方法的hide方法,而且无论是触发还是没有触发minShowTimer,最终都会走到`hideUsingAnimation这个方法里

而无论是show方法,还是hide方法,在设定animated属性为YES的前提下,最终都会走到animateIn: withType: completion:方法:

// 无论是show方法还是hide方法,如果设置了animated属性为YES的话,最终都会走到这个方法
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    // 通过animateIn来判断动画效果
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }

    // 设置放大和缩小的倍数
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);
   // Set starting state
     UIView *bezelView = self.bezelView;
    if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
        bezelView.transform = small;
    } 
else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
        bezelView.transform = large;
    }
  // 创建动画任务
    dispatch_block_t animations = ^{
        if (animatingIn) {
            bezelView.transform = CGAffineTransformIdentity;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) {
            bezelView.transform = large;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
            bezelView.transform = small;
        }
#pragma clang diagnostic push
#pragma clang diagnostic ignored 
"-Wdeprecated-declarations"
        bezelView.alpha = animatingIn ? self.opacity : 0.f;#pragma clang diagnostic pop        self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
    };

    
// Spring animations are nicer, but only available on iOS 7+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0
) {
        [UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion
:completion];
        return;
    }
#endif    
        [UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}

总结

  1. 提供的外部API最终都走到同一个私有方法,通过一个参数来判断是show还是hide
  2. 使用CADisplayLink来刷新频率很高的view
  3. 使用NSAssert来捕获各种异常

写在最后

读源码MBProgressHUD就这样大致写完了,没有怎么读过第三方框架的源码,所以第一次可能漏洞百出,还希望大家多多见谅,有什么写的不对或者欠妥的地方,希望大家指出来哈~~~

新垣结衣镇楼

新垣结衣
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 源码来源:gitHub源码 转载于: CocoaChina 来源:南峰子的技术博客 版本:0.9.1 MBPr...
    李小六_阅读 6,359评论 2 5
  • - (void)testMBProgressHUD { NSLog(@"test MBProgressHUD ")...
    有偶像包袱的程序狗阅读 1,486评论 0 1
  • 简书博客已经暂停更新,想看更多技术博客请到: 掘金 :J_Knight_ 个人博客: J_Knight_ 个人公众...
    J_Knight_阅读 5,784评论 36 38
  • 我在天外飞 眼前橙绿翡翠 我想饮下这甘冽 这无人的荒野 这单纯的荒野 像海洋展开大自然 一片无与伦比的羞涩 …… ...
    赛腾烟阅读 133评论 5 10
  • 我有一个同学,世界级名校毕业,成绩一直名列前茅,大学四年,一直也没谈个男朋友,生活上倒是贤惠,洗衣做饭样样全...
    Bonnie21阅读 447评论 0 3