實現iOS圖片等資源文件的熱更新化(四): 一個最小化的補丁更新邏輯

来源:http://www.cnblogs.com/ios122/archive/2016/10/10/5945403.html
-Advertisement-
Play Games

我覺得,這篇文章最大的特點是,完整記錄了一次優化解決問題的過程;示例代碼看起來前後有些不太統一,是因為: 我不是先有了方案再寫博客,而是藉助博客本身來梳理思路,簡化邏輯!如此,寫博客,就不單單是一個耗時的分享知識的過程,更成為了一個幫助自己思考的有力工具!贊!!! ...


簡介

邏輯圖

以前寫過一個補丁更新的文章,此處會做一個更精簡的最小化實現,以便於集成.為了使邏輯具有通用性,將剝離對AFNetworking和ReativeCocoa的依賴.原來的文章,可以先看這裡: http://www.ios122.com/2015/12/jspatconline/

這麼做的意義

先交代動機和意義,或許應該成為自己博客的一個標準框架內容之一,不然以後自己需要看著,也不過是一堆乾癟的代碼.基本的邏輯圖,如上!此處,我就從簡!

從簡的原因有3:

  1. 補丁更新,狀態可以設計的很複雜,就像開頭那篇文章提到的那樣,但是我感覺沒多大必要,至少在我們的App中;
  2. 我想演示一個相對完整的邏輯,但是又不想耗費太多的時間構建場景;
  3. 從簡後的方案,簡單但夠用了,至少目前針對我們的項目來說;

所以說:這篇文章的意義,其實是在於簡化已有的熱更新代碼,越簡單越好維護.

基本思路

  1. App啟動時,判斷特定的伺服器介面所返回的圖片url是否為最新,判斷方式就是比對返回值中的md5欄位與本地保存的資源的url是否一致;
  2. 如果圖片資源有更新,則下載解壓到指定的緩存目錄,初步打算以資源文件的md5來劃分文件夾,來避免衝突;
  3. 讀取圖片時,優先從緩存目錄讀取,緩存目錄不存在再從ipa資源包中讀取;

下麵就一步一步來實現了.

App啟動時,判斷有無最新圖片資源

此處主要涉及到的可能的技術點:

1. 如何用基礎的網路類庫發送網路請求?

先簡單封裝一個函數來獲取,用到了block.block經常用,但到現在都記不太清形式,大都是從其他處copy下,然後改改參數.記不住,也懶得記!

- (void)fetchPatchInfo:(NSString *) urlStr completionHandler:(void (^)(NSDictionary * patchInfo, NSError * error))completionHandler
{
    NSURLSessionConfiguration * defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession * defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

    NSURL * url = [NSURL URLWithString:urlStr];

    NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithURL:url
                                                    completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) {
                                                        NSDictionary * patchInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];;

                                                        completionHandler(patchInfo, error);
                                                    }];

    [dataTask resume];
}

 

基於block,調用的代碼也就很簡答了.

[self fetchPatchInfo: @"https://raw.githubusercontent.com/ios122/ios_assets_hot_update/master/res/patch_04.json"
 completionHandler:^(NSDictionary * patchInfo, NSError * error) {
     if ( ! error) {
         NSLog(@"patchInfo:%@", patchInfo);
     }else
     {
         NSLog(@"fetchPatchInfo error: %@", error);
     }
 }];

 

好吧,我承認AFNetworking用習慣了,好久沒用原始的網路請求的代碼了,有點low,莫怪!

2. 如何校驗下載的文件的md5值,如果你需要的話?

開頭那篇文章鏈接里,有提到.核心,其實是在於下載文件之後,md5值的計算,剩餘的就是字元串比較操作了.

註意要先引入系統庫

 #include <CommonCrypto/CommonDigest.h>

 

/**
 *  獲取文件的md5信息.
 *
 *  @param path 文件路徑.
 *
 *  @return 文件的md5值.
 */
-(NSString *)mcMd5HashOfPath:(NSString *)path
{
    NSFileManager * fileManager = [NSFileManager defaultManager];

    // 確保文件存在.
    if( [fileManager fileExistsAtPath:path isDirectory:nil] )
    {
        NSData * data = [NSData dataWithContentsOfFile:path];
        unsigned char digest[CC_MD5_DIGEST_LENGTH];
        CC_MD5( data.bytes, (CC_LONG)data.length, digest );

        NSMutableString * output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];

        for( int i = 0; i < CC_MD5_DIGEST_LENGTH; i++ )
        {
            [output appendFormat:@"%02x", digest[i]];
        }

        return output;
    }
    else
    {
        return @"";
    }
}

 

