对移动客户端技术的思考

14年到16年 客户端相关技术快速发展,大家在这方面的研究和落地结果非常多,插件化,包管理,组件化,各种脚手架和研发体系,换肤,热更新,动态化,跨平台,等等。各种技术和项目不断出现,百花齐放,那是移动客户端快速发展的阶段。现在,客户端的技术早已趋于稳定,同时偏重于运营的移动互联网对动态化的需求更为强烈。在这方面,前端,大前端更有优势,也更容易落地,也更为标准化。早期在动态化上的探索,主要集中在H5和Hybrid H5,之后还出现各种native的json动态化方案,还有以各种特有DSL去描述UI的方案,16年Facebook发起的react native项目,彻底点燃了前端跨平台演进的激情,weex随后跟进,再到flutter的出现。同时,前端框架react,Vue,angular的不断成熟,在构建前端/移动应用上,变得更加简单。

总体上,移动客户端开发早已进入稳定期,技术框架开发模式成熟,各种配套设施齐全。前端这几年的发展日新月异,到目前为止,以Vue/React/Angular为代表的前端框架也趋于稳定,周边配套也较为齐全,开发模式也趋于稳定,都以组件化和标准化为指导原则。以RN/Weex/Flutter为基础的大前端技术也趋于稳定,开发模式稳定了,周边配套也完善了,后续更多地会在框架性能稳定性开发效率上做文章,应用层面不会大动干戈了,但小的变动仍会有不少。敬那些逝去的项目框架,期待这些新框架仍能不断带来惊喜。

在对这些框架的使用和反哺上,大公司和小公司也必然是迥异的策略,小公司拿来即用,再配合一些胶水层代码,甚至这些代码也取之三方。大部分框架在追求通用性的同时,必然会在易用性上打折,大公司有能力也有人力对其深度定制,满足自身需求,提高使用效率,也能反馈给社区,甚至新的轮子就出来了。

2019/3/20 posted in  移动开发

跨端方案

UI系统

  • UI系统包含UI元素的布局、渲染和UI事件的捕获、传递、处理等。
  • iOS、Android、Web、微信小程序、其他小程序都有各自的原生UI系统。

系统服务

  • 原生系统的各种能力,来自硬件的、软件的。

开发语言不同、运行环境不同

开发模式不同

跨端方案

  • 不同原生系统上的UI系统和系统服务是有差别和相似的,跨端方案是尽量屏蔽这些差异,对应用开发者来说,开发体验是一致的的、透明的,就像jQuery屏蔽不同Web浏览器间的差异性。
  • 需要做到这点,跨端方案需要提供包括DSL、Runtime、组件库、工具链等,一整套屏蔽原生系统开发和运行的差异。

Runtime

  • Runtime需要支撑DSL在原生系统的运行,需要桥接到到原生系统服务、原生UI系统,需要提供自有或轻或重的UI渲染系统,需要提供对自有组件库或三方组件库的访问等。
  • UI引擎,轻的方案是直接桥接到原生UI系统,引擎只做UI数据的处理、转换、传递,重的方案是基于跨平台的渲染方案如OpenGL做一套自己的UI系统。

组件库

  • 自有、三方、原生系统的组件库等等。

工具链

  • 开发阶段,需要提供转换器或编译器,对DSL的支持,IDE,等等。
  • 调试阶段,调试工具、方式。
  • 测试阶段,测试工具。
  • 部署阶段,本地部署、远程部署。
2019/2/27 posted in  移动开发

2018-3-15

客户端上的一个页面搭建采用哪种架构,往往取决与页面类型。

展示型页面

数据从数据层到展示层单向流动

表单型页面

数据在数据层和展示层双向流动

交互型页面

展示层不仅展示数据,还有大量动画,操作行为等

组件

系统底层框架
网络
存储
缓存
序列化
架构
组件化
工程化
数据监控
日志
性能
Crash
安全
图片
视频、音频
AR、VR、ML
UI组件
屏幕分辨率
布局、样式
渲染
研发效率
编程语言、OC/Swift、OOP、AOP、FP
Hybrid
Native动态化
热补丁
小程序
PWA
编译
部署

