基于 Objective-C 实现的框架设计,YTKNetwork网络层 + AOP替代基类 + MVVM + ReactiveObjC + JLRoutes路由

我理解的框架,就好比计算机的主板,房屋的建筑骨架,道路的基础设施配套,框架搭的好,能直接影响开发者的开发心情,更能让项目健壮性和扩展性大大增强。


? 要求

  • iOS 8.0+
  • Xcode 8.0+
  • Objective-C

? 测试 UI 什么样子?

1.展示页 2.展示页 3.展示页 4.说明页
登录视图 示例展示 跳转页面 介绍页面

? 安装方法

安装

iOS, 你需要在 Podfile 中添加.

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!# 提示组件框架pod 'SVProgressHUD', '~> 2.2.2'# 网络请求框架pod 'YTKNetwork', '~> 2.0.3'# AOP面向切面pod 'Aspects', '~> 1.4.1'# 响应函数式框架pod 'ReactiveObjC', '~> 3.0.0'# 路由组件化解耦pod 'JLRoutes', '~> 2.0.5'# 提示组件框架pod 'SVProgressHUD', '~> 2.2.2'# 自动布局pod 'Masonry', '~> 1.0.2'
复制代码

? 框架介绍

1.AOP 模式(Aspects-RunTime 代替基类)+ Category 方法交换

采用AOP思想,使用 Aspects 来完成替换 Controller ,View,ViewModel基类,和基类说拜拜

Casa反革命工程师 iOS应用架构谈 view层的组织和调用方案 博客中提到一个疑问 是否有必要让业务方统一派生ViewController

Casa大神回答是NO,原因如下

  1. 使用派生比不使用派生更容易增加业务方的使用成本
  2. 不使用派生手段一样也能达到统一设置的目的 对于第一点,从 集成成本 ,上手成本 ,架构维护成本等因素入手,大神博客中也已经很详细。

框架不需要通过继承即能够对ViewController进行统一配置。业务即使脱离环境,也能够跑完代码,ViewController一旦放入框架环境,不需要添加额外的或者只需添加少量代码,框架也能够起到相应的作用 对于本人来说 ,具备这点的吸引力,已经足够让我有尝试一番的心思了。

对于OC来说,方法拦截很容易就想到自带的黑魔法方法调配 Method Swizzling, 至于为ViewController做动态配置,自然非Category莫属了 Method Swizzling 业界已经有非常成熟的三方库 Aspects, 所以Demo代码采用 Aspects 做方法拦截。

+ (void)load {[super load];[FKViewControllerIntercepter sharedInstance];
}
// .... 单例初始化代码- (instancetype)init {self = [super init];if (self) {/* 方法拦截 */// 拦截 viewDidLoad 方法[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo>aspectInfo){[self _viewDidLoad:aspectInfo.instance];}  error:nil];// 拦截 viewWillAppear:[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated){[self _viewWillAppear:animated controller:aspectInfo.instance];} error:NULL];}return self;
}
复制代码

至于 Category 已经非常熟悉了

@interface UIViewController (NonBase)/**去Model&&表征化参数列表*/
@property (nonatomic, strong) NSDictionary *params;/**ViewModel 属性*/
@property (nonatomic, strong) id <FKViewControllerProtocol> viewModel;#pragma mark - 通用类/**返回Controller的当前bounds@param hasNav 是否有导航栏@param hasTabBar 是否有tabbar@return 坐标*/
- (CGRect)fk_visibleBoundsShowNav:(BOOL)hasNav showTabBar:(BOOL)hasTabBar;/**隐藏键盘*/
- (void)fk_hideKeyBoard;
@end
复制代码

至此,我们已经实现了不继承基类来实现对ViewController的配置,项目中的 View ViewModel 去基类原理如出一辙。

2.View层采用 MVVM 设计模式,使用 ReactiveObjC 进行数据绑定

-MVC-

