猜想: 能否通过服务器下发动态库实现App动态更新?
要上App Store的话,答案是不能的~
新建一个动态库
- 新建一个
framwork
,修改mach-O
为动态库类型(Xcode13.2.1
默认就是) - 把需要公开的Headers头文件暴露出来即可
例子:
#import "DynamicClass.h"
@implementation DynamicClass
+ (void)hello {
NSLog(@"我是动态库的方法: %s",__FUNCTION__);
}
- (void)hello{
NSLog(@"我是动态库的方法: %s",__FUNCTION__);
}
@end
下载动态库
依赖zip解压库 SSZipArchive
- (NSString *)fileNameWithURL:(NSURL *)url {
NSString *path = url.absoluteString;
NSRange range = [path rangeOfString:@"/" options:(NSBackwardsSearch)];
if( range.location != NSNotFound ){
return [path substringFromIndex:range.location+1];
}
return @"";
}
- (void)downloadDynamicFramework{
//把编译好带签名的动态库下载到Documents目录下并解压
NSURL *url=[NSURL URLWithString:@"http://192.168.1.122:8080/DynamicLib.framework.zip"];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
[[session downloadTaskWithRequest:req completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@",location.absoluteString);
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *targetZipPath = [doc stringByAppendingPathComponent:@"DynamicLib.framework.zip"];
NSString *targetPath = [doc stringByAppendingPathComponent:@"DynamicLib.framework"];
BOOL result = [[NSFileManager defaultManager] copyItemAtURL:location toURL:[NSURL fileURLWithPath:targetZipPath] error:nil];
NSLog(@"result: %@",result? @"成功!":@"失败!");
BOOL unzip = [SSZipArchive unzipFileAtPath:targetZipPath toDestination:targetPath];
if (unzip) {
NSLog(@"解压成功!!");
[[NSFileManager defaultManager] removeItemAtURL:[NSURL fileURLWithPath:targetZipPath] error:nil];
}else{
NSLog(@"解压失败!!");
}
}] resume];
}
加载动态库
// 加载沙盒里的动态库代码
- (void)loadFrameworkNamed:(NSString *)bundleName {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = nil;
if ([paths count] != 0) {
documentDirectory = [paths objectAtIndex:0];
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *bundlePath = [documentDirectory stringByAppendingPathComponent:[bundleName stringByAppendingString:@".framework"]];
// Check if new bundle exists
if (![manager fileExistsAtPath:bundlePath]) {
NSLog(@"No framework update");
bundlePath = [[NSBundle mainBundle]
pathForResource:bundleName ofType:@"framework"];
// Check if default bundle exists
if (![manager fileExistsAtPath:bundlePath]) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Oooops" message:@"Framework not found" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
[alertView show];
return ;
}
}
// Load bundle
NSError *error = nil;
NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath];
if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) {
NSLog(@"Load framework successfully");
}else {
NSLog(@"Failed to load framework with err: %@",error);
return ;
}
// Load class
Class cls = NSClassFromString(@"DynamicClass");
if (!cls) {
NSLog(@"Unable to load class");
return ;
}
[cls performSelector:@selector(hello)];
NSObject *obj = [cls new];
[obj performSelector:@selector(hello)];
}
经过实际验证:
下载framework 动态下发~ 事实证明并不能加载成功 目前只支持内嵌(Embedded)的动态库 动态下发到沙盒加载还是不行的
报错信息如下:
/**
Failed to load framework with err: Error Domain=NSCocoaErrorDomain Code=4 "The bundle “DynamicLib.framework” couldn’t be loaded because its executable couldn’t be located." UserInfo={NSLocalizedFailureReason=The bundle’s executable couldn’t be located., NSLocalizedRecoverySuggestion=Try reinstalling the bundle., NSBundlePath=/var/mobile/Containers/Data/Application/8E73B713-16B1-476A-96CB-C7DD0A1930A4/Documents/DynamicLib.framework, NSLocalizedDescription=The bundle “DynamicLib.framework” couldn’t be loaded because its executable couldn’t be located.}
*/
目前已尝试以下的两种方式:
- 把动态库添加进主工程并设置
Embed & sign
可以正常使用,但是应用一启动framework
就会自动被loaded
- 把动态库当做
bundle
资源,在Build phases
->Copy Bundle Resources
里添加.framework
文件, 通过代码拷贝到沙盒,这种方式也可以从沙盒加载,实际上也是从一开始就打包打进去了,就类似于那种不需要启动app
时一股脑儿加载一堆的库,而是使用到了才去加载,这种就类似于懒加载原理吧,从某种意义上(依赖的动态库数量级比较大的时候)来说可以优化启动时间
NSLog(@"%@",[NSBundle allFrameworks]);
//通过打印已加载的frameworks 发现并没有DynamicLib.framework
//Resources资源文件默认是放在mainBunle 把它拷贝到Documents目录下 (当然也可以直接加载mainBundle的路径 只是这里加载方法写的是Documents目录的)
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"DynamicLib" ofType:@"framework"];
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *targetPath = [doc stringByAppendingPathComponent:@"DynamicLib.framework"];
BOOL result = [[NSFileManager defaultManager] copyItemAtPath:filePath toPath:targetPath error:nil];
NSLog(@"result: %@",result? @"copy成功!":@"copy失败!");
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self loadFrameworkNamed:@"DynamicLib"]; //动态加载DynamicLib.framework
NSLog(@"%@",[NSBundle allFrameworks]);// 现在发现DynamicLib.framework也加载进来了
}
//2022-05-09 15:51:25.955926+0800 Demo[66285:11922021] 我是动态库的方法: +[DynamicClass hello]
//2022-05-09 15:51:25.956082+0800 Demo[66285:11922021] 我是动态库的方法: -[DynamicClass hello]
// "NSBundle </var/mobile/Containers/Data/Application/D645554B-097B-4AF7-944B-3445ABE84482/Documents/DynamicLib.framework> (loaded)",
//mainBundle直接加载如下:
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"DynamicLib" ofType:@"framework"];
NSError *error = nil;
NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath];
if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) {
NSLog(@"Load framework successfully");
}else {
NSLog(@"Failed to load framework with err: %@",error);
return ;
}
Class objCls = NSClassFromString(@"DynamicClass");
SEL sel = NSSelectorFromString(@"hello");
//类方法调用一
void *(*msgSend)(id, SEL) = (void *)objc_msgSend;
(void)(msgSend(objCls, sel));
//类方法调用二
[objCls performSelector:@selector(hello)];
//实例方法调用
NSObject *obj = [objCls new];
[obj performSelector:@selector(hello)];
// 使用dlopen加载如下, 需要导入头文件#include <dlfcn.h>
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"DynamicLib.framework/DynamicLib" ofType:nil];
//参数1: 可执行文件的路径
//参数2: 模式,立即加载/懒加载等几个宏定义常量
void* res= dlopen([bundlePath cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
if (res) {
Class objCls = NSClassFromString(@"DynamicClass");
SEL sel = NSSelectorFromString(@"hello");
void *(*msgSend)(id, SEL) = (void *)objc_msgSend;
(void)(msgSend(objCls, sel));
[objCls performSelector:@selector(hello)];
NSObject *obj = [objCls new];
[obj performSelector:@selector(hello)];
}
当然针对接口调用还是采用Protocol形式暴露出相关的接口进行调用,尽可能避免字符串调用hardcode的形式来编写代码