iOS動態庫加載探究

猜想: 能否通過服務器下發動態庫實現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.}
 */
目前已嘗試以下的兩種方式:
  1. 把動態庫添加進主工程並設置 Embed & sign 可以正常使用,但是應用一啓動framework就會自動被loaded
  2. 把動態庫當做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的形式來編寫代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章