作为老牌思想MVC,大家早已耳熟能详,MVC素有 Massive VC之称,随着业务增加,Controller将会越来越复杂,最终Controller会变成一个"神类", 即有网络请求等代码,又充斥着大量业务逻辑,所以为Controller减负,在某些情况下变得势在必行

-MVVM-

MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel (注:胖Model 是指包含了一些弱业务逻辑的Model) 胖Model实际上是为了减负 Controller 而存在的,而 MVVM 是为了拆分胖Model , 最终目的都是为了减负Controller。

我们知道,苹果MVC并没有专门为网络层代码分专门的层级,按照以往习惯,大家都写在了Controller 中,这也是Controller 变Massive得元凶之一,现在我们可以将网络请求等诸如此类的代码放到ViewModel中了 (文章后半部分将会描述ViewModel中的网络请求)

-数据流向-

正常的网络请求获取数据,然后更新View自然不必多说,那么如果View产生了数据要怎么把数据给到Model,由于View不直接持有ViewModel,所以我们需要有个桥梁 ReactiveCocoa, 通过 Signal 来和 ViewModel 通信,这个过程我们使用 通知 或者 Target-Action也可以实现相同的效果,只不过没有 ReactiveCocoa 如此方便罢了

/*  View -> ViewModel 传递数据示例   */
#pragma mark - Bind ViewModel
- (void)bindViewModel:(id<FKViewModelProtocol>)viewModel withParams:(NSDictionary *)params {if ([viewModel isKindOfClass:[FKLoginViewModel class]]){FKLoginViewModel *_viewModel = (FKLoginViewModel *)viewModel;// 绑定账号 View -> ViewModel 传递数据 @weakify(self);RAC(_viewModel, userAccount) = [[self.inputTextFiled.rac_textSignal takeUntil:self.rac_prepareForReuseSignal] map:^id _Nullable(NSString * _Nullable account) {@strongify(self);// 限制账号长度if (account.length > 25) {self.inputTextFiled.text = [account substringToIndex:25];}return self.inputTextFiled.text;}];}
}
复制代码

上面代码给出了 View -> ViewModel 绑定的一个例子 具体一些详情,可以直接看Demo MVVM一些总结:

  1. View <-> C <-> ViewModel <-> Model 实际上应该称之为MVCVM
  2. Controller 将不再直接和 Model 进行绑定,而通过桥梁ViewModel
  3. 最终 Controller 的作用变成一些UI的处理逻辑,和进行View和ViewModel的绑定
  4. MVVM 和 MVC 兼容
  5. 由于多了一层 ViewModel, 会需要写一些胶水代码,所以代码量会增加

3.网络层使用 YTKNetwork 配合 ReactiveCocoa 封装网络请求,解决如何交付数据,交付什么样的数据(去Model化)等问题

YTKNetwork 是猿题库 iOS 研发团队基于 AFNetworking 封装的 iOS 网络库,其实现了一套 High Level 的 API,提供了更高层次的网络访问抽象。

笔者对 YTKNetwork 进行了一些封装,结合 ReactiveCocoa,并提供 reFormatter 接口对服务器响应数据重新处理,灵活交付给业务层。 接下来,本文会回答两个问题

  1. 以什么方式将数据交付给业务层?
  2. 交付什么样的数据 ? 对于第一个问题

以什么方式将数据交付给业务层?

虽然 iOS应用架构谈 网络层设计方案 中 Casa大神写到 尽量不要用block,应该使用代理 的确,Block难以追踪和定位错误,容易内存泄漏, YTKNetwork 也提供代理方式回调

@protocol YTKRequestDelegate <NSObject>@optional
///  Tell the delegate that the request has finished successfully.
///
///  @param request The corresponding request.
- (void)requestFinished:(__kindof YTKBaseRequest *)request;///  Tell the delegate that the request has failed.
///
///  @param request The corresponding request.
- (void)requestFailed:(__kindof YTKBaseRequest *)request;@end
复制代码

前文有说过,MVVM 并不等于 ReactiveCocoa , 但是想要体验最纯正的 ReactiveCocoa 还是Block较为酸爽,Demo中笔者两者都给出了代码, 大家可以自行选择和斟酌哈 我们看一下 YTKNetwork 和 ReactiveCocoa 结合的代码

- (RACSignal *)rac_requestSignal {[self stop];RACSignal *signal = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {// 请求起飞[self startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest * _Nonnull request) {// 成功回调[subscriber sendNext:[request responseJSONObject]];[subscriber sendCompleted];} failure:^(__kindof YTKBaseRequest * _Nonnull request) {// 错误回调[subscriber sendError:[request error]];}];return [RACDisposable disposableWithBlock:^{// Signal销毁 停止请求[self stop];}];}] takeUntil:[self rac_willDeallocSignal]];//设置名称 便于调试if (DEBUG) {[signal setNameWithFormat:@"%@ -rac_xzwRequest",  RACDescription(self)];}return signal;
}
复制代码

写了一个简单的 Category FKBaseRequest+Rac.h ViewModel 中使用 RACCommand 封装调用:

- (RACCommand *)loginCommand {if (!_loginCommand) {@weakify(self);_loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {@strongify(self);return [[[FKLoginRequest alloc] initWithUsr:self.userAccount pwd:self.password] rac_requestSignal];}];}return _loginCommand;
}
复制代码

Block方式交付业务

FKLoginRequest *loginRequest = [[FKLoginRequest alloc] initWithUsr:self.userAccount pwd:self.password];
return [[[loginRequest rac_requestSignal] doNext:^(id  _Nullable x) {// 解析数据[[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:@"isLogin"];}] materialize];
复制代码

Delegate方式交付业务

FKLoginRequest *loginRequest = [[FKLoginRequest alloc] initWithUsr:self.userAccount pwd:self.password];
// 数据请求响应代理 通过代理回调
loginRequest.delegate = self;
return [loginRequest rac_requestSignal];#pragma mark - YTKRequestDelegate
- (void)requestFinished:(__kindof YTKBaseRequest *)request {// 解析数据[[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:@"isLogin"];
}
复制代码

交付什么样的数据 ?

现在诸如 JSONModel ,YYModel 之类的Json转Model的库也非常多,大多数Json对象,网络请求成功直接就被转成Model了 然而 iOS应用架构谈 网络层设计方案 中给出了两种有意思的交付思路

  1. 使用 reformer 对数据进行清洗
  2. 去特定对象表征 (去Model)

Casa文章中好处已经写得很详细了,通过不同的 reformer 来重塑和交付不同的业务数据,可以说是非常灵活了

使用 reformer 对数据进行清洗

在网络层封装 FKBaseRequest.h 中 给出了 FKBaseRequestFeformDelegate 接口来重塑数据

@protocol FKBaseRequestFeformDelegate <NSObject>/**自定义解析器解析响应参数@param request 当前请求@param jsonResponse 响应数据@return 自定reformat数据*/
- (id)request:(FKBaseRequest *)request reformJSONResponse:(id)jsonResponse;@end
然后在对应的 reformer 对数据进行重塑
#pragma mark - FKBaseRequestFeformDelegate
- (id)request:(FKBaseRequest *)request reformJSONResponse:(id)jsonResponse {if([request isKindOfClass:FKLoginRequest.class]){// 在这里对json数据进行重新格式化}return jsonResponse;
}
复制代码

也可以直接在子类的 RequestManager 中覆盖父类方法达到一样的效果

/* FKLoginRequest.m */// 可以在这里对response 数据进行重新格式化, 也可以使用delegate 设置 reformattor
- (id)reformJSONResponse:(id)jsonResponse {
}
复制代码

去特定对象表征 (去Model)

这思路可以说是业界的泥石流了 去Model也就是说,使用NSDictionary形式交付数据,对于网络层而言,只需要保持住原始数据即可,不需要主动转化成数据原型 但是会存在一些小问题

  1. 去Model如何保持可读性?
  2. 复杂和多样的数据结构如何解析?

Casa大神 提出了 使用EXTERN + Const 字符串形式,并建议字符串跟着reformer走,个人觉得很多时候API只需要一种解析格式,所以Demo跟着 APIManager 走,其他情况下常量字符串建议听从 Casa大神 的建议, 常量定义:

/* FKBaseRequest.h */
// 登录token key
FOUNDATION_EXTERN NSString *FKLoginAccessTokenKey;/* FKBaseRequest.m */
NSString *FKLoginAccessTokenKey = @"accessToken";
复制代码

在 .h 和 .m 文件中要同时写太多代码,我们也可以使用局部常量的形式,只要在 .h 文件中定义即可

// 也可以写成 局部常量形式
static const NSString *FKLoginAccessTokenKey2 = @"accessToken";
最终那么我们的reformer可能会变成这样子
- (id)request:(FKBaseRequest *)request reformJSONResponse:(id)jsonResponse {if([request isKindOfClass:FKLoginRequest.class]){// 在这里对json数据进行重新格式化return @{FKLoginAccessTokenKey : jsonResponse[@"token"],};}return jsonResponse;
}
复制代码

复杂和多样的数据结构如何解析? 有时候,reformer 交付过来的数据,我们需要解析的可能是字符串类型,也可能是NSNumber类型,也有可能是数组 为此,笔者提供了一系列 Encode Decode方法,来降低解析的复杂度和安全性

#pragma mark - Encode Decode 方法
// NSDictionary -> NSString
FK_EXTERN NSString* DecodeObjectFromDic(NSDictionary *dic, NSString *key);
// NSArray + index -> id
FK_EXTERN id        DecodeSafeObjectAtIndex(NSArray *arr, NSInteger index);
// NSDictionary -> NSString
FK_EXTERN NSString     * DecodeStringFromDic(NSDictionary *dic, NSString *key);
// NSDictionary -> NSString ? NSString : defaultStr
FK_EXTERN NSString* DecodeDefaultStrFromDic(NSDictionary *dic, NSString *key,NSString * defaultStr);
// NSDictionary -> NSNumber
FK_EXTERN NSNumber     * DecodeNumberFromDic(NSDictionary *dic, NSString *key);
// NSDictionary -> NSDictionary
FK_EXTERN NSDictionary *DecodeDicFromDic(NSDictionary *dic, NSString *key);
// NSDictionary -> NSArray
FK_EXTERN NSArray      *DecodeArrayFromDic(NSDictionary *dic, NSString *key);
FK_EXTERN NSArray      *DecodeArrayFromDicUsingParseBlock(NSDictionary *dic, NSString *key, id(^parseBlock)(NSDictionary *innerDic));#pragma mark - Encode Decode 方法
// (nonull Key: nonull NSString) -> NSMutableDictionary
FK_EXTERN void EncodeUnEmptyStrObjctToDic(NSMutableDictionary *dic,NSString *object, NSString *key);
// nonull objec -> NSMutableArray
FK_EXTERN void EncodeUnEmptyObjctToArray(NSMutableArray *arr,id object);
// (nonull (Key ? key : defaultStr) : nonull Value) -> NSMutableDictionary
FK_EXTERN void EncodeDefaultStrObjctToDic(NSMutableDictionary *dic,NSString *object, NSString *key,NSString * defaultStr);
// (nonull Key: nonull object) -> NSMutableDictionary
FK_EXTERN void EncodeUnEmptyObjctToDic(NSMutableDictionary *dic,NSObject *object, NSString *key);
复制代码

我们的reformer可以写成这样子

#pragma mark - FKBaseRequestFeformDelegate
- (id)request:(FKBaseRequest *)request reformJSONResponse:(id)jsonResponse {if([request isKindOfClass:FKLoginRequest.class]){// 在这里对json数据进行重新格式化return @{FKLoginAccessTokenKey : DecodeStringFromDic(jsonResponse, @"token")};}return jsonResponse;
}
复制代码

解析有可能是这样子

NSString *token = DecodeStringFromDic(jsonResponse, FKLoginAccessTokenKey)
复制代码

好了,至此我们解决了两个问题

  1. 以什么方式将数据交付给业务层 答:delegate 最佳,block为次
  2. 交付什么样的数据 答:纯字典,去Model

4.采用 JLRoutes 路由对应用进行组件化解耦

带着问题思考如何才能设计出最好的组件化路由:

  • 1)3D-Touch功能或者点击推送消息,要求外部跳转到App内部一个很深层次的一个界面。
  • 2)自家的一系列App之间如何相互跳转?
  • 3)如何解除App组件之间和App页面之间的耦合性?
  • 4)如何能统一iOS和Android两端的页面跳转逻辑?甚至如何能统一三端的请求资源的方式?
  • 5)如果使用了动态下发配置文件来配置App的跳转逻辑,那么如果做到iOS和Android两边只要共用一套配置文件?
  • 6)如果App出现bug了,如何不用JSPatch,就能做到简单的热修复功能?
  • 7)如何在每个组件间调用和页面跳转时都进行埋点统计?每个跳转的地方都手写代码埋点?利用Runtime AOP ?
  • 8)如何在每个组件间调用的过程中,加入调用的逻辑检查,令牌机制,配合灰度进行风控逻辑?
  • 9)如何在App任何界面都可以调用同一个界面或者同一个组件?只能在AppDelegate里面注册单例来实现?