3. 使用什麼保存與獲取本地緩存資源的md5等信息?

好吧,我打算直接使用用戶配置文件,

NSString * source_patch_key = @"SOURCE_PATCH";

[[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];
patchInfo = [[NSUserDefaults standardUserDefaults] objectForKey: source_patch_key];

NSLog(@"patchInfo:%@", patchInfo);

 

補丁下載與解壓

此處主要涉及到的可能的技術點:

1. 如何基於圖片緩存信息來找到指定的緩存目錄?

問題本身有些繞口,其實我想做的就是根據補丁的md5,放到不同的緩存文件夾,如補丁md5為 e963ed645c50a004697530fa596f180b,則對應放到 patch/e963ed645c50a004697530fa596f180b 文件夾.封裝一個簡單的根據md5返回緩存路徑的方法吧:

- (NSString *)cachePathFor:(NSString * )patchMd5
{
    NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);

    NSString * cachePath = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches/patch"] stringByAppendingPathComponent:patchMd5];

    return cachePath;
}

 

使用時,類似這樣:

NSString * urlStr = [patchInfo objectForKey: @"url"];

[weak_self downloadFileFrom:urlStr completionHandler:^(NSURL * location, NSError * error) {
    if (error) {
        NSLog(@"download file url:%@  error: %@", urlStr, error);
        return;
    }

    NSString * cachePath = [weak_self cachePathFor: [patchInfo objectForKey:@"md5"]];
    NSLog(@"location:%@ cachePath:%@",location, cachePath);

}];

 

2. 如何解壓文件到指定目錄?

在模擬中查看解壓後的文件

如果需要安裝 CocoaPods ,建議使用 brew:

brew install CocoaPods

 

解壓本身推薦 SSZipArchive 庫,一行代碼搞定:

[SSZipArchive unzipFileAtPath:location.path toDestination: patchCachePath overwrite:YES password:nil error:&error];

 

3. 在什麼時候更新本地的緩存資源的相關信息?

建議是在下載並解壓資源文件到指定緩存目錄後,再更新補丁的相關緩存信息,因為這個信息,讀取圖片時,也是需要的.如果刪除某個補丁,按照目前的設計,一種比較偷懶的方案就是,在伺服器上放上一個新的空資源文件就可以了.

NSString * source_patch_key = @"SOURCE_PATCH";

[[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];

 

讀取圖片功能擴展

此處主要涉及到的可能的技術點:

1. 如何用基礎的網路類庫下載文件?

依然是要封裝一個簡單函數,下載完成後,通過block傳出文件臨時的保存位置:

-(void) downloadFileFrom:(NSString * ) urlStr completionHandler: (void (^)(NSURL *location, NSError * error)) completionHandler
{
    NSURL * url = [NSURL URLWithString:urlStr];

    NSURLSessionConfiguration * defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession * defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:self delegateQueue: [NSOperationQueue mainQueue]];

    NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url
                                                                completionHandler:^(NSURL * location, NSURLResponse * response, NSError * error)
                                              {

                                                  completionHandler(location,error);

                                              }];
    [downloadTask resume];

}

 

2. 如何判斷bundle中是否含有某文件?

可以使用 fileExistsAtPath,但其實使用 -pathForResource: ofType: 就夠了,因為找不到資源問加你時,它返回nil,所以我們直接調用它,然後判斷返回是否為 nil 即可:

NSString * imgPath = [mainBundle pathForResource:imgName ofType:@"png"];

 

3. 將代碼如何與原有的imageNamed:邏輯合併?

不需要初始複製到緩存目錄 + 初始請求最新的資源補丁信息 + 代碼遷移合併 + 介面優化

相對完整的邏輯代碼

註意,按照目前的設計,就不需要初始把原來ipa中的bundle複製到緩存目錄了;當緩存目錄中沒有相關資源時,會自動嘗試從ipa中的bundle讀取,bundle約定統一使用 main.bundle 來簡化操作,

類目,對外暴露兩個方法:

#import <UIKit/UIKit.h>

@interface UIImage (imageNamed_bundle_)
/* load img smart .*/
+ (UIImage *)yf_imageNamed:(NSString *)imgName;

