野火🔥

生命如野火,骄傲而顽强

JSPatch的一些使用注意点

JSPatch代码量不大,满满的都是神机妙算,到处都是黑科技。

主要的坑 -- UIWebView相关

如果在JSPatch的代码使用前,app没有初始化过UIWebView,那会造成UIWebView中JS不能解析的问题,打开网页根本不能看。解决方案:

JSPatch使用前先alloc一个UIWebView,然后让ARC自动dealloc它就好了:

UIWebView *webView = [[UIWebView alloc]init];   //Useless. Just to load the UIWebview framework.
webView.frame = CGRectZero;

[JPEngine startEngine];

[self startCache:^(NSString * script) {
    [JPEngine evaluateScript:script];
}]; 

哪怕初始化过UIWebView,也不能在使用JSPatch代码的时候进行UIWebView的初始化(既不能相同runloop初始化UIWebView),否则会造成WebView不稳定,极容易crash:

JavaScriptCore  WTF::equal(WTF::StringImpl const*, unsigned char const*) + 116
JavaScriptCore  std::__1::pair<std::__1::pair<WTF::StringImpl**, bool>, unsigned int> WTF::HashTable<WTF::StringImpl*, WTF::StringImpl*, WTF::IdentityExtractor, WTF::StringHash, WTF::HashTraits<WTF::StringImpl*>, WTF::HashTraits<WTF::StringImpl*> >::fullLookupForWriting<WTF::HashSetTranslatorAdapter<WTF::CStringTranslator>, unsigned char const*>(unsigned char const* const&) + 268
JavaScriptCore  WTF::HashTableAddResult<WTF::HashTableIterator<WTF::StringImpl*, WTF::StringImpl*, WTF::IdentityExtractor, WTF::StringHash, WTF::HashTraits<WTF::StringImpl*>, WTF::HashTraits<WTF::StringImpl*> > > WTF::HashTable<WTF::StringImpl*, WTF::StringImpl*, WTF::IdentityExtractor, WTF::StringHash, WTF::HashTraits<WTF::StringImpl*>, WTF::HashTraits<WTF::StringImpl*> >::addPassingHashCode<WTF::HashSetTranslatorAdapter<WTF::CStringTranslator>, unsigned char const* const&, unsigned char const* const&>(unsigned char const* const&&&, unsigned char const* const&&&) + 200
JavaScriptCore  WTF::AtomicStringImpl::add(unsigned char const*) + 160
WebCore WebCore::Settings::initializeDefaultFontFamilies() + 720
WebCore WebCore::Settings::Settings(WebCore::Page*) + 484
WebCore WebCore::Settings::create(WebCore::Page*) + 36
WebCore WebCore::Page::Page(WebCore::PageConfiguration&&) + 184
WebKitLegacy    -[WebView(WebPrivate) _commonInitializationWithFrameName:groupName:] + 1712
WebKitLegacy    -[WebView(WebPrivate) _initWithFrame:frameName:groupName:] + 132
UIKit   -[UIWebDocumentView initWithWebView:frame:] + 332
UIKit   -[UIWebBrowserView initWithWebView:frame:] + 48
UIKit   -[UIWebBrowserView initWithFrame:] + 44
UIKit   -[UIWebView _webViewCommonInitWithWebView:scalesPageToFit:] + 244
UIKit   -[UIWebView initWithFrame:] + 76

RN学习5——QDaily iOS app中通信和热修复实践

因为我们还用React Native 0.30版本,以后肯定会换最新版本,所以着急炒炒这份冷饭,总结总结使用经验。

上一篇介绍了Android的一些React Native应用中Native部分的开发,这篇主要在这个基础上继续介绍下iOS部分。iOS坑会少一点。

该文章为系列文章,之前的文章为RN学习1——前奏,app插件化和热更新的探索RN学习2——客户端开发者的一些准备工作RN学习3——集成进现有原生项目RN学习4——QDaily Android app中通信和热修复实践

一、先说针对hot fix的支持

启动时请求JSBundle更新

直接看流程图,此类用来管理JSBundle的位置以及热更新的版本。


资源文件的指向

上一篇说过,为支持资源的热更新,我们将所有resource都使用http://qdaily.cage/开头作为标记,在编译Android的JSBundle过程中,将其修改为file://data/*的本地文件路径。

iOS在此处的处理更为简单:在看React Native源码中可以发现,处理ImageView(即RCTImageView)的请求资源的方法在RCTConvert中,负责将传入的source json文件进行识别,如果uri以http开头,最终会调到+ (NSURL *)NSURL:(id)json方法中,最后调用系统方法NSURL *URL = [NSURL URLWithString:path]完成地址的绑定。知道原理就好了,我们只需要改变系统的+(NSURL*) URLWithString:(NSString*)url方法即可:

// NSURL+QDAdditions.m

+ (void) load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(URLWithString:) withSEL:@selector(swizzled_URLWithString:)];
    });
}

+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
    Class class = object_getClass((id)self); //不是类方法这里写 Class class = [self class];  就好了
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
    
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSEL,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSEL,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

+ (NSURL* )swizzled_URLWithString:(NSString*)strUrl {
    if ([strUrl hasPrefix:RNBundleResourcePrefix]) {
        strUrl = [GET_SERVICE(QDRNBundleManager) fileURLStringFromRNResourceRequest:strUrl];
    }
   return [self swizzled_URLWithString:strUrl];
}

原理很简单,替换NSURL的类方法,针对固定Prefix的url进行处理,下面是处理方法,针对模拟器情况下,为调试方便,会指向本地server的地址,非模拟器情况会按顺序读取沙盒和mainBundle。

//非模拟器读缓存
- (NSString *)fileURLStringFromRNResourceRequest:(NSString *)request {
#if (TARGET_IPHONE_SIMULATOR)
    return [request stringByReplacingOccurrencesOfString:RNBundleResourcePrefix withString:@"http://localhost:8081/resources/"];
#else
    NSString* strPath = [request stringByReplacingOccurrencesOfString:RNBundleResourcePrefix withString:@""];
    
    //查找沙盒
    if (_isDirReady) {
        NSString* file = [_sandBoxBundleResourceDirectory stringByAppendingPathComponent:strPath];
        if ([_fileManager fileExistsAtPath:file]) {
            return [NSString stringWithFormat:@"file://%@", file];
        }
    }
    //查找bundle
    NSString* file = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:strPath];
    if ([_fileManager fileExistsAtPath:file]) {
        return [NSString stringWithFormat:@"file://%@", file];
    }
    
    return request;
#endif
}

二、调起一个React Native组件

这里appdelegate中增加RCTBridge的成员变量bridge,因其初始化时会进行bundle的load操作以及大量的反射来生成module映射表,耗性能还耗内存,而我们又是混合app,很大概率不会使用,所以采用懒加载方式。为调试方便,在模拟器下指向本机server。

- (RCTBridge*)bridge {
    if (_bridge == nil) {
        _bridge= [[RCTBridge alloc]initWithDelegate:self launchOptions:nil];
    }
    return _bridge;
}

#pragma - mark RCTBridgeDelegate
- (NSURL*) sourceURLForBridge:(RCTBridge *)bridge {
#if (TARGET_IPHONE_SIMULATOR)
    return [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
    return [NSURL fileURLWithPath:GET_SERVICE(QDRNBundleManager).localReactNativeBundle];
#endif
}

在使用过程中,iOS没有那么多坑,很简单,初始化rootView并赋给ViewController的self.view就好了,传参可以传字典,所有基本数据类型都可以~

- (void) showImageSplash:(QDSplashResource*) source :(QDSplashEntity *)splash{
    RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[AppDelegate sharedAppDelegate].bridge
                                                     moduleName:@"adImageLaunch"
                                              initialProperties:@{
                                                                  @"imagePaths" : source.imagePathArray,
                                                                  @"redirectUrl" : splash.url,
                                                                  @"feedbackUrl" : splash.feedbackUrl,
                                                                  @"adTagUrl" : splash.adTagUrl,
                                                                  @"totalSeconds" : splash.totalSeconds,
                                                                  @"extras" : source.themeDictionary? source.themeDictionary: @{}
                                                                  }];
    [self showAdWithRN:rootView];
}

- (void) showAdWithRN:(RCTRootView* )rootView {
    if (rootView == nil) {
        return;
    }
    
    QDLaunchAdViewController* adControl = [[QDLaunchAdViewController alloc] init];
    
    rootView.frame = [UIScreen mainScreen].bounds;
    UIView *adVideoHub = [[[NSBundle mainBundle] loadNibNamed:@"LaunchVideoHub" owner:nil options:nil] firstObject];
    adVideoHub.frame = [UIScreen mainScreen].bounds;
    
    rootView.loadingView = adVideoHub;
    adControl.view = rootView;
    _currentAdController = adControl;
    adControl.delegate = self;
    
    self.adWindow.rootViewController = [[QDNavigationController alloc] initWithRootViewController: adControl];
}

非常小的一个技巧,可以把任意一个xib文件座位RCTRootView的loadingView,作为其没有加载时候的默认图。

三、JS端向Native端的通信方式

先贴一段code,和之前Android部分贴的功能一致:

@interface LaunchBridgeModule : NSObject<RCTBridgeModule>

@end


@implementation LaunchBridgeModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(dismissSplash:(RCTResponseSenderBlock)callback)
{
    [[QDLaunchADTool shareLaunchTool] clickForRemoveLaunthADView];
    callback(@[[NSNull null]]);
}