2019/1/21 posted in  移动开发

技术图谱

2019/1/19 posted in  移动开发

h5 hybrid方案

三方库选择

方案比较通用,对UIWebView也支持。

需要改用DWKWebView,对原有代码略有侵入。

目前方案

  • 直接使用WKWebView原生接口

  • JS调用Native

[config.userContentController addScriptMessageHandler:[WKScriptMessageHandlerWrapper wrapper:self] name:JS_HANDLER_UrlRouter];

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSLog(@"Recv Js call : name =  %@", message.name);
    if ([message.name isEqualToString:JS_HANDLER_UrlRouter]) {
        [self handleUrlRouterMessage:message.body];
    } else if ([message.name isEqualToString:JS_HANDLER_ShareService]) {
        [self handleShareServiceMessage:message.body];
    }
}
  • Native调用JS
[self.webView evaluateJavaScript:@"window.JSHandler()" completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
    if (obj) {
        NSLog(@"receive js response %@", obj);
    }
}];

  • JS调用Native不支持返回值。

  • 接口调用不支持回调:JS和Native侧都增加个Callback队列,根据CallbackId执行对应的回调函数。

2019/1/19 posted in  移动开发

我想说的是,在接下来的几年里,我们已经发展起来的微型库架
构将逐渐被那些单体框架所取代。它们将采用一种约定配置
(convention-over-configuration)的方式,用透明度
换取生产力的提升。

RN

  • 优势:跨平台、动态性、性能媲美原生、Web开发人力
  • 劣势:兼容性问题(也许花在处理多端不一致、兼容性等问题上的时间比使用原生方案实现业务所花的时间更多)、包大小、与原生数据的异步交互等等
2019/1/19 posted in  移动开发

weex代码走读

  • WXSDKInstance -> WXBridgeManager->WXBridgeContext->WXJSCoreBridge->JSContext

  • WXSDKInstance -> WXComponentManager

  • WXBridgeManager : 对WXBridgeContext进一步封装,提供JS API调用,JS 事件发送,注册Native Call Handler,注册Native模块Handler、注册Native组件Handler、注册Native服务Handler等接口。提供Bridge独立线程,上述接口基本上都在该线程中执行。

  • WXComponentManager :管理着组件的布局,样式等

// WXComponentManager.mm

- (void)_layoutAndSyncUI
{
    [self _layout];
    if(_uiTaskQueue.count > 0){
        [self _syncUITasks];
        _noTaskTickCount = 0;
    } else {
        // suspend display link when there's no task for 1 second, in order to save CPU time.
        _noTaskTickCount ++;
        if (_noTaskTickCount > 60) {
            [self _suspendDisplayLink];
        }
    }
}

- (void)_layout
{
    BOOL needsLayout = NO;

    needsLayout = [_rootComponent needsLayout];

    if (!needsLayout) {
        return;
    }
#ifdef DEBUG
    WXLogDebug(@"flexLayout -> action__ calculateLayout root");
#endif

        std::pair<float, float> renderPageSize;
        renderPageSize.first = self.weexInstance.frame.size.width;
        renderPageSize.second = self.weexInstance.frame.size.height;
        _rootFlexCSSNode->calculateLayout(renderPageSize);
    NSMutableSet<WXComponent *> *dirtyComponents = [NSMutableSet set];
    [_rootComponent _calculateFrameWithSuperAbsolutePosition:CGPointZero gatherDirtyComponents:dirtyComponents];
    [self _calculateRootFrame];

    for (WXComponent *dirtyComponent in dirtyComponents) {
        [self _addUITask:^{
            [dirtyComponent _layoutDidFinish];
        }];
    }
}
  • WXSDKInstance : 加载bundle文件,管理、渲染根视图。

  • WXSDKManager :管理WXSDKInstance多实例

//WXSDKInstance.m

