猜想: 能否通過服務器下發動態庫實現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的形式來編寫代碼