RCT_EXPORT_METHOD(open:(NSString*) strUrl:(RCTResponseSenderBlock)callback)
{
    if (EmptyString(strUrl)) {
        return;
    }
    QDLaunchAdViewController* launchController = [QDLaunchADTool shareLaunchTool].currentAdController;
    if (launchController) {
        strUrl = [strUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        [QDViewControllerRouter routeWithURL:[NSURL URLWithString:strUrl] byNavigationController:launchController.navigationController ArticleDetailJumpDelegate:nil];
        launchController.view.window.windowLevel = UIWindowLevelNormal;
        launchController.showRedirectURL = YES;
    }
    callback(@[[NSNull null]]);
}

- (dispatch_queue_t)methodQueue
{
    return dispatch_get_main_queue();
}
@end

所有JS能调用的Native都能力都在RCTBridgeModule这个protocol里面做了定义。每个原生模块都以单实例模式限制了嵌入。

RCTBridgeModule这个协议只有一个require方法:

+ (NSString *)moduleName;
// 对应Android中:
public String getName();

相对于Android将Module加进react native的映射表要先后生成Packager文件,然后addPackage两步,iOS拥有load方法:在implement中增加RCT_EXPORT_MODULE();一行宏就能解决之前的所有问题,极限解耦。该宏会覆写require方法,默认返回nil,并覆写load方法,该方法会在app启动时候class进入内存就会调用。具体load方法有哪些黑科技请自行google,这里不做介绍。

通过load方法的覆写,会把当前class在ReactBridge中进行注册,如果moduleName为空就直接使用当前class的name(注意,不管是否为空,都要保证最终module name和Android中定义的一致)。

第二个神奇的黑科技是RCT_EXPORT_METHOD()这个宏(Android中一般实现AOP编程--所谓黑科技基本用注解,iOS基本是宏)。这个宏会为其包住的方法再生成一个新的方法:

+ (NSArray<NSString *> *)RCT_CONCAT(__rct_export__, \
    RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \
    return @[@#js_name, @#method]; \
}

看实现可以知道所有用这个宏新生成的方法都以__rct_export__开头,返回值的都包含其方法名,在bridge初始化的时候,通过OC的runtime特性将其添加进入映射表,从而实现了"export"。

 while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
      Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);

      for (unsigned int i = 0; i < methodCount; i++) {
        Method method = methods[i];
        SEL selector = method_getName(method);
        if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
          IMP imp = method_getImplementation(method);
          NSArray<NSString *> *entries =
            ((NSArray<NSString *> *(*)(id, SEL))imp)(_moduleClass, selector);
          id<RCTBridgeMethod> moduleMethod =
            [[RCTModuleMethod alloc] initWithMethodSignature:entries[1]
                                                JSMethodName:entries[0]
                                                 moduleClass:_moduleClass];

          [moduleMethods addObject:moduleMethod];
        }
      }

      free(methods);
      cls = class_getSuperclass(cls);
    }

这些非常好的宏技巧我也在我自己的iOSBus中进行了广泛的使用,实现了非常好的解耦,还在进行开发调试中,欢迎follow、star。

RCTBridgeModule中还定义了一些optional方法,我这里只用了一个methodQueue,规定调用在主线程队列,这个也是默认队列,可以不写,其它请自行发现。

RN学习4——QDaily Android app中通信和热修复实践

React Native现在已经到了0.37版本了,在集成RN初期使用的0.30版本还不支持将resources打入bundle实施热更新,0.37版本已经解决了这些问题,如果再不写篇文章,炒炒这份冷饭,那就过气了。

QDaily现在在Android和iOS的版本中都集成了React Native,用其做广告效果页的展示。

本文介绍基于Android平台,在RN进行混合app研发过程中,native部分做过的一些工作和踩过一些坑。

本该双平台一起介绍的,但Android的坑多,先说Android,iOS的对应工作会在下个文章描述。

该文章为系列文章,之前的文章为RN学习1——前奏,app插件化和热更新的探索RN学习2——客户端开发者的一些准备工作RN学习3——集成进现有原生项目

一、Android中RN的View初始化及传参

马上看代码

//React相关组件初始化
ReactRootView mReactRootView = new ReactRootView(this);
mReactInstanceManager = ReactInstanceManager.builder()
      .setApplication(getApplication())
      .setJSBundleFile(getBundleFilePath())
      .setJSMainModuleName("index.android")
      .addPackage(new MainReactPackage())
      .setUseDeveloperSupport(BuildConfig.DEBUG)
      .setInitialLifecycleState(LifecycleState.BEFORE_RESUME)
      .setUseOldBridge(true)
      .build();