- (void)_renderWithMainBundleString:(NSString *)mainBundleString
{
    if (!self.instanceId) {
        WXLogError(@"Fail to find instance!");
        return;
    }
    self.performance.renderTimeOrigin = CACurrentMediaTime()*1000;

    if (![WXUtility isBlankString:self.pageName]) {
        WXLog(@"Start rendering page:%@", self.pageName);
    } else {
        WXLogWarning(@"WXSDKInstance's pageName should be specified.");
        id<WXJSExceptionProtocol> jsExceptionHandler = [WXHandlerFactory handlerForProtocol:@protocol(WXJSExceptionProtocol)];
        if ([jsExceptionHandler respondsToSelector:@selector(onRuntimeCheckException:)]) {
            WXRuntimeCheckException * runtimeCheckException = [WXRuntimeCheckException new];
            runtimeCheckException.exception = @"We highly recommend you to set pageName.\n Using WXSDKInstance * instance = [WXSDKInstance new]; instance.pageName = @\"your page name\" to fix it";
            [jsExceptionHandler onRuntimeCheckException:runtimeCheckException];
        }
    }
    if (!self.userInfo) {
        self.userInfo = [NSMutableDictionary new];
    }
    if (!self.userInfo[@"jsMainBundleStringContentLength"]) {
        self.userInfo[@"jsMainBundleStringContentLength"] = @([mainBundleString length]);
    }
    if (!self.userInfo[@"jsMainBundleStringContentLength"]) {
        self.userInfo[@"jsMainBundleStringContentMd5"] = [WXUtility md5:mainBundleString];
    }

    id<WXPageEventNotifyEventProtocol> pageEvent = [WXSDKEngine handlerForProtocol:@protocol(WXPageEventNotifyEventProtocol)];
    if ([pageEvent respondsToSelector:@selector(pageStart:)]) {
        [pageEvent pageStart:self.instanceId];
    }

    WX_MONITOR_INSTANCE_PERF_START(WXPTFirstScreenRender, self);
    WX_MONITOR_INSTANCE_PERF_START(WXPTAllRender, self);

    NSMutableDictionary *dictionary = [_options mutableCopy];
    if ([WXLog logLevel] >= WXLogLevelLog) {
        dictionary[@"debug"] = @(YES);
    }

    if ([WXDebugTool getReplacedBundleJS]) {
        mainBundleString = [WXDebugTool getReplacedBundleJS];
    }

    //TODO WXRootView
    WXPerformBlockOnMainThread(^{
        _rootView = [[WXRootView alloc] initWithFrame:self.frame];
        _rootView.instance = self;
        if(self.onCreate) {
            self.onCreate(_rootView);
        }
    });
    // ensure default modules/components/handlers are ready before create instance
    [WXSDKEngine registerDefaults];
     [[NSNotificationCenter defaultCenter] postNotificationName:WX_SDKINSTANCE_WILL_RENDER object:self];

    _needDestroy = YES;
    _mainBundleString = mainBundleString;
    if ([self _handleConfigCenter]) {
        NSError * error = [NSError errorWithDomain:WX_ERROR_DOMAIN code:9999 userInfo:nil];
        if (self.onFailed) {
            self.onFailed(error);
        }
        return;
    }

    _needDestroy = YES;
    [WXTracingManager startTracingWithInstanceId:self.instanceId ref:nil className:nil name:WXTExecJS phase:WXTracingBegin functionName:@"renderWithMainBundleString" options:@{@"threadName":WXTMainThread}];
    [[WXSDKManager bridgeMgr] createInstance:self.instanceId template:mainBundleString options:dictionary data:_jsData];
    [WXTracingManager startTracingWithInstanceId:self.instanceId ref:nil className:nil name:WXTExecJS phase:WXTracingEnd functionName:@"renderWithMainBundleString" options:@{@"threadName":WXTMainThread}];

    WX_MONITOR_PERF_SET(WXPTBundleSize, [mainBundleString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], self);
}
//WXBridgeProtocol.h

@protocol WXBridgeProtocol <NSObject>

@property (nonatomic, readonly) JSValue* exception;

/**
 * Executes the js framework code in javascript engine
 * You can do some setup in this method
 */
- (void)executeJSFramework:(NSString *)frameworkScript;

/**
 * Executes the js code in javascript engine
 * You can do some setup in this method
 */
- (void)executeJavascript:(NSString *)script;

/**
 * Executes global js method with specific arguments
 */
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray*)args;