/* smart update for patch */
+ (void)yf_updatePatchFrom:(NSString *) pathInfoUrlStr;
@end

 

App啟動時,或在其他合適的地方,要註意檢查有無更新:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    /* fetch pathc info every time */
    NSString * patchUrlStr = @"https://raw.githubusercontent.com/ios122/ios_assets_hot_update/master/res/patch_04.json";
    [UIImage yf_updatePatchFrom: patchUrlStr];

    return YES;
}

 

內部實現,優化了許多,但也算不上複雜:

#import "UIImage+imageNamed_bundle_.h"
#import <SSZipArchive.h>

@implementation UIImage (imageNamed_bundle_)

+ (NSString *)yf_sourcePatchKey{
    return @"SOURCE_PATCH";
}

+ (void)yf_updatePatchFrom:(NSString *) pathInfoUrlStr
{
    [self yf_fetchPatchInfo: pathInfoUrlStr
       completionHandler:^(NSDictionary *patchInfo, NSError *error) {
           if (error) {
               NSLog(@"fetchPatchInfo error: %@", error);
               return;
           }

           NSString * urlStr = [patchInfo objectForKey: @"url"];
           NSString * md5 = [patchInfo objectForKey:@"md5"];

           NSString * oriMd5 = [[[NSUserDefaults standardUserDefaults] objectForKey: [self yf_sourcePatchKey]] objectForKey:@"md5"];
           if ([oriMd5 isEqualToString:md5]) { // no update
               return;
           }

           [self yf_downloadFileFrom:urlStr completionHandler:^(NSURL *location, NSError *error) {
               if (error) {
                   NSLog(@"download file url:%@  error: %@", urlStr, error);
                   return;
               }

               NSString * patchCachePath = [self yf_cachePathFor: md5];
               [SSZipArchive unzipFileAtPath:location.path toDestination: patchCachePath overwrite:YES password:nil error:&error];

               if (error) {
                   NSLog(@"unzip and move file error, with urlStr:%@ error:%@", urlStr, error);
                   return;
               }

               /* update patch info. */
               NSString * source_patch_key = [self yf_sourcePatchKey];
               [[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];
           }];
       }];

}

+ (NSString *)yf_relativeCachePathFor:(NSString *)md5
{
    return [@"patch" stringByAppendingPathComponent:md5];
}

+ (UIImage *)yf_imageNamed:(NSString *)imgName{
    NSString * bundleName = @"main";

    /* cache dir */
    NSString * md5 = [[[NSUserDefaults standardUserDefaults] objectForKey: [self yf_sourcePatchKey]] objectForKey:@"md5"];

    NSString * relativeCachePath = [self yf_relativeCachePathFor: md5];

    return [self yf_imageNamed: imgName bundle:bundleName cacheDir: relativeCachePath];
}

+ (UIImage *)yf_imageNamed:(NSString *)imgName bundle:(NSString *)bundleName cacheDir:(NSString *)cacheDir
{
    NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);

    bundleName = [NSString stringWithFormat:@"%@.bundle",bundleName];

    NSString * ipaBundleDir = [NSBundle mainBundle].resourcePath;
    NSString * cacheBundleDir = ipaBundleDir;

    if (cacheDir) {
        cacheBundleDir = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches"] stringByAppendingPathComponent:cacheDir];
    }

    imgName = [NSString stringWithFormat:@"%@@3x",imgName];

    NSString * bundlePath = [cacheBundleDir stringByAppendingPathComponent: bundleName];
    NSBundle * mainBundle = [NSBundle bundleWithPath:bundlePath];
    NSString * imgPath = [mainBundle pathForResource:imgName ofType:@"png"];

    /* try load from ipa! */
    if ( ! imgPath && ! [ipaBundleDir isEqualToString: cacheBundleDir]) {
        bundlePath = [ipaBundleDir stringByAppendingPathComponent: bundleName];
        mainBundle = [NSBundle bundleWithPath:bundlePath];
        imgPath = [mainBundle pathForResource:imgName ofType:@"png"];
    }

    UIImage * image;
    static NSString * model;

    if (!model) {
        model = [[UIDevice currentDevice]model];
    }

    if ([model isEqualToString:@"iPad"]) {
        NSData * imageData = [NSData dataWithContentsOfFile: imgPath];
        image = [UIImage imageWithData:imageData scale:2.0];
    }else{
        image = [UIImage imageWithContentsOfFile: imgPath];
    }
    return  image;
}

