iOS动态更新技术探索 (上)

简介

声明

该文章中的内容并不能用于实际项目中.(因为这样的玩法是被Apple禁止的).
当然企业账号没人管,可以这么玩玩试试.
文内也不包含什么高端技术.仅仅是突然想到了就做做试试,纯属个人兴趣,当做闲暇时间的娱乐.
补充: 模拟器玩玩就可以, 在真机上,目前常见系统版本,几乎都已经不允许动态加载沙盒内的动态库了.

目标:

实现像 PC 游戏维护更新一样,每次更新至下载相关文件而无需重新全部下载.
应用相关功能也全部由服务器提供下载更新部分内容的动态库来实现,而无需完全下载更新客户端.

说明:

工程共2个类:

  1. Framework类主要处理网络相关内容.
  2. ViewController类作为视图相关操作类.

内容

Framework类

该类主要是负责处理服务器请求到的framework相关信息的处理以及服务下载framework相关库等操作.

Framework头文件相关属性
相关方法未作展示

#import <Foundation/Foundation.h>

@interface Framework : NSObject

// 服务器返回的相关字段
@property (nonatomic, strong) NSString *name;       // 名称
@property (nonatomic, strong) NSString *className;  // 类名
@property (nonatomic, strong) NSString *type;       // 类型
@property (nonatomic, strong) NSString *url;        // 下载地址
@property (nonatomic, strong) NSString *version;    // 版本号

// 本地需要使用到的属性
@property (nonatomic, strong) NSString *fileName;   // 文件名 (解压后的 .framework 文件)
@property (nonatomic, strong) NSString *cachePath;  // 下载文件缓存路径
@property (nonatomic, strong) NSString *libPath;    // 解压后存放 fraework 文件夹路径
@property (nonatomic, strong) NSString *loadPath;   // framework 的完整路径

@end

Framework 类实现
最常见的一类方法,把从服务器请求回来的数据转换成 model对象,方便相关操作

/**
 根据字典创建对象

 @param dictionary 对象信息的字典
 @return  framewor 信息对象
 */
+ (instancetype)frameworkWithDictionary:(NSDictionary *)dictionary {
    Framework *lib = [[Framework alloc] init];
    [lib setValuesForKeysWithDictionary:dictionary];
    return lib;
}

/**
 通过请求结果创建对象

 @param data 请求数据
 @return 信息对象数组
 */
+ (NSArray *)librarysWithRequestData:(NSArray *)data {
    NSMutableArray *array = [NSMutableArray new];
    for (NSDictionary *dic in data) {
        [array addObject:[self frameworkWithDictionary:dic]];
    }
    return [array copy];
}

服务器获取 frmework相关信息

/**
 获取最新的 library 信息
 
 @param success 成功回调
 @param failure 失败回调
 */
+ (void)frameworksInfoWithSuccess:(void(^)(NSArray *frameworks))success
                          failure:(void(^)(NSError *error))failure {
    [[AFHTTPSessionManager manager] POST:@"http://172.16.30.197:8888/update/request.php" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"responseObject = %@", responseObject);
        success([self librarysWithRequestData:responseObject[@"librarys"]]);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"error = %@", error);
        failure(error);
    }];
}

取到 frmework 相关信息后,通过服务器返回的地址下载对应 framework 动态库文件

解压 zip 需要首先引入 #import <SSZipArchive.h>

- (void)downLoad:(void(^)(NSError * error))resultBlock {
    NSURL *url = [NSURL URLWithString:_url];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    NSURLSessionDownloadTask *task =
    [[AFHTTPSessionManager manager] downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
        return [NSURL fileURLWithPath:self.cachePath];
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
        if (!error) {
            [SSZipArchive unzipFileAtPath:self.cachePath toDestination:self.libPath];
        }
        resultBlock(error);
    }];
    [task resume];
}

以下是 Getter 方法

文件的存放的路径完全可以看自己的喜好

拼接 frmework 动态库文件,nametype扩展名都有服务器返回

- (NSString *)fileName {
    if (_fileName == nil) {
        self.fileName = [NSString stringWithFormat:@"%@.%@", _name, _type];
    }
    return _fileName;
}

下载的 zip 文件的存放地址