/**
 * Register callback when call native tasks occur
 */
- (void)registerCallNative:(WXJSCallNative)callNative;

/**
 * Reset js engine environment, called when any environment variable is changed.
 */
- (void)resetEnvironment;
// WXJSCoreBridge.m

- (void)executeJSFramework:(NSString *)frameworkScript
{
    WXAssertParam(frameworkScript);
    if (WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
        NSString * fileName = @"native-bundle-main.js";
        if ([WXSDKManager sharedInstance].multiContext) {
            fileName = @"weex-main-jsfm.js";
        }
        [_jsContext evaluateScript:frameworkScript withSourceURL:[NSURL URLWithString:fileName]];
    }else{
        [_jsContext evaluateScript:frameworkScript];
    }
}

- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
    WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
    return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}

- (void)registerCallNative:(WXJSCallNative)callNative
{
    JSValue* (^callNativeBlock)(JSValue *, JSValue *, JSValue *) = ^JSValue*(JSValue *instance, JSValue *tasks, JSValue *callback){
        NSString *instanceId = [instance toString];
        NSArray *tasksArray = [tasks toArray];
        NSString *callbackId = [callback toString];
        WXLogDebug(@"Calling native... instance:%@, tasks:%@, callback:%@", instanceId, tasksArray, callbackId);
        return [JSValue valueWithInt32:(int32_t)callNative(instanceId, tasksArray, callbackId) inContext:[JSContext currentContext]];
    };

    _jsContext[@"callNative"] = callNativeBlock;
}
2019/1/19 posted in  移动开发

AppLink

屏幕快照 2017-09-13 下午2.09.46.png

2017/9/13 posted in  移动开发

网络相关

2017/6/23 posted in  移动开发

开发笔记4

2017/6/17 posted in  移动开发

开发笔记2

数据

看了下自己app的友盟统计,还有6%的iOS 8.x设备。等iOS 11出来后,再考虑放弃iOS 8.x用户吧。

OC稳住呀~

文章推荐(转)

最近看到Lottie项目,一个iOS,Android和React Native库,可以实时渲染After Effects动画,并且允许本地app像静态资源那样轻松地使用动画。回过头又把这篇图片格式调研的文章看了一遍,哈。这篇文章把移动开发常用的几种静态、动态图片做了详细的比较。

WKWebView不支持NSURLProtocol,导致基于UIWebView+NSURLProtocol并做了一些深度优化的,很难一下子就迁移到WKWebViewWKWebView内部使用了NSURLProtocol,但没有对外开放,不知道出于什么考虑。这篇文章以比较hack的方式让WKWebView支持NSURLProtocol

另一种Native动态化的方案,在OC编译器上动手脚,将OC代码自动编译成可动态下发的JS代码。

有些特性需要配合ios 10设备使用,比如内存循环引用、DYLD_PRINT_STATISTICS(打印应用预加载时间)等。

在Scheme的Environment Variables添加DYLD_PRINT_STATISTICS=YES,可以打印应用预加载时间。从打印出来的日志看,dylib loading time占用了不少时间,应该是跟加载的系统库和三方库有关。
The loading of Apple system frameworks is highly optimized but loading your embedded frameworks can be expensive. 当三方库不采用Framework形式打包,加载时间是少了不少。

// 三方库以Frameworks形式打包
Total pre-main time: 392.25 milliseconds (100.0%)
         dylib loading time: 336.73 milliseconds (85.8%)
        rebase/binding time:  10.88 milliseconds (2.7%)
            ObjC setup time:  15.91 milliseconds (4.0%)
           initializer time:  28.65 milliseconds (7.3%)
           slowest intializers :
             libSystem.B.dylib :   4.63 milliseconds (1.1%)
                  AFNetworking :  12.03 milliseconds (3.0%)
// 三方库以.a静态链接库形式链接
Total pre-main time:  93.71 milliseconds (100.0%)
         dylib loading time:  11.46 milliseconds (12.2%)
        rebase/binding time:  27.05 milliseconds (28.8%)
            ObjC setup time:  13.16 milliseconds (14.0%)
           initializer time:  41.94 milliseconds (44.7%)
           slowest intializers :
             libSystem.B.dylib :   5.47 milliseconds (5.8%)
   libBacktraceRecording.dylib :   4.48 milliseconds (4.7%)
                        Fanmei :  54.14 milliseconds (57.7%) 

