简介
声明
该文章中的内容并不能用于实际项目中.(因为这样的玩法是被
Apple禁止的). 当然企业账号没人管,可以这么玩玩试试. 文内也不包含什么高端技术.仅仅是突然想到了就做做试试,纯属个人兴趣,当做闲暇时间的娱乐.补充: 模拟器玩玩就可以, 在真机上,目前常见系统版本,几乎都已经不允许动态加载沙盒内的动态库了.###目标:
实现像 PC 游戏维护更新一样,每次更新至下载相关文件而无需重新全部下载.
应用相关功能也全部由服务器提供下载更新部分内容的动态库来实现,而无需完全下载更新客户端.
说明:
工程共 2 个类:
Framework类主要处理网络相关内容.ViewController类作为视图相关操作类.
内容
Framework类
该类主要是负责处理服务器请求到的 framework 相关信息的处理以及服务下载 framework 相关库等操作.
Framework 头文件相关属性
相关方法未作展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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 对象,方便相关操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 根据字典创建对象
 @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 相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 获取最新的 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (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 动态库文件,name和type扩展名都有服务器返回
1
2
3
4
5
6
- (NSString *)fileName {
    if (_fileName == nil) {
        self.fileName = [NSString stringWithFormat:@"%@.%@", _name, _type];
    }
    return _fileName;
}
下载的 zip 文件的存放地址
因为直接下载
.framework文件到本地之后,是一个无后缀的文件.为了避免出现别的问题直接由服务器提供zip文件,另外资源文件做相关压缩操作也可以减小文件大小,提升下载速度.
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (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存放的文件夹路径
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (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动态库文件完整的路径
 1 2 3 4 5 6 7 - (NSString *)loadPath { if (_loadPath == nil) { NSString *path = [self.libPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", _name, _type]]; self.loadPath = [path stringByAppendingPathComponent:_name]; } return _loadPath; }用于存放
zip和framework文件的上路文件夹路径
 1 2 3 4 5 6 7 8 9 10 - (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 是工程的主视图控制器,同时也担任了动态库的加载以及动态库入口类的实例化相关操作.
1
2
3
4
- (void)viewDidLoad {
    [super viewDidLoad];
    [self serverLibiaryVersion];
}
参数 className 是已经约定好的动态库内部功能的入口类类名, 并且动态库里必须包含该类.否则相关操作也会失败.
1
2
3
4
5
6
7
- (void)startAppWithClassName:(NSString *)className {
    Class class = NSClassFromString(className);
    UIViewController *vc = [[class alloc] init];;
    [self addChildViewController:vc];
    [self.view addSubview:vc.view];
}
获取服务器目前提供的最新版本的动态库. 当前并未实现版本号等对比的逻辑,仅仅只是直接从服务器下载返回的版本信息指定的动态库并进行加载运行.相关原理大概就是这样,此处可以作为后续研究.
1
2
3
4
5
6
7
8
9
10
/**
 获取服务器动态库版本信息
 */
- (void)serverLibiaryVersion {
    [Framework frameworksInfoWithSuccess:^(NSArray *frameworks) {
        [self loadFrameworks:frameworks];
    } failure:^(NSError *error) {
        NSLog(@"error = %@", error);
    }];
}
根据服务器返回的动态库文件地址下载动态库文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 下载动态库
 */
- (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 
dlopen()是一个计算机函数,功能是以指定模式打开指定的动态链接库文件,并返回一个句柄给 dlsym() 的调用进程。使用 dlclose() 来卸载打开的库。
通过 framework 文件在本地的路径加载该动态库.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 通过路径加载动态库
 */
- (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动态更新技术探索 (下)