MBProgressHUD源碼解析

523878 7年前發布 | 10K 次閱讀 iOS開發 C語言 移動開發

聽過好多次:“程序員要通過多讀好的源碼來提升自己”這樣類似的話,而且又覺得自己有很多不會的,于是就馬上啟動了自己的 讀好源碼Project

從哪個框架開始呢?我想到了 SDWebImage ,但是大致看下來文件很多,代碼也不少,不知道從何看起,于是作罷。所以茅塞頓開,還是從最最簡單的框架開始吧~因為學習曲線要給自己設定得平緩一點才有利于穩步提升,小步快跑才是王道~

找著找著就找到了 MBProgressHUD ,這個框架只有兩個文件,一個頭文件和一個實現文件,很適合我現在的水平(對于一個沒怎么讀過源碼的選手),于是就擼起了袖子開始了。

連查知識點帶記筆記一共花了大概3個小時(雖然文件很少,但是里面好多東西都不知道[捂臉])。整體說來,收獲還是比較大的,除了一些零碎的語法之外,框架作者對于代碼結構的設計和各種情況的考慮還是很出色的,很值得學習,而且我在下文也有介紹。

這篇總結主要分三個部分來介紹這個框架:

  1. 核心Public API
  2. 方法調用流程圖
  3. 方法內部實現

不多說了,開始吧~

1. 核心Public API

1.1 屬性:

@property (assign, nonatomic) MBProgressHUDMode mode;//HUD的類型
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;//動畫類型

@property (assign, nonatomic) NSTimeInterval graceTime;//show函數觸發到顯示HUD的時間段
@property (assign, nonatomic) NSTimeInterval minShowTime;//HUD顯示的最短時間

1.2 類方法:

/**
 * 在某個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;

1.3 對象方法:

/**
 * 一個HUD的便利構造函數,用某個view來初始化HUD:這個view的bounds就是HUD的bounds
 */
- (instancetype)initWithView:(UIView *)view;

/** 
 * 顯示HUD,有無動畫。
 */
- (void)showAnimated:(BOOL)animated;

/** 
 * 隱藏HUD,有無動畫。
 */
- (void)hideAnimated:(BOOL)animated;

/** 
 * 在delay的時間過后隱藏HUD,有無動畫。
 */
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

看完了這些比較主要的API,我們看一下方法調用的流程圖:

2. 方法調用流程圖:

總體來說,這個第三方框架的接口還是比較整齊的,可以大致上分為兩類:顯示(show)和隱藏(hide)。而且無論是調用顯示方法還是隱藏方法,最終都會走到私有方法 animateIn:withType: completion: 里(前提是附加動畫效果)。可以看一下方法調用的流程圖:

方法調用流程圖

看完方法調用的結構之后,我們來具體看一下方法內部是如何實現的:

3. 方法內部實現:

在講解API之前,有必要先介紹一下HUD使用的三個Timer。

@property (nonatomic, weak) NSTimer *graceTimer; //執行一次:在show方法觸發后到HUD真正顯示之前,前提是設定了graceTime,默認為0
@property (nonatomic, weak) NSTimer *minShowTimer;//執行一次:在HUD顯示后到HUD被隱藏之前
@property (nonatomic, weak) NSTimer *hideDelayTimer;//執行一次:在HUD被隱藏的方法觸發后到真正隱藏之前
  • graceTimer:用來推遲HUD的顯示。如果設定了graceTime,那么HUD會在 show 方法觸發后的graceTime時間后顯示。它的意義是:如果任務完成所消耗的時間非常短并且短于graceTime,則HUD就不會出現了,避免HUD一閃而過的差體驗。
  • minShowTimer:如果設定了minShowTime,就會在 hide 方法觸發后判斷任務執行的時間是否短于minShowTime。因此即使任務在minShowTime之前完成了,HUD也不會立即消失,它會在走完minShowTime之后才消失,這應該也是避免HUD一閃而過的情況。
  • hideDelayTimer:用來推遲HUD的隱藏。如果設定了delayTime,那么在觸發 hide 方法后HUD也不會立即隱藏,它會在走完delayTime之后才隱藏。