项目

After Effects动画的渲染引擎。

A library for converting Adobe AE shape based animations to a data format and playing it back on Android and iOS devices.

Lottie类似,也是After Effects动画的渲染引擎。

A data-driven UICollectionView framework for building fast and flexible lists.

An in-app debugging and exploration tool for iOS

iOS设备调试工具,可以查看沙盒,抓包,查看视图信息等。

仿android的meterial design风格的控件

2017/6/16 posted in  移动开发

开发笔记3

数据

主要是针对Crash和ANR的统计。

文章推荐

监控先于优化。没有数据支撑的优化,往往没有说服力,也很难衡量优化的效果如何。

大部分App或多或少都会涉及到存储(持久化或缓存)。ibireme/YYCache,支持在硬盘持久化和在内存中缓存,从功能、性能和接口易用性上都不错,目前项目中也在用,推荐。

老文章了,视图布局机制在iOS、Android、Html CSS是相通的,可以互相借鉴。youngsoft/MyLinearLayout实现很多布局机制。

MyLinearLayout is equivalent to: UIStackView of iOS and LinearLayout of Android.

MyRelativeLayout is equivalent to: AutoLayout of iOS and RelativeLayout of Android.

MyFrameLayout is equivalent to: FrameLayout of Android.

MyTableLayout is equivalent to: TableLayout of Android and table of HTML.

MyFlowLayout is equivalent to: flexbox of CSS3.

MyFloatLayout is equivalent to: float of CSS.

借鉴Android的LinearLayout以及Html CSS的FlexBox,自己写了FMLayouts,对iOS的布局机制做补充。

Android

项目

SnapKit/Masonry的Swift版本

2017/6/16 posted in  移动开发

开发笔记1

文章推荐

徐川大神对2016年移动技术发展的概述,以及对2017年的展望。总结了各种技术,热补丁、组件化、动态化、跨平台、直播、AR/VR等等。自己在项目实践中,也尝试了热补丁、动态化、组件化等技术,比较有体会。推荐他的博客移动开发前线,还是相当不错的。

对于往往存在大量异构元素的app首页或集合页,该方案的思路还是可以借鉴下的。

为满足大部分动态化需求,在App中内嵌一个WebView支持H5页,这是比较常见的做法。iOS上就UIWebView和WKWebView,WKWebView相比较UIWebView,性能更优,但也多了不少限制。目前自己项目中也是直接使用WKWebView,好在H5和Native的交互比较少,对WKWebView也没有做多少优化。

异步编程,特别是多层嵌套的情况,即使用了GCD&Block回调方式去写,代码仍会过于凌乱,可读性也大打折扣。我会通过以下两种方法去处理,将每个异步回调的处理逻辑封装在一个函数内,或将每个异步过程封装起来(即Future/Promise的做法)。无论哪种,也都会额外增加些成本,结合自己项目内的代码风格和使用成本再选择一个合适的。

知识点比较基础,罗列得比较多,可以结合项目多看看。

代码推荐

收集了大量的iOS开源项目、网页等等

2017/1/24 posted in  移动开发

移动安全

安全检查项

  • 移动App上关键数据(密码、个人信息等)在存储、传输等各个阶段需要做一定的安全保护。下图罗列了常见的检查项。 app_security_checks

传输安全

  • 保密性,消息是加密的,内容没有泄露,可以用对称加密算法进行加解密,如AES256。

  • 完整性,消息是完整的,内容没有被篡改,对消息做哈希得到摘要,作为消息的签名。哈希算法有SHA256等,MD5、SHA1已经不太安全,尽量用SHA256等相对安全的算法。

  • 真实性,消息是可靠的,内容来自受信的来源,验证来源的数字证书是否可靠。