因为直接下载 .framework文件到本地之后,是一个无后缀的文件.为了避免出现别的问题直接由服务器提供 zip文件,另外资源文件做相关压缩操作也可以减小文件大小,提升下载速度.

- (NSString *)cachePath {
    if (_cachePath == nil) {
        // 这里要返回一个NSURL,其实就是文件的位置路径
        NSString * path = [self frameworkDir];
        path = [path stringByAppendingPathComponent:@"zip"];
        if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:nil]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
        }
        // 使用建议的路径
        _cachePath = [path stringByAppendingPathComponent:self.fileName];
        if ([[NSFileManager defaultManager] fileExistsAtPath:_cachePath]) {
            [[NSFileManager defaultManager] removeItemAtPath:_cachePath error:nil];
        }
    }
    return _cachePath;
}

zip文件解压后framework存放的文件夹路径

- (NSString *)libPath {
    if (_libPath == nil) {
        // 这里要返回一个NSURL,其实就是文件的位置路径
        NSString * path = [self frameworkDir];
        path = [path stringByAppendingPathComponent:@"lib"];
        if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:nil]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
        }
        // 使用建议的路径
        _libPath = path;
        if ([[NSFileManager defaultManager] fileExistsAtPath:_libPath]) {
            [[NSFileManager defaultManager] removeItemAtPath:_libPath error:nil];
        }
    }
    return _libPath;
}

framework动态库文件完整的路径

- (NSString *)loadPath {
    if (_loadPath == nil) {
        NSString *path = [self.libPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", _name, _type]];
        self.loadPath = [path stringByAppendingPathComponent:_name];
    }
    return _loadPath;
}

用于存放zipframework 文件的上路文件夹路径

- (NSString *)frameworkDir {
    // 这里要返回一个NSURL,其实就是文件的位置路径
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    // 使用建议的路径
    path = [path stringByAppendingPathComponent:@"Framework"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:nil]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
    }
    return path;
}

ViewController类

ViewController是工程的主视图控制器,同时也担任了动态库的加载以及动态库入口类的实例化相关操作.

- (void)viewDidLoad {
    [super viewDidLoad];
    [self serverLibiaryVersion];
}

参数className是已经约定好的动态库内部功能的入口类类名, 并且动态库里必须包含该类.否则相关操作也会失败.

- (void)startAppWithClassName:(NSString *)className {
    Class class = NSClassFromString(className);
    UIViewController *vc = [[class alloc] init];;
    [self addChildViewController:vc];
    [self.view addSubview:vc.view];
}

获取服务器目前提供的最新版本的动态库.
当前并未实现版本号等对比的逻辑,仅仅只是直接从服务器下载返回的版本信息指定的动态库并进行加载运行.相关原理大概就是这样,此处可以作为后续研究.

/**
 获取服务器动态库版本信息
 */
- (void)serverLibiaryVersion {
    [Framework frameworksInfoWithSuccess:^(NSArray *frameworks) {
        [self loadFrameworks:frameworks];
    } failure:^(NSError *error) {
        NSLog(@"error = %@", error);
    }];
}

根据服务器返回的动态库文件地址下载动态库文件

/**
 下载动态库
 */
- (void)loadFrameworks:(NSArray *)frameworks {
    Framework *framework = frameworks[0];
    
    [framework downLoad:^(NSError *error) {
        if (error) {
            NSLog(@"error = %@", error);
        }else {
            [self dlopenLoadDylibWithPath:framework.loadPath];
            [self startAppWithClassName:framework.className];
        }
    }];
}

加载本地动态库
加载本地动态库 首先需要引入 #import <dlfcn.h>

dlopen() 是一个计算机函数,功能是以指定模式打开指定的动态链接库文件,并返回一个句柄给dlsym()的调用进程。使用dlclose() 来卸载打开的库。

通过 framework 文件在本地的路径加载该动态库.

/**
 通过路径加载动态库
 */
- (BOOL)dlopenLoadDylibWithPath:(NSString *)path {
    void * libHandle = NULL;
    libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
    if (libHandle == NULL) {
        char *error = dlerror();
        NSLog(@"dlopen error: %s", error);
        dlclose(libHandle);
        return NO;
    } else {
        NSLog(@"dlopen load framework success.");
        dlclose(libHandle);
        return YES;
    }
}

未完待续

后续服务端以及最终效果见:iOS动态更新技术探索 (下)

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