iOS应用架构谈 组件化方案 一文中 Casa 针对 蘑菇街组件化 提出了质疑,质疑点主要在这几方面

  1. App启动时组件需要注册URL
  2. URL调用组件方式不太好传递类似 UIImage 等非常规对象
  3. URL需要添加额外参数可读性差,所以没必要使用URL

对于 App启动时组件需要注册URL 顾虑主要在于,注册的URL需要在应用生存周期内常驻内存,如果是注册Class还好些,如果注册的是实例,消耗的内存就非常可观了

#pragma mark - 路由表
NSString *const FKNavPushRoute = @"/com_madao_navPush/:viewController";
NSString *const FKNavPresentRoute = @"/com_madao_navPresent/:viewController";
NSString *const FKNavStoryBoardPushRoute = @"/com_madao_navStoryboardPush/:viewController";
NSString *const FKComponentsCallBackRoute = @"/com_madao_callBack/*";
复制代码

而且JLRoutes 还支持 * 来进行通配,路由表如何编写大家可以自由发挥 对应的路由事件 handler

// push
// 路由 /com_madao_navPush/:viewController
[[JLRoutes globalRoutes] addRoute:FKNavPushRoute handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {dispatch_async(dispatch_get_main_queue(), ^{[self _handlerSceneWithPresent:NO parameters:parameters];});return YES;
}];// present
// 路由 /com_madao_navPresent/:viewController
[[JLRoutes globalRoutes] addRoute:FKNavPresentRoute handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {dispatch_async(dispatch_get_main_queue(), ^{[self _handlerSceneWithPresent:YES parameters:parameters];});return YES;
}];#pragma mark - Private
/// 处理跳转事件
- (void)_handlerSceneWithPresent:(BOOL)isPresent parameters:(NSDictionary *)parameters {// 当前控制器NSString *controllerName = [parameters objectForKey:FKControllerNameRouteParam];UIViewController *currentVC = [self _currentViewController];UIViewController *toVC = [[NSClassFromString(controllerName) alloc] init];toVC.params = parameters;if (currentVC && currentVC.navigationController) {if (isPresent) {[currentVC.navigationController presentViewController:toVC animated:YES completion:nil];}else{[currentVC.navigationController pushViewController:toVC animated:YES];}}
}
复制代码

