写点什么

iOS 源代码分析 ---- MBProgressHUD

  • 2019-12-10
  • 本文字数:5278 字

    阅读完需:约 17 分钟

iOS 源代码分析 ---- MBProgressHUD

MBProgressHUD 是一个为 iOS app 添加透明浮层 HUD 的第三方框架. 作为一个 UI 层面的框架, 它的实现很简单, 但是其中也有一些非常有意思的代码.

MBProgressHUD

MBProgressHUD 是一个 UIView 的子类, 它提供了一系列的创建 HUD 的方法. 我们在这里会主要介绍三种使用 HUD 的方法.


  • + showHUDAddedTo:animated:

  • - showAnimated:whileExecutingBlock:onQueue:completionBlock:

  • - showWhileExecuting:onTarget:withObject:

+ showHUDAddedTo:animated:

MBProgressHUD 提供了一对类方法 + showHUDAddedTo:animated:+ hideHUDForView:animated: 来创建和隐藏 HUD, 这是创建和隐藏 HUD 最简单的一组方法


Objective-C


+ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {  MBProgressHUD *hud = [[self alloc] initWithView:view];  hud.removeFromSuperViewOnHide = YES;  [view addSubview:hud];  [hud show:animated];  return MB_AUTORELEASE(hud);}
复制代码

- initWithView:

首先调用 + alloc - initWithView: 方法返回一个 MBProgressHUD 的实例, - initWithView: 方法会调用当前类的 - initWithFrame: 方法.


通过 - initWithFrame: 方法的执行, 会为 MBProgressHUD 的一些属性设置一系列的默认值.


Objective-C


- (id)initWithFrame:(CGRect)frame {  self = [super initWithFrame:frame];  if (self) {    // Set default values for properties    self.animationType = MBProgressHUDAnimationFade;    self.mode = MBProgressHUDModeIndeterminate;    ...    // Make it invisible for now    self.alpha = 0.0f;
[self registerForKVO]; ... } return self;}
复制代码


MBProgressHUD 初始化的过程中, 有一个需要注意的方法 - registerForKVO, 我们会在之后查看该方法的实现.

- show:

在初始化一个 HUD 并添加到 view 上之后, 这时 HUD 并没有显示出来, 因为在初始化时, view.alpha 被设置为 0. 所以我们接下来会调用 - show: 方法使 HUD 显示到屏幕上.


Objective-C


- (void)show:(BOOL)animated {    NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread.");  useAnimation = animated;  // If the grace time is set postpone the HUD display  if (self.graceTime > 0.0) {        NSTimer *newGraceTimer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];        [[NSRunLoop currentRunLoop] addTimer:newGraceTimer forMode:NSRunLoopCommonModes];        self.graceTimer = newGraceTimer;  }  // ... otherwise show the HUD imediately  else {    [self showUsingAnimation:useAnimation];  }}
复制代码


因为在 iOS 开发中, 对于 UIView 的处理必须在主线程中, 所以在这里我们要先用 [NSThread isMainThread] 来确认当前前程为主线程.


如果 graceTime0, 那么直接调用 - showUsingAnimation: 方法, 否则会创建一个 newGraceTimer 当然这个 timer 对应的 selector 最终调用的也是 - showUsingAnimation: 方法.

- showUsingAnimation:

Objective-C


- (void)showUsingAnimation:(BOOL)animated {    // Cancel any scheduled hideDelayed: calls    [NSObject cancelPreviousPerformRequestsWithTarget:self];    [self setNeedsDisplay];
if (animated && animationType == MBProgressHUDAnimationZoomIn) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); } else if (animated && animationType == MBProgressHUDAnimationZoomOut) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); } self.showStarted = [NSDate date]; // Fade in if (animated) { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.30]; self.alpha = 1.0f; if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) { self.transform = rotationTransform; } [UIView commitAnimations]; } else { self.alpha = 1.0f; }}
复制代码


这个方法的核心功能就是根据 animationTypeHUD 的出现添加合适的动画.


Objective-C


typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {  /** Opacity animation */  MBProgressHUDAnimationFade,  /** Opacity + scale animation */  MBProgressHUDAnimationZoom,  MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,  MBProgressHUDAnimationZoomIn};
复制代码


它在方法刚调用时会通过 - cancelPreviousPerformRequestsWithTarget: 移除附加在 HUD 上的所有 selector, 这样可以保证该方法不会多次调用.


同时也会保存 HUD 的出现时间.


Objective-C


self.showStarted = [NSDate date]
复制代码

+ hideHUDForView:animated:

Objective-C


+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {  MBProgressHUD *hud = [self HUDForView:view];  if (hud != nil) {    hud.removeFromSuperViewOnHide = YES;    [hud hide:animated];    return YES;  }  return NO;}
复制代码


+ hideHUDForView:animated: 方法的实现和 + showHUDAddedTo:animated: 差不多, + HUDForView: 方法会返回对应 view 最上层的 MBProgressHUD 的实例.


Objective-C


+ (MB_INSTANCETYPE)HUDForView:(UIView *)view {  NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];  for (UIView *subview in subviewsEnum) {    if ([subview isKindOfClass:self]) {      return (MBProgressHUD *)subview;    }  }  return nil;}
复制代码


然后调用的 - hide: 方法和 - hideUsingAnimation: 方法也没有什么特别的, 只有在 HUD 隐藏之后 - done 负责隐藏执行 completionBlockdelegate 回调.


Objective-C


- (void)done {  [NSObject cancelPreviousPerformRequestsWithTarget:self];  isFinished = YES;  self.alpha = 0.0f;  if (removeFromSuperViewOnHide) {    [self removeFromSuperview];  }#if NS_BLOCKS_AVAILABLE  if (self.completionBlock) {    self.completionBlock();    self.completionBlock = NULL;  }#endif  if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {    [delegate performSelector:@selector(hudWasHidden:) withObject:self];  }}
复制代码

- showAnimated:whileExecutingBlock:onQueue:completionBlock:

block 指定的队列执行时, 显示 HUD, 并在 HUD 消失时, 调用 completion.


同时 MBProgressHUD 也提供一些其他的便利方法实现这一功能:


Objective-C


- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block;- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(MBProgressHUDCompletionBlock)completion;- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue;
复制代码


该方法会异步在指定 queue 上运行 block 并在 block 执行结束调用 - cleanUp.


- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue   completionBlock:(MBProgressHUDCompletionBlock)completion {  self.taskInProgress = YES;  self.completionBlock = completion;  dispatch_async(queue, ^(void) {    block();    dispatch_async(dispatch_get_main_queue(), ^(void) {      [self cleanUp];    });  });  [self show:animated];}
复制代码


关于 - cleanUp 我们会在下一段中介绍.

- showWhileExecuting:onTarget:withObject:

当一个后台任务在新线程中执行时, 显示 HUD.


Objective-C


- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated {  methodForExecution = method;  targetForExecution = MB_RETAIN(target);  objectForExecution = MB_RETAIN(object);  // Launch execution in new thread  self.taskInProgress = YES;  [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil];  // Show HUD view  [self show:animated];}
复制代码


在保存 methodForExecution targetForExecutionobjectForExecution 之后, 会在新的线程中调用方法.


Objective-C


- (void)launchExecution {  @autoreleasepool {#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks"    // Start executing the requested task    [targetForExecution performSelector:methodForExecution withObject:objectForExecution];#pragma clang diagnostic pop    // Task completed, update view in main thread (note: view operations should    // be done only in the main thread)    [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO];  }}
复制代码


- launchExecution 会创建一个自动释放池, 然后再这个自动释放池中调用方法, 并在方法调用结束之后在主线程执行 - cleanUp.

Trick

MBProgressHUD 中有很多神奇的魔法来解决一些常见的问题.

ARC

MBProgressHUD 使用了一系列神奇的宏定义来兼容 MRC.


Objective-C


#ifndef MB_INSTANCETYPE#if __has_feature(objc_instancetype)  #define MB_INSTANCETYPE instancetype#else  #define MB_INSTANCETYPE id#endif#endif
#ifndef MB_STRONG#if __has_feature(objc_arc) #define MB_STRONG strong#else #define MB_STRONG retain#endif#endif
#ifndef MB_WEAK#if __has_feature(objc_arc_weak) #define MB_WEAK weak#elif __has_feature(objc_arc) #define MB_WEAK unsafe_unretained#else #define MB_WEAK assign#endif#endif
复制代码


通过宏定义 __has_feature 来判断当前环境是否启用了 ARC, 使得不同环境下宏不会出错.

KVO

MBProgressHUD 通过 @property 生成了一系列的属性.


Objective-C


- (NSArray *)observableKeypaths {  return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor",      @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil];}
复制代码


这些属性在改变的时候不会, 重新渲染整个 view, 我们在一般情况下覆写 setter 方法, 然后再 setter 方法中刷新对应的属性, 在 MBProgressHUD 中使用 KVO 来解决这个问题.


Objective-C


- (void)registerForKVO {  for (NSString *keyPath in [self observableKeypaths]) {    [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];  }}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (![NSThread isMainThread]) { [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO]; } else { [self updateUIForKeypath:keyPath]; }}
- (void)updateUIForKeypath:(NSString *)keyPath { if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] || [keyPath isEqualToString:@"activityIndicatorColor"]) { [self updateIndicators]; } else if ([keyPath isEqualToString:@"labelText"]) { label.text = self.labelText; } else if ([keyPath isEqualToString:@"labelFont"]) { label.font = self.labelFont; } else if ([keyPath isEqualToString:@"labelColor"]) { label.textColor = self.labelColor; } else if ([keyPath isEqualToString:@"detailsLabelText"]) { detailsLabel.text = self.detailsLabelText; } else if ([keyPath isEqualToString:@"detailsLabelFont"]) { detailsLabel.font = self.detailsLabelFont; } else if ([keyPath isEqualToString:@"detailsLabelColor"]) { detailsLabel.textColor = self.detailsLabelColor; } else if ([keyPath isEqualToString:@"progress"]) { if ([indicator respondsToSelector:@selector(setProgress:)]) { [(id)indicator setValue:@(progress) forKey:@"progress"]; } return; } [self setNeedsLayout]; [self setNeedsDisplay];}
复制代码


- observeValueForKeyPath:ofObject:change:context: 方法中的代码是为了保证 UI 的更新一定是在主线程中, 而 - updateUIForKeypath: 方法负责 UI 的更新.

End

MBProgressHUD 由于是一个 UI 的第三方库, 所以它的实现还是挺简单的.


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/ios-yuan-dai-ma-fen-xi-mbprogresshud


2019-12-10 17:52764

评论

发布
暂无评论
发现更多内容

知乎开发了一个搜索引擎

红泥

搜索引擎 百度 知乎

敏捷开发 | 张三与需求管理

易成研发中心

敏捷开发 需求管理

程序员陪娃漫画系列——修龙头

孙苏勇

程序员 生活 陪伴 漫画

如何阅读源码?

武培轩

Java 源码 面试 进阶 后端

我为什么选择infoq写作平台

三爻

Java并发编程系列——线程

孙苏勇

Java 并发编程 线程

除了负载均衡的算法,你还应该知道这些

松花皮蛋me

Java 负载均衡 分布式

决定我们认知深度的究竟是什么?

石君

深度思考 方法论 连接

前端如何搞监控总结篇

大前端洞见

大前端 监控 全链路监控

随手记备忘录的好习惯

changyou

微信朋友圈为什么没有阅读数?

彭宏豪95

微信 产品 产品设计

程序员都应该了解的运维知识经验

松花皮蛋me

DevOps 分布式 运维

小论互联网项目管理

南方

项目管理 互联网 个人成长 碧海潮生曲

回"疫"录(3):让人怀念的普通一天

小天同学

疫情 回忆录 现实纪录 纪实

程序员5分钟:你的程序占用了多少内存?

顾仲贤

程序员

Java并发编程系列插曲——对象的内存结构

孙苏勇

Java 内存模型 面向对象 ClassLayout

「超级右键」

非著名程序员

macos 程序员 效率工具 软件 Mac

程序员5分钟:你了解32位带符号的整型吗?

顾仲贤

程序员

很不幸,自动化测试永远只能是必要非充分条件

刘华Kenneth

DevOps 敏捷 自动化 测试 金字塔

基于Kubernetes的多云和混合云

倪朋飞

云计算 架构 Kubernetes 微服务 Service Mesh

爱他,就让他走?

Selina

团队管理 领导力 团队协作

Golang 真的好用吗?

极客时间

编程语言 Go 语言

克制文章长度

changyou

聊聊苹果公司技术部门的宫斗和冷战

赵钰莹

程序员 外包 apple

在谈判中,你有哪些属于自己的独特的方法和技巧?

Yolanda

世界知识产权日碎碎念

Yin

成长 随笔 知识产权

程序员陪娃漫画系列——看医生

孙苏勇

程序员 生活 陪伴 漫画

最近的一些人生感悟

小智

人生 哲学

加班能解决交付的期望么?

拖地先生

项目管理 领导力 管理 时间管理

别总说CMS、G1,该聊聊ZGC了

猿人谷

CMS G1 ZGC JVM

漫谈哲学与编程

keelii

编程 哲学

iOS 源代码分析 ---- MBProgressHUD_语言 & 开发_Draveness_InfoQ精选文章