//准备参数
Bundle bundle = new Bundle();
bundle.putString("url", "http://www.qdaily.com");
bundle.putFloat("totalSeconds", 12);
bundle.putInt("style", 1);
bundle.putString("extras", "{\"key\" : \"value\"}");
bundle.putStringArray("imagePaths", new String[]{"url1", "url1"});

//组件启动
mReactRootView.startReactApplication(mReactInstanceManager, "adImageLaunch", bundle);

Android M、N适配踩坑

我们上个月才决定开始进行Android M、N的集中适配,发现很多问题,在此一起进行总结。

首先我们把buildToolsVersion和compileSdkVersion都改为24,相关support的lib也都改为24.*,以此放开了适配,遇上了很多坑。

这里不是一个大而全的适配方案,仅仅是一个小app(好奇心日报)的适配总结。

Android N的适配主要为组内同事操刀,所以文内部分内容源于该同事的总结。

ps:此后统一博客文章的路由命名方式,改为文章创见时间命名,如“2016-11-20”,若当天有第二篇则顺序命名为“2016-11-20-1”,以此来统一化,避免未来路由失效问题。

一、权限适配 -- Android M

作为一个新闻类app,适配的最主要的部分应该就是权限了。
Android6.0引入了动态权限控制,7.0使用了私有目录被限制访问Strict Mode API 政策
因此权限适配包含app权限获取部分和私有目录访问部分。

QDaily app连续crash处理方案


上周出现了一次MIUI8.0的开机crash情况,在已经提交应用市场审核后通过的前一刻发现并下架,但也惊出了一身冷汗,深刻感觉开机crash的保护方案尤其重要。
以前有crash的保护方案,但只是在连续crash后进行本地缓存清理,而且在Android端一直做的不够,不能够处理很多复杂情况。事实证明,这半年出现过两次打开crash的情况(一次iPad,一次MIUI8.0),之前的保护方案都未能生效。
本文先进行头脑风暴,画出思维导图,然后进行细化和测试。

思维导图

先出梗概,之后慢慢进行两个系统的细化实现。


iOS实现代码

Android的实现代码

闲聊计划

看了年初的计划,基本都没完成,基本都做了一些。

以前会要求自己每周列一个计划给自己,下周看完成情况,也是基本上都完不成,但每周很充实,都学了很多。

最近频繁往返京津、京冀,弄得自己好累,也耽误了个人的学习和提高,这样不好,缺少充实感和安全感,弄得这两天竟然怀疑人生。

所以计划还是有用的,就如同三只青蛙的励志段子,起码它告诉我这段时间我最重要的那三只青蛙是什么,一有机会还是会去做的。

这半年,看了几本书,写了几篇文章,学了一点新技术,也推动了组内的分享学习,挺好。
涨了工资,买了房子,办了户口,还有些希望。
也锻炼,也健身,也去咖啡馆研究过技术,不过没坚持,算是低谷吗?

虽然没达标,但起码有些回忆。

以后注意,还是要有点紧迫感。

都是瞎扯,有个博客发泄,还挺好。

QDaily - Android MVP改造

演进式框架设计。使得工程未来更有设计感和易于维护。
上个月事情多,周末好多时间都周转于河北天津,而且有点懈怠,以后博客继续更新。

整个改造思路来源于google的Android Architecture Blueprints,google这个框架名字就非常霸气,Android架构蓝图,有了这个做参考,给了架构设计一个非常指导性的建议。

吐槽《盛世的蝼蚁》


有人还没看过这篇文章,所以文章结尾附上原文

说是吐槽《盛世的蝼蚁》,其实是想表达下朋友圈中的这个病毒分享现象。

每次出现这种群体性分享情况,我的第一印象都是“暴民政治”,这堆暴民!民众懂个屁啊,国家肯定是精英治理。

国家的存在的主要目的是维护秩序,换句话说,维护大多数人的利益。维稳是国家的天然职责,而对穷人的福利却不是。国家和底层群众的基本关系就是,国家一定要避免底层群众揭竿而起。

读过《中国是部金融史》,才知道什么才叫做天地不仁,以万物为刍狗。

QDaily app流量和文章打开速度优化--js和css的加载逻辑


本文属于“好奇心日报app的流量和文章打开速度优化”系列文章第二篇,主要介绍为实现文章尽快的打开速度而进行的js和css的预载和缓存以及重用的逻辑。

整篇代码均为Objective-C,Android可以参考书写。

前言

好奇心日报所有文章都是自产,js和css文件在所有文章中都使用的一套进行逻辑和样式处理。在每次前端同事修改了js或者css代码后,会给该文件直接生成一个64位的hashcode附在其后,以区分不同版本,用于浏览器端更新使用,例如:

    http://app3.qdaily.com/assets/app3/common-bc6aa258d92609720eb97f34f86f978367bd3d849c9c0bbc82feeed9e79b6623.js

其中主要包括common.js、show.js、common.css和show.css四个文件。