通过URL中传入的组件名动态注册,处理相应跳转事件,并不需要每个组件一一注册 使用URL路由,必然URL会散落到代码各个地方

NSString *key = @"key";
NSString *value = @"value";
NSString *url = [NSString stringWithFormat:@"/com_madao_navPush/%@?%@=%@", NSStringFromClass(ViewController.class), key, value];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
复制代码

诸如此类丑陋的代码,散落在各个地方的话简直会让人头皮发麻, 所以笔者在 JLRoutes+GenerateURL.h 写了一些 Helper方法

/**避免 URL 散落各处, 集中生成URL@param pattern 匹配模式@param parameters 附带参数@return URL字符串*/
+ (NSString *)fk_generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters;/**避免 URL 散落各处, 集中生成URL额外参数将被 ?key=value&key2=value2 样式给出@param pattern 匹配模式@param parameters 附加参数@param extraParameters 额外参数@return URL字符串*/
+ (NSString *)fk_generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters extraParameters:(NSDictionary *)extraParameters;/**解析NSURL对象中的请求参数
http://madao?param1=value1¶m2=value2 解析成 @{param1:value1, param2:value2}@param URL NSURL对象@return URL字符串*/
+ (NSDictionary *)fk_parseParamsWithURL:(NSURL *)URL;/**将参数对象进行url编码将@{param1:value1, param2:value2} 转换成 ?param1=value1&param2=value2@param dic 参数对象@return URL字符串*/
+ (NSString *)fk_mapDictionaryToURLQueryString:(NSDictionary *)dic;
复制代码

