iOS應用動態部署方案

iOS的動態部署能極大的節約成本。蘋果的審覈週期很長,有的時候,你可能不得不等待將近2個星期去上架你的新功能或者bug。因此動態部署是有價值的。
我這裏討論的狀況不把純web應用考慮在內,由於用戶體驗過於差,偶爾出現幾個頁面還能夠,可是整個app都是的話,無疑是很是糟糕的。html

理論上講,由於OC是一門動態語言,你能夠利用runtime框架,把任意腳本語言,只要該腳本的解釋器是由c語言解釋器寫成,就能夠實現由文本向代碼的轉變。甚至你也能夠本身實現一個解釋器(也許會有人喜歡造這樣的輪子),不過過小衆的話,可能除了你本身,就沒有人能夠維護了。git

說說比較大衆的解決方案:
第一個是lua
lua目前來說,更多的是應用在遊戲上,一般遊戲的包都很大,讓玩家經常來更新大家的客戶端,那麼等着玩家刪掉大家的客戶端吧。lua第一次名聲大噪大概是被魔獸世界所應用,如今手游上cocos-2d中的lua版本即使算不上很主流,但使用的廠商仍然很多。
iOS中,由阿里維護的wax是個比較好的選擇,使用起來也比較穩定,具體的仍能夠參見github上的文檔,感受阿里的同窗的維護。github

https://github.com/alibaba/wax

lua的優勢在於:解釋的速度要比js(下面介紹)要快一些
lua的缺點在於:須要將整個lua的解釋器打入程序中,不過這也是能夠接受的。另外lua的開發者可能會少一些,在招人上可能難一些。web

第二個是js
js目前也是更多的應用在遊戲上,因爲遊戲的特性驅動所產生技術的產生與成熟,確實讓遊戲在動態部署成熟了些。cocos-2d的js版本應用要比lua版本普遍一些。騰訊的bang同窗爲咱們開源了 JSPatch,向他致敬。app

https://github.com/bang590/JSPatch

js的優勢在於:系統內置了js的解釋器(iOS7及以後),會js的人多。
js的缺點在於:解釋稍慢。框架

以上兩種動態部署方案,我我的都嘗試過,出現的一些坑,確定要寫出來分享給你們。
1.庫衝突問題。這也是我爲何兩種方案都嘗試的緣由。我在首先嚐試的JSPatch,發現Aspects這個框架ide

https://github.com/steipete/Aspects    作一些AOP變成的hook庫。

同時使用了_objc_msgForward這個IMP,上面是Aspects,下面是JSPatch。我抱着僥倖的內心,雖然已經認識到WaxPatch極有可能也是如此實現的,去嘗試了WaxPatch,果不其然,依舊不行,看最下面的代碼,就是lua的。lua

static BOOL aspect_isMsgForwardIMP(IMP impl) {
    return impl == _objc_msgForward
#if !defined(__arm64__)
    || impl == (IMP)_objc_msgForward_stret
#endif
    ;
}
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
    SEL selector = NSSelectorFromString(selectorName);
    
    if (!typeDescription) {
        Method method = class_getInstanceMethod(cls, selector);
        typeDescription = (char *)method_getTypeEncoding(method);
    }
    
    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
    
    IMP msgForwardIMP = _objc_msgForward;
    #if !defined(__arm64__)
        if (typeDescription[0] == '{') {
            //In some cases that returns struct, we should use the '_stret' API:
            //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
            //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
            NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
            if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
                msgForwardIMP = (IMP)_objc_msgForward_stret;
            }
        }
    #endif

    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
    }
#pragma clang diagnostic pop

    if (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(!class_respondsToSelector(cls, originalSelector)) {
            class_addMethod(cls, originalSelector, originalImp, typeDescription);
        }
    }
    
    NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
    SEL JPSelector = NSSelectorFromString(JPSelectorName);

    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function;
    
    class_addMethod(cls, JPSelector, msgForwardIMP, typeDescription);
}

static BOOL isMethodReplacedByInvocation(id klass, SEL selector){
    Method selectorMethod = class_getInstanceMethod(klass, selector);
    IMP imp = method_getImplementation(selectorMethod);
    
#if defined(__arm64__)
    return imp == _objc_msgForward;
#else
    return imp == _objc_msgForward || imp == (IMP)_objc_msgForward_stret;
#endif
}

static BOOL isMethodReplacedByInvocation(id klass, SEL selector){
    Method selectorMethod = class_getInstanceMethod(klass, selector);
    IMP imp = method_getImplementation(selectorMethod);
    
#if defined(__arm64__)
    return imp == _objc_msgForward;
#else
    return imp == _objc_msgForward || imp == (IMP)_objc_msgForward_stret;
#endif
}
static void replaceMethodAndGenerateORIG(id klass, SEL selector, IMP newIMP){
    Method selectorMethod = class_getInstanceMethod(klass, selector);
    const char *typeDescription =  method_getTypeEncoding(selectorMethod);
    
    IMP prevImp = class_replaceMethod(klass, selector, newIMP, typeDescription);
    if(prevImp == newIMP){
//        NSLog(@"Repetition replace but, never mind");
        return ;
    }
    
    const char *selectorName = sel_getName(selector);
    char newSelectorName[strlen(selectorName) + 10];
    strcpy(newSelectorName, WAX_ORIGINAL_METHOD_PREFIX);
    strcat(newSelectorName, selectorName);
    SEL newSelector = sel_getUid(newSelectorName);
    if(!class_respondsToSelector(klass, newSelector)) {
        BOOL res = class_addMethod(klass, newSelector, prevImp, typeDescription);
//        NSLog(@"res=%d", res);
    }
}

既然衝突了,就必須解決衝突。魚與熊掌,兩者不可得兼,舍魚而取熊掌。只能把 Aspects幹掉了。
但是Aspects,實現的方法如何去替換呢?我就直接用了method swizzle。簡單的實現了下Aspects的功能,可是沒那麼優雅。debug

@implementation UIViewController (AOP)

+ (void)initialize {
    Method ori_Method =  class_getInstanceMethod([UIViewController class], @selector(viewDidAppear:));
    Method my_Method = class_getInstanceMethod([UIViewController class], @selector(aop_viewDidAppear:));
    method_exchangeImplementations(ori_Method, my_Method);
}

- (void)aop_viewDidAppear:(BOOL)animated {
    [self aop_viewDidAppear:animated];
    NSLog(@"------hook");
}

@end

對,就是簡單的hook一下。不過你要考慮清楚,對同一個方法hook幾回,最後的執行順序問題。code

ReactiveCocoa這個框架也可能存在相似問題,可能沒那麼好解決了。這個時候可能要作一些取捨,若是你是leader的話,可能要制定下規範來避免這個問題, 什麼方法不能夠用,要如何hook等等,暫時我也沒有太好的解決方案。

相關文章
相關標籤/搜索