常见工具

  • class-dump-z
    一个跨平台的 Objective-C 接口提取器,用于分析 iPhoneOS 可执行文件中私有的 API。需 通过 Cydia 安装。

  • darwin cc tools (otools) 一个开源的苹果程序编译和连接器。需通过 Cydia 安装。

  • HTTP代理抓包工具(Fiddler)

  • Plist文件查看器(plistEditor)

  • iOS内存修改工具(iGameGuard)

  • 内购破解工具(LocalIAPStore、iAPFree)

  • keychain-dumper
    该工具可以读取已越狱 IOS 设备中的 keychain。需通过 Cydia 安装。

  • Link Identity Editor (ldid)
    该工具可以协助测试人员修改 Mach-O 二进制文件的签名信息。需通过 Cydia 安装。

  • OpenSSH
    OpenSSH 是 Linux 下常用的服务,装上后设备可充当 SSH 服务端。需通过 Cydia 安装。

  • Snoop-it
    IOS APP 安全评估工具。可对 APP 进行静态、动态分析。需通过 Cydia 安装。

相关博文

2016/5/4 posted in  移动开发

移动三方应用分享

iOS原生分享控件

    NSString *textToShare = @"要分享的文本内容";
    UIImage *imageToShare = [UIImage imageNamed:@"shop"];
    NSURL *urlToShare = [NSURL URLWithString:@"http://blog.csdn.net/hitwhylz"];
    NSArray *activityItems = @[textToShare, imageToShare, urlToShare];
    
    UIActivityViewController *vc = [[UIActivityViewController alloc]initWithActivityItems:activityItems applicationActivities:nil];
    [self presentViewController:vc animated:YES completion:nil];

三方开放平台

URL Schemes

  • iOS应用间可以通过URL Schemes解决相互通信的问题。
  • DemoA通过openURL唤起DemoB,并带上参数。
DemoA

[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"demob://page?sourcescheme=demoa"]];
  • DemoB收到并解析参数并作逻辑处理,然后同样通过openURL返回到DemoA,并带上参数。
DemoB

- (BOOL)handleUrl:(NSURL *)url {
    NSArray *querys = [url.query componentsSeparatedByString:@"&"];
    
    NSMutableDictionary *queryDict = [[NSMutableDictionary alloc] init];
    for (NSString *q in querys) {
        NSArray *kv = [q componentsSeparatedByString:@"="];
        if (kv.count == 2) {
            queryDict[kv[0]] = kv[1];
        }
    }
    
    NSString *returnScheme = queryDict[@"sourcescheme"];
    
    UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"url"
                                                                message:url.absoluteString
                                                         preferredStyle:UIAlertControllerStyleAlert];
    [vc addAction:[UIAlertAction actionWithTitle:@"返回原应用"
                                           style:UIAlertActionStyleDefault
                                         handler:^(UIAlertAction * _Nonnull action) {
                                             [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@://", returnScheme]]];
                                         }]];
    
    [vc addAction:[UIAlertAction actionWithTitle:@"留在当前应用"
                                           style:UIAlertActionStyleCancel
                                         handler:^(UIAlertAction * _Nonnull action) {
                                         }]];
    
    [self.window.rootViewController presentViewController:vc animated:YES completion:nil];
    
    return YES;
}

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url NS_DEPRECATED_IOS(2_0, 9_0, "Please use application:openURL:options:") __TVOS_PROHIBITED {
    return [self handleUrl:url];
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation NS_DEPRECATED_IOS(4_2, 9_0, "Please use application:openURL:options:") __TVOS_PROHIBITED {
    return [self handleUrl:url];
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options NS_AVAILABLE_IOS(9_0) {
    return [self handleUrl:url];

}

urlschemedemo

iosappsinteraction

三方分享或认证

  • 三方应用分享或认证都会用到URL Schemes流程,实际过程和参数会更复杂点,还需要考虑安全性。
  • 如微信三方登录认证,除了三方应用通过URL Scheme跳转微信拿到code,再通过https请求微信后台拿到access_token,后续用https带上access_token可以请求各种资源。
    weixinlogin

  • 三方分享则是通过URL Schemes带上不同格式的数据(文本、图片、链接等)。

2016/5/3 posted in  移动开发

Deep Linking

2016/4/12 posted in  移动开发