宏定义Helper

#undef JLRGenRoute
#define JLRGenRoute(Schema, path) \
([NSString stringWithFormat: @"%@:/%@", \
Schema, \
path])#undef JLRGenRouteURL
#define JLRGenRouteURL(Schema, path) \
([NSURL URLWithString: \
JLRGenRoute(Schema, path)])
复制代码

最终我们的调用可以变成

NSString *router = [JLRoutes fk_generateURLWithPattern:FKNavPushRoute parameters:@[NSStringFromClass(ViewController.class)] extraParameters:nil];
[[UIApplication sharedApplication] openURL:JLRGenRouteURL(FKDefaultRouteSchema, router)];
复制代码

? 整理制作

Casa Taloyum:https://casatwy.com/modulization_in_action.html

简书博客:http://www.jianshu.com/p/921dd65e79cb


? 联系

  • 微信 : WhatsXie
  • 邮件 : ReverseScale@iCloud.com
  • 博客 : https://reversescale.github.io
  • 源码 : https://github.com/ReverseScale/OCTemplate

走在技术前沿的 iOS 架构实现相关推荐

  1. 走在技术前沿,今年最火的top5 +Java开源项目

    首先看一张编程语言图谱,了解一下 Java 最近20 年到底有多火. 从趋势图可以看出,Java 在最近 20 年里一直处于前三的位置,可见受欢迎的程度还是很高的. 1. CS-Notes 2. ad ...

  2. 融云 CTO 岑裕:出海技术前沿探索和排「坑」实践

    在本文中,你将看到以下内容: 全球通信网络在接入点.链路加速.服务商.协议等层面的动态演进: 进入到具体市场,禁运国.跨国拦截.区域一致性差等细节"坑点"如何应对: 融云如何从技术 ...

  3. Samtec技术前沿 | 全新电缆系统提升了热管理并延长了信号覆盖范围

    [摘要/前言] 一种全新高速.高密度电缆系统扩展了信号覆盖范围,以实现下一代的速度,并改善了热管理. Samtec的高速电缆产品经理Andy Shrout向我们展示了这个最初在DesignCon 20 ...

  4. 费爱国院士:中国城市大脑已走在世界前沿,但仍需努力

    信息来源:网易科技 2022年9月1日,中国指挥与控制学会在京召开<城市大脑首批标准新闻发布会>正式发布<城市大脑 术语>.<城市大脑顶层规划和总体架构>:< ...

  5. 喜马拉雅 FM--- [ Java 高级开发] [ Java 架构师] [iOS 架构师] 招聘啦

    喜马拉雅 FM 是中国知名音频平台,拥有超过 4.7 亿手机用户,3500 万海外用户和 3000 万车载.穿戴.音响智能设备用户.上线至今始终位居 APPSTORE 图书榜前列,市场表现持续火爆,目 ...

  6. 喜马拉雅 FM--- [ Java 高级开发] [ Java 架构师] [iOS 架构师] 招聘中~

    喜马拉雅 FM 是中国知名音频平台,拥有超过 4.5 亿手机用户,3500 万海外用户和 3000 万车载.穿戴.音响智能设备用户.上线至今始终位居 APPSTORE 图书榜前列,市场表现持续火爆,目 ...

  7. iOS架构-静态库.framework之依赖第三方库(7)

    需求分析:     把自己的能力封装成静态库提供给客户使用,但是有些工作市场上已经有很好的公开的代码,或者成本很低的解决方案,我们就可以使用别人公开的.或者低成本采购的技术来为我们服务.制作静态库也是 ...

  8. 聚焦AI落地痛点,纵论跨域学习技术前沿和应用趋势 | CNCC技术论坛

    <AI落地的跨域学习技术和进展>技术论坛将于CNCC期间,10月24日下午16:00-18:00,在北京新世纪日航饭店2层江苏厅举行.本论坛邀请跨域学习领域.学术界的顶尖学者和工业界的领军 ...

  9. 亿级商品详情页架构演进技术解密 | 高可用架构系列

    亿级商品详情页架构演进技术解密 | 高可用架构系列 --http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=210272034&i ...