這三者的關系可以由下面這張圖來體現(并沒有包含所有的情況):

三種timer

下面開始分別講解 show 系列的方法和 hide 系列的方法。

3.1 show系列方法

+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [[self alloc] initWithView:view];// 接著調用 [self initWithFrame:view.bounds]:根據傳進來的view的frame來設定自己的frame
    hud.removeFromSuperViewOnHide = YES;//removeFromSuperViewOnHide 應該是一個標記,表明HUD自己處于“應該被移除的狀態”
    [view addSubview:hud];//在view上將自己的實例添加上去
    [hud showAnimated:animated];
    return hud;
}

//調用showAnimated:
- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.minShowTimer invalidate];//取消當前的minShowTimer
     self.useAnimation = animated;//設置animated狀態
     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;
    } 
    // ... otherwise show the HUD immediately
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

//self.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 :取消delay的timer
    [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;
    }
}

我們可以看到,無論是類方法的show方法,還是對象方法的show方法,而且無論是觸發了 graceTimer 還是沒有觸發,最后都會走到 showUsingAnimation: 方法來讓HUD顯示出來。

這里補充講解一下NSProgress的監聽方法:

- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    // 這里使用 CADisplayLink 來刷新progress的變化。因為如果使用kvo機制來監聽的話可能會非常消耗主線程(因為頻率可能非常快)。
    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;
    }
}

CADisplayLink是一個能讓我們以和屏幕刷新率同步的頻率將特定的內容畫到屏幕上的定時器類。 CADisplayLink以特定模式注冊到runloop后, 每當屏幕顯示內容刷新結束的時候,runloop就會向 CADisplayLink指定的target發送一次指定的selector消息, CADisplayLink類對應的selector就會被調用一次。

3.2 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]; //倒敘排序
    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最小顯示時間,那就需要判斷最小顯示時間和已經經過的時間的大小
     if (self.minShowTime > 0.0 && self.showStarted) {
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];

        //如果最小顯示時間比較大,則暫時不觸發HUD的隱藏,而是啟動一個timer,再經過二者的時間差的時間之后再觸發隱藏
        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;
        } 
     }
    //如果最小顯示時間比較小,則立即將HUD隱藏
    [self hideUsingAnimation:self.useAnimation];
}

//self.minShowTimer觸發的方法
- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}

- (void)hideUsingAnimation:(BOOL)animated {
    if (animated && self.showStarted) {
        //隱藏時,將showStarted設為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 方法,而且無論是觸發還是沒有觸發 minShowTimer ,最終都會走到 hideUsingAnimation 這個方法里。

而無論是 show 方法,還是 hide 方法,在設定animated屬性為YES的前提下,最終都會走到 animateIn: withType: completion: 方法:

- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    // Automatically determine the correct zoom animation type
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }

    //()內代表x和y方向縮放倍數
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);

    // 設定初始狀態
    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
       //如果animatingIn是true,就是show方法,否則是hide方法
        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) {
        //執行動畫 >= iOS7
        [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. 將真正顯示的時間的前后加上緩沖的時間(graceTimer 和 hideDelayTimer),可以提高可定制性和穩定性。
  3. 如果有兩個方法是矛盾的,并且可以同時調用,就需要在全局設置一個屬性來判斷當前的狀態(removeFromSuperViewOnHide屬性,finished屬性)
  4. 使用CADisplayLink來刷新更新頻率可能很高的view。
  5. 使用NSAssert來捕獲各種異常。

就這樣大致寫完了,沒有怎么讀過第三方框架的源碼,所以第一次可能顯得稍許不足。有不好的地方還希望多多指點哈~

哦對了,還有一件事,筆者的個人主頁正式開放啦,主要將簡書里的大部分文章復制到了里面,以后發布博客的話二者會同時發布滴~

因為前段時間學了H5和CSS3,所以覺得博客主題不好的地方就自己花時間調了一下,整體效果還是比較滿意的:

 

來自:http://www.jianshu.com/p/6a5bd5fd8124

 

 本文由用戶 523878 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!