+ (void)yf_fetchPatchInfo:(NSString *) urlStr completionHandler:(void (^)(NSDictionary * patchInfo, NSError * error))completionHandler
{
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];

    NSURL * url = [NSURL URLWithString:urlStr];

    NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithURL:url
                                                    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                        NSDictionary * patchInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];;

                                                        completionHandler(patchInfo, error);
                                                    }];

    [dataTask resume];
}

+ (void) yf_downloadFileFrom:(NSString * ) urlStr completionHandler: (void (^)(NSURL *location, NSError * error)) completionHandler
{
    NSURL * url = [NSURL URLWithString:urlStr];

    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:nil delegateQueue: [NSOperationQueue mainQueue]];

    NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url
                                                                completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)
                                              {

                                                  completionHandler(location,error);

                                              }];
    [downloadTask resume];
}

+ (NSString *)yf_cachePathFor:(NSString * )patchMd5
{
    NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);

    NSString * cachePath = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches"] stringByAppendingPathComponent:[self yf_relativeCachePathFor: patchMd5]];

    return cachePath;
}

@end

 

現在,載入圖片的代碼更簡單了:

UIImage * image = [UIImage yf_imageNamed:@"sub/sample"];
self.sampleImageView.image = image;

 

如果熱更新生效,運行看到的應該是一個錘子圖片:

熱更新生效

後記

我覺得,這篇文章最大的特點是,完整記錄了一次優化解決問題的過程;示例代碼看起來前後有些不太統一,是因為: 我不是先有了方案再寫博客,而是藉助博客本身來梳理思路,簡化邏輯!如此,寫博客,就不單單是一個耗時的分享知識的過程,更成為了一個幫助自己思考的有力工具!贊!!!

參考資源:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JavaScript 中一些概念理解 :clientX、clientY、offsetX、offsetY、screenX、screenY ...
  • 導航條對於每一個Web前端攻城獅來說並不陌生,但是毛玻璃可能會相對陌生一些。簡單的說,毛玻璃其實就是讓圖片或者背景使用相應的方法進行模糊處理。這種效果對用戶來說是十分具有視覺衝擊力的。 本次分享的主題:通過CSS3來製作類似下麵的導航條和毛玻璃效果。 導航條是梯形形狀的。 背景區域的毛玻璃效果。 把 ...
  • 寫在前面 本文章版權歸博客園和作者共同所有,轉載請註明原文地址博客園吳雙 http://www.cnblogs.com/tdws/ 閉包真的是學過一遍又一遍,Js博大精深,每次學習都感覺有新的收穫。相信在大家封裝前端插件時,閉包是必不可少的。閉包的真正好處我個人認為除了封裝還是封裝,能帶個我們私有方 ...
  • 目錄結構: 效果圖: 方式 一: 方式 二: ...
  • 自己對正則驗證也沒系統用過,這次自己做個demo,一下子把這些全都用上了,下次有需要直接來拿了。 以下代碼是在頁面使用JQuery進行驗證的,也有在後臺進行驗證的,可以試試,都一樣的原理。 直接上代碼:註意:(有些驗證規則當然不僅僅是本文的,也許還有其他更好的,可以留言交流) 手機號:(移動-電信- ...
  • 圖片放大鏡 效果 "線上演示"    "源碼" 原理 首先選擇圖片的一塊區域,然後將這塊區域放大,然後再繪製到原先的圖片上,保證兩塊區域的中心點一致, 如下圖所示: <! more 初始化 獲得 canvas 和 image 對象,這裡使用 `` 標簽預載入圖片, 關於圖片預載入 ...
  • 1、現有兩對象間的繼承:Object.setPrototypeOf(child,father); 2、基於現有父對象創建子對象:var child=Object.create(father,{新屬性}); 3、批量修改多個子對象的父對象:在創建第一個子對象前,修改構造函數的prototype為新對象 ...
  • 繪製普通直線,先看效果圖: 實現代碼如下: 繪製貝塞爾曲線 效果圖如下: 代碼如下: 關於瞭解的html5的基本知識點就到這裡了,畢竟項目中沒有去使用,出於個人閑來無事有個大體瞭解.並且都很基本,其實這些基本的知識點感覺沒必要花費這麼多精力去關註,這個時間個人感覺花的太多,完全可以找個小demo去研 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...