最新文章

  1. 每日一皮:你不知道你的骑手为了给你送餐要经历什么...
  2. PhpMyAdmin导入数据库大小限制?
  3. MatLab基础操作
  4. 4由通道检测_大唐阜新煤制天然气「榜样力量」实训做实出实效——废水总酚检测时间由4小时缩短至10分钟...
  5. vue 转为静态html,Vue CLI 3使用:HTML和静态资源(五)
  6. 实现京东金融-悬浮框效果
  7. URL编码 java
  8. UVA434 Matty‘s Blocks【贪心】
  9. 游戏筑基开发之双链表及其基本功能(C语言)
  10. laravel-excel文档翻译笔记
  11. 水果电商网站开发过程
  12. Markdown编辑器使用-yellowcong
  13. labview与android,LabVIEW与Android手机的无线视频实时传输
  14. 土豆网(第三方网站)使用qq登录的步骤和原理------oAuth协议
  15. 股票交易接口有什么优势?
  16. NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE翻译
  17. 文墨绘学呵护那一点点光
  18. left函数未定义_access中LEFT函数未定义的解决方案\表达式中'left'函数未定义。
  19. Html设置表格撑开,【CSS】表格或div被撑开的解决办法
  20. Android6.0 按键流程(七)无线鼠标右键无效 -- Framework层

热门文章

  1. Winform中使用控件的Dock属性设计窗体布局,使不随窗体缩放而改变
  2. Jquery中实现表单提交到SSM后台前进行post请求实现数据的校验
  3. Ajax请求SSM后台时提示:Invalid character found in the request target. The valid characters are defined in RF
  4. 爬虫进行request请求时User-Agent怎样写
  5. 【爬虫笔记】Scrapy爬虫技术文章网站
  6. 一维数组中的一些问题
  7. 618 兵临城下,你需要一个更省钱省力的数据根基平台!
  8. 你的 A/B 测试数据期骗你了吗?
  9. 实现一个vue的图片预览插件
  10. Commons BeanUtils包学习2