当前位置: 首页 > 编程日记 > 正文

仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装

程序框架确定了,还需要封装网络模块。

一个丰富多彩的APP少不了网络资源的支持,毕竟用户数据要存储,用户之间也要交互,用户行为要统计等等。

使用开源框架

俗话说得好,轮子多了路好走,我们不需要自己造轮子,拿来主义就行了。

android网络模块核心功能使用xUtils3开源框架来完成。

而iOS则使用AFNetWorking,别告诉我你没听说过AFNetworking。

xUtils3拥有4大功能:数据库,视图注解,网络,图片(支持webp)。

AFNetWorking则包含网络和图片2部分。

我们只需要用到其中的网络模块和图片缓存模块。

Model(Record)封装:

《App研发录》中强烈要求把后台返回的json数据转换成类实例Record(有些人喜欢称为model,即:MVC中的M,而个人习惯称之为Record,而Model的使用我更倾向于可共享可本地化的全局单例)类。在业务逻辑中使用的是这些类实例化后的对象。

这样做的好处有3个:

  • 不易出错。JSONObject对象操作起来有点麻烦,比如:每次需要使用has方法来判断某些值是否存在,如果不判断,而这些值恰好不存在,则会崩溃。更重要的是,需要使用字符串来做键,写错了也是没有编译器提示的。
  • 数据传递更为容易。页面间,对象间传递数据可直接传递Record对象,更加具有可读性,也更高效。如果传递JSONObject则需要再次解析。从而造成同一个数据多次解析。
  • 代码更加规范。可以把Json转Record封装到网络层,从而在使用者看来,网络请求回来的数据就是Record。这样更容易让不同的程序员做更少的事情,从而写出尽量类似的代码。

为了达到上述目的,我们需要再次引入一个第三方库,来自Google的Gson,如何引入及如何调用请另行查询,它的作用就是把json字符串转换成本地类对象。

iOS则需要引入另一个第三方库,MJExtension。这个库作用同Android的Gson,但是相对android来说,它更加强大,更加易用。使用MJExtension的方法请见官方Demo。

然后我们需要建立一个基类BaseRecord来表示网络数据的基类,它是一个空的类,实现了Serializable这个接口,目的是让它可以通过Intent传递,也可以方便的本地化(把对象写入到硬盘)。

//android:
//BaseRecord.java
public class BaseRecord implements Serializable{
}       
//iOS:
//BaseRecord.h
@interface BaseRecord: NSObject
@end
//BaseRecord.m
@implements BaseRecord
@end    

后续所有的表示服务端返回的数据都需要继承BaseRecord这个类,这样写在设计模式中对应的说法是:里氏替换。

至于具体的record如何写,如何使用Gson进行绑定,下面代码中有部分内容,更多细节请自行查询资料。

这里提供一个json自动转java类的网址作为参考。

ServerBinder的封装:

为了达到上述目的,让使用者用最简单的方法就能够获取到网络资源,我们需要封装一个类,ServerBinder。

ServerBinder是一个单例,它需要用户输入后台接口的名字后,然后输出一个对应的存储了所有返回的服务端数据的Record。

ServerBinder中需要这样一个方法:regist,表示注册某个接口,只有在ServerBinder中注册过的服务端接口,留下了必要信息,后续才能够调用。

我们需要分析一下服务端调用地址的构成,来决定此方法的传入参数:
服务端接口往往是这样的,http://xxx.com/api/user_info?id=1000
其中可变的部分为:

  • http://xxx.com:表示服务器地址
  • api:服务端入口
  • user_info:接口名,
  • ?后面表示参数。

这样,我们的regist函数包括5个参数:网址,服务端入口,接口名,接口类型(get还是post),还有返回的record的类型。此函数需要做到,把地址,入口,方法名,record类型 存储起来。存储的数据需以方法名为键。此方法全局只需调用一次。

以方法名为键的原因是:对于服务端来说,同一个方法名对应的数据格式是相同的。

我们还需要一个方法:call,来表示调用此接口,可以在任何需要网络数据的时候调用它。

call方法需要3个参数,方法名,参数列表,还有回调函数(实现为一个内部接口,供调用者实现,类似观察者模式,但是这个观察者寿命比较短,只能观察一次)。

用户调用call方法时,所需要的数据都有了。返回的数据需要在真正的服务端回调中处理,把json转成record,然后把结果交给上面说的观察者即可。

另外每次服务端数据返回,都会带有当前服务器时间,因此客户端需要做时间校正:令app客户端每次获取的时间都是服务器时间,避免用户修改设置里面的手机时间,导致app内时间错误。

好了,知道了上面的内容,我们就可以写一份完整的封装网络数据的类了。内容如下(下面代码仅是伪代码,使用时请自行调试)。

//android:
//ServerBinder.java
public class ServerBinder{private final static String TAG = "ServerBinder";private long timeOffset = 0;//服务器时间和本地时间的差值//单例private ServerBinder(){}private static ServerBinder sBinder = null;public sythornized ServerBinder getInstance(){if(sBinder == null){sBinder = new ServerBinder();}return sBinder;}//保存所有注册的数据,当然要保存了,不保存怎么调用?private HashMap<String, BindData> mBindDatas;//表示注册的服务端数据public static class BindData{public String addr;//服务端地址public String entry;//服务端代码入口public String ifaceName;//接口名public String ifaceType;//接口类型public Class <?> recordClass;//返回record类型}//服务端返回数据public static class ServerData{public BindData bindData;//注册数据,让你分辨是什么接口及参数public BaseRecord serverRecord;//服务端返回的数据public int status;//接口调用状态 status为1表示成功,为0表示失败public String message;//服务端返回的错误或提示信息}//客户端回调接口public interface ServerCallback{//status 表示网络请求状态,bindData表示当前请求相关参数,record表示返回数据public void onServerCallback(ServerData data);}//注册!!public void regist(String addr, String entry, String ifaceName, String ifaceType, Class<?> recordClass){//初始化BindDataBindData data = new BindData();data.addr = addr;data.entry = entry;data.ifaceName = ifaceName;data.recordClass = recordClass;data.ifaceType = ifaceType;//把数据存起来mBindDatas.put(entry, data);}//客户端调用接口,注意接口参数,params是一个字符串数组,后端是无类型的php,可以这样写,但是如果后端是java则需要修改。或者可以用json。public void call(String ifaceName, ServerCallback cb, String ...params){if(!mBindDatas.contains(ifaceName)){Log.e();return;}BindData bindData = mBindDatas.get(ifaceName);switch(bindData.ifaceType){case "get":get(bindData, params, cb);break;case "post":post(bindData,params, cb);break;case "download":download(bindData, params, cb);break;case "upload":upload(bindData,params, cb);break;}}/*假设服务端数据格式为:{"status": 1,//1表示正确 0表示错误"time":17383592394,"message": "一切正常","data":{//需要转换成record的部分}}*/private void handleResponse(BindData bindData, String jsonStr, ServerCallback cb){JSONObject jsonObj = new JSONObject(jsonStr);ServerData serverData = new ServerData();serverData.bindData = bindData;serverData.status = jsonObj.getInt("status");serverData.message = jsonObj.getString("message");if(serverData.status == 1){String data = jsonObj.getObject("data").toString();serverData.serverRecord = (BaseRecord)new Gson().fromJson(data, bindData.recordClass);}cb.onServerCallback(serverData);//时间校正if(jsonObj.contains("time")){long time = jsonObj.getLong("time");timeOffset = time - getLocalTime();}}public long getLocalTime(){return System.currentTimeMillis();//毫秒,注意时间单位的统一。}public long getServerTime(){return getLocalTime() + timeOffset;}// 下面就是真正调用接口了// 另外iOS版本的ServerBinder,除了下面的4个函数内容不一样之外,其余部分逻辑完全一致。// 只需要把java翻译成objective-c即可。public void get(BindData bindData, String[]params, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}public void post(BindData bindData, String[]params, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}public void download(BindData bindData, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}public void upload(BindData bindData, String[]params, ServerCallback cb){//...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置handleResponse(bindData, jsonStr, cb);}
}
//ServerBinder.h
#import <Foundation/Foundation.h>//表示注册的服务端数据
@interface BindData : NSObject
@property (nonatomic, copy) NSString *addr;
@property (nonatomic, copy) NSString *entry;
@property (nonatomic, copy) NSString *ifaceName;
@property (nonatomic, copy) NSString *ifaceType;
@property (nonatomic, copy) Class recordClass;
@end//表示服务端返回数据
@interface ServerData : NSObject
@property (nonatomic, strong) BindData *bindData;
@property (nonatomic, strong) BaseRecord *serverRecord;
@property (nonatomic, unsafe_unretained) NSInteger status;
@property (nonatomic, copy) NSString *message;
@end//客户端回调接口
typedef void(^ServerCallbacka)(ServerData *);@interface ServerBindera : NSObject//单例
+(instancetype) getInstance;//注册接口
-(void) registWithAddr:(NSString *)addrentry:(NSString *)entryifaceName:(NSString *)ifaceNameifaceType:(NSString *)ifaceTypeclazz:(Class) clazz;//调用接口
-(void) callWithIfaceName:(NSString *)ifaceNamecb:(ServerCallback) cbparams:(NSDictionary *)params;//获取当前服务器时间
-(NSInteger) getServerTime;@end
//ServerBinder.m
#import "ServerBinder.h"@implementation BindData
@end@implementation ServerData
@end@implementation ServerBinder{NSInteger mTimeOffset;//服务器时间和本地时间的差值NSMutableDictionary *mBindDatas;//保存所有注册的数据,当然要保存了,不保存怎么调用?
}+(instancetype) getInstance{static ServerBinder *binder = nil;static dispatch_once_t dispatchOnce;dispatch_once(&dispatchOnce, ^{binder = [[ServerBinder alloc] init];});return binder;
}//注册某接口,只有注册过的接口才能使用 call 方法调用。全局每个接口只需调用一次
-(void) registWithAddr:(NSString *)addrentry:(NSString *)entryifaceName:(NSString *)ifaceNameifaceType:(NSString *)ifaceTypeclazz:(Class) clazz{BindData *data = [[BindData alloc] init];data.addr = addr;data.entry = entry;data.ifaceName = ifaceName;data.recordClass = clazz;data.ifaceType = ifaceType;[mBindDatas setObject:data forKey:entry];
}//调用某接口,在任何需要数据的时候调用。
-(void) callWithIfaceName:(NSString *)ifaceNamecb:(ServerCallback) cbparams:(NSDictionary *)params{if (![mBindDatas containsKey:ifaceName]) {NSLog(@"cant find this ifaceName: %@", ifaceName);return;}BindData *bindData = [mBindDatas objectForKey:ifaceName];if ([bindData.ifaceType isEqualToString:@"get"]) {[self getWithBindData:bindData andParams:params cb:cb];}else if ([bindData.ifaceType isEqualToString:@"post"]) {[self postWithBindData:bindData andParams:params cb:cb];}else if ([bindData.ifaceType isEqualToString:@"download"]) {[self downloadWithBindData:bindData andParams:params cb:cb];}else if ([bindData.ifaceType isEqualToString:@"upload"]) {[self uploadWithBindData:bindData andParams:params cb:cb];}
}//处理服务器返回数据
-(void) handleResponseWithBindData:(BindData *) bindData jsonDict:(NSDictionary *)jsonDict cb:(ServerCallback)cb{ServerData *serverData = [[ServerData alloc] init];serverData.bindData = bindData;serverData.status = [[jsonDict objectForKey:@"status"] intValue];serverData.message = [[jsonDict objectForKey:@"message"] stringValue];if (serverData.status == 1) {id data = [jsonDict objectForKey:@"data"];//把json数据转换成RecordserverData.serverRecord = [[[bindData.recordClass alloc] init]mj_setKeyValues:[data mj_JSONObject]];}if (cb) {cb(serverData);}//同步服务器时间if ([jsonDict containsKey:@"time"]) {NSInteger time = [[jsonDict objectForKey:@"time"] longValue];mTimeOffset = time - [self getLocalTime];}
}-(NSInteger) getLocalTime{//TODO 返回本地当前时间return 0;
}-(NSInteger) getServerTime{return [self getLocalTime] + mTimeOffset;
}-(void) getWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}-(void) postWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}-(void) downloadWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}-(void) uploadWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{//...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理//...此部分不在本文范围内,需自行完成//服务端数据回调时调用,当前只是示例不是真正调用位置[self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];
}@end

程序如何使用上述代码进行网络注册和调用呢?

android:
1. 需要自定义Application 假设定义为 MyApplication。
2. 在MyApplication中注册xUtils。
3. 新建某个接口对应的Record类: XXXRecord.java,这个类应该继承BaseRecord,具体写法参照。
4. 在MyApplication的onCreate方法中,添加代码:

ServerBinder.getInstance().regist("http://www.xxx.com", "api", "get_user_info", "get", XXXRecord.class);

5.在需要调用接口的地方这样写:

ServerBinder.getInstance().call("get_user_info", new ServerCallback(){@Overridepublic void onServerCallback(ServerData data){//data中包含很多数据,其中 data.serverRecord 就是我们的XXXRecord的实例了。XXXRecord *record = (XXXRecord)data.serverRecord;}
}, "uid", "1");

iOS:
1. 新建某个接口对应的Record类:XXXRecord,请参照MJExtension及其demo进行创建。
2. 在AppDelegate的如下方法中:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

添加代码

[[ServerBinder getInstance] registWithAddr: @"http://www.xxx.com" entry:@"api" ifaceName:@"get_user_info" ifaceType:@"get" Class:[XXXRecord class]];

3 . 在需要调用的地方这样写:

[ServerBinder getInstance] callWithIfaceName:@"get_user_info" cb:^(ServerData *serverData){//serverData中包含很多数据,其中 serverData.serverRecord 就是我们的XXXRecord的实例了。XXXRecord *record = (XXXRecord *)serverData.serverRecord;
} params:@{@"uid":1}];

至此,一个完整的网络模块就完成了。

相关文章:

结构体成员数组不定长如何实现

【目的】 定义一个结构体类&#xff0c;其中的成员变量数组长度不定&#xff0c;根据实例化的对象指定长度&#xff0c;所以想到用指针实现 【现状】 指针可以指向任意长度数组&#xff0c;但结构体类只分配指针本身4字节长度&#xff0c;所以无法扩展 1 /**2 ****************…

团队项目:二次开发

至此&#xff0c;我们有了初步的与人合作经验&#xff0c;接下来投入到更大的团队中去。 也具备了一定的个人能力&#xff0c;能将自己的代码进行测试。接下来尝试在别人已有的基础上进行开发。 上一界51冯美欣同学的项目&#xff1a;http://www.cnblogs.com/maxx/ 1.每个团队从…

arduino 呼吸灯_如何改善您的Arduino呼吸机:用于临时COVID-19呼吸机设计的RTS和SCS简介...

arduino 呼吸灯The world as we know it was recently taken by storm. That storm was the outbreak of the COVID-19 pandemic. This has in turn created a shortage of ventilators world wide which has led many people to foray into the world of ventilator design. 我…

reboot 百度网盘资源

提醒&#xff1a;同志们这是记录&#xff0c;视频文件是加密的&#xff0c;请勿下载 基础班第十三期&#xff1a;http://pan.baidu.com/s/1c2GcvKG 密码: 743j 基础班第十四期链接: http://pan.baidu.com/s/1c24AYa8 密码: x2sh 第十五期&#xff1a; https://pan.baidu.com…

仅需6步,教你轻易撕掉app开发框架的神秘面纱(5):数据持久化

遇到的问题 有的时候程序中需要全局皆可访问的变量&#xff0c;比如&#xff1a;用户是否登录&#xff0c;用户个人信息(用户名&#xff0c;地区&#xff0c;生日)&#xff0c;或者一些其他信息如&#xff1a;是否是首次登录&#xff0c;是否需要显示新手引导等等。 其中有些…

响应因特网端口ping命令_如何使用Ping命令识别基本的Internet问题

响应因特网端口ping命令Next time you call your help desk, do you want to wow them with your networking knowledge? Using a command called “ping”, built right into your existing Mac, Windows, or Linux computer, will help identify basic connection problems.…

Android 常见工具类封装

1&#xff0c;MD5工具类&#xff1a; public class MD5Util {public final static String MD5(String s) {char hexDigits[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,a, b, c, d, e, f };try {byte[] btInput s.getBytes();// 获得MD5摘要算法的 MessageDigest 对象MessageDigest md…

keras系列︱图像多分类训练与利用bottleneck features进行微调(三)

引自&#xff1a;http://blog.csdn.net/sinat_26917383/article/details/72861152 中文文档&#xff1a;http://keras-cn.readthedocs.io/en/latest/ 官方文档&#xff1a;https://keras.io/ 文档主要是以keras2.0。 训练、训练主要就”练“嘛&#xff0c;所以堆几个案例就知…

LIKE 操作符

LIKE 操作符LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式。SQL LIKE 操作符语法SELECT column_name(s)FROM table_nameWHERE column_name LIKE pattern原始的表 (用在例子中的)&#xff1a;Persons 表:IdLastNameFirstNameAddressCity1AdamsJohnOxford StreetLondon2Bush…

服务器云ide_语言服务器协议如何影响IDE的未来

服务器云ideThe release of Visual Studio Code single-handedly impacted the developer ecosystem in such a way that theres no going back now. Its open source, free, and most importantly, a super powerful tool. Visual Studio Code的发布以一种无可匹敌的方式对开发…

仅需6步,教你轻易撕掉app开发框架的神秘面纱(6):各种公共方法及工具类的封装

为什么要封装公共方法 封装公共方法有2方面的原因&#xff1a; 一是功能方面的原因&#xff1a;有些方法很多地方都会用&#xff0c;而且它输入输出明确&#xff0c;并且跟业务逻辑无关。比如检查用户是否登录&#xff0c;检查某串数字是否为合法的手机号。像这种方法就应该封…

MySQL优化配置之query_cache_size

原理MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存&#xff0c;会立刻返回结果&#xff0c;跳过了解析&#xff0c;优化和执行阶段。 查询缓存会跟踪查询中涉及的每个表&#xff0c;如果这写表发生变化&#xff0c;那么和这个表相关的所有缓存都将失效。 但是随着服…

request.getSession()

request.getSession(); 与request.getSession(false);区别 服务器把session信息发送给浏览器 浏览器会将session信息存入本地cookie中 服务器本地内存中也会留一个此session信息 以后用户发送请求时 浏览器都会把session信息发送给服务器 服务器会依照浏览器发送过来的se…

alpine 交互sh_在这个免费的交互式教程中学习Alpine JS

alpine 交互shAlpine.js is a rugged, minimal framework for composing Javascript behavior in your markup. Thats right, in your markup! Alpine.js是一个坚固的最小框架&#xff0c;用于在标记中构成Javascript行为。 是的&#xff0c;在您的标记中&#xff01; It allo…

浅谈 MVP in Android

一、概述 对于MVP&#xff08;Model View Presenter&#xff09;&#xff0c;大多数人都能说出一二&#xff1a;“MVC的演化版本”&#xff0c;“让Model和View完全解耦”等等。本篇博文仅是为了做下记录&#xff0c;提出一些自己的看法&#xff0c;和帮助大家如何针对一个Acti…

test markdown

test test public void main(String[] args){System.out.println("test"); } 转载于:https://www.cnblogs.com/cozybz/p/5427053.html

java开发工具对比eclipse·myeclipse·idea

eclipse:不说了&#xff0c;习惯了 myeclipse&#xff1a;MyEclipse更适合企业开发者&#xff0c;更团队开发 idea:idea更适合个人开发者,细节优化更好转载于:https://www.cnblogs.com/gjack/p/8136964.html

软件测试质量过程检测文档_如何编写实际上有效的质量检查文档

软件测试质量过程检测文档A software product is like an airplane: it must undergo a technical check before launch.软件产品就像飞机&#xff1a;必须在发射前经过技术检查。 Quality Assurance is a necessary step towards launching a successful software product. I…

Android深度探索--HAL与驱动开发----第一章读书笔记

1.1 Android拥有非常完善的系统构架可以分为四层&#xff1a; 第一层&#xff1a;Linux内核。主要包括驱动程序以及管理内存、进程、电源等资源的程序 第二层&#xff1a;C/C代码库。主要包括Linux的.so文件以及嵌入到APK程序中的NDK代码 第三层&#xff1a;android SDK API …

[NOI2011]Noi嘉年华

题解:我们设计状态方程如下: num[i][j]表示从时间i到j中有多少个 pre[i][j]表示时间1~i中,A选了j个时的B能选的数量的最大值. nex[i][j]表示时间i~cnt中,A选了j个时的B能选的数量的最大值. mus[i][j]表示从时间i到j的保证选时,A和B选的数量中的较小值的最大值. ①对于num数组直…

只有20%的iOS程序员能看懂:详解intrinsicContentSize 及 约束优先级/content Hugging/content Compression Resistance

在了解intrinsicContentSize之前&#xff0c;我们需要先了解2个概念&#xff1a; AutoLayout在做什么约束优先级是什么意思。 如果不了解这两个概念&#xff0c;看intinsic content size没有任何意义。 注&#xff1a;由于上面这几个概念都是针对UIView或其子类(UILabel&…

redux rxjs_可观察的RxJS和Redux入门指南

redux rxjsRedux-Observable is an RxJS-based middleware for Redux that allows developers to work with async actions. Its an alternative to redux-thunk and redux-saga.Redux-Observable是Redux的基于RxJS的中间件&#xff0c;允许开发人员使用异步操作。 它是redux-t…

javascript数组排序和prototype详解

原型的概念:&#xff1a;原型对象里的所有属性和方法 被所有构造函数实例化出来的对象所共享&#xff0c;类似于java中的 static 正因为共享所以单一的操作 就会影响了全局&#xff0c;因此使用时需注意 基于prototype&#xff1a;为数组扩展方法 //获取数组最大值function get…

Qt 在Label上面绘制罗盘

自己写的一个小小的电子罗盘的一个小程序&#xff0c;不过是项目的一部分&#xff0c;只可以贴绘制部分代码 效果如下图 首先开始自己写的时候&#xff0c;虽然知道Qt 的坐标系是从左上角开始的&#xff0c;所以&#xff0c;使用了算法&#xff0c;在绘制后&#xff0c;在移动回…

终极方案!解决正确设置LaunchImage后仍然不显示的问题

对于如何设置LaunchImage&#xff0c;网络上有各种各样的教程。 主要分2点&#xff1a; 1. 正确设置图片尺寸 2. 取消LaunchScreen.xib 但是经过上述步骤之后&#xff0c;你觉得完全没有问题了&#xff0c;但是仍然无法显示LaunchImage。 或者&#xff0c;你在多个模拟器上…

c# 持续集成 单元测试_如何在不进行单元测试的情况下设置持续集成

c# 持续集成 单元测试Do you think continuous integration is not for you because you have no automated tests? Or no unit tests at all? Not true. Tests are important. But there are many more aspects to continuous integration than just testing. Lets see what…

Handlebars模板引擎

介绍 Handlebars 是 JavaScript 一个语义模板库&#xff0c;通过对view和data的分离来快速构建Web模板。它采用"Logic-less template"&#xff08;无逻辑模版&#xff09;的思路&#xff0c;在加载时被预编译&#xff0c;而不是到了客户端执行到代码时再去编译&#…

字符集图标制作

字符集图标&#xff1a; 将网页上常见的icon做成font&#xff08;字符集&#xff09;&#xff0c;以字体的方式插入到网页上&#xff0c;作用是减轻服务器负担&#xff0c;减少宽带。 我最常在这两个网站上下载字体图标&#xff1a; https://icomoon.io/app/#/select https://w…

Adobe源码泄漏?3行代码搞定,Flash动画无缝导入Android/iOS/cocos2dx(一)

[注] iOS代码已重构&#xff0c;效率提升90%&#xff0c;200层动画不卡。[2016.10.27] 项目介绍 项目名称&#xff1a;FlashAnimationToMobile 源码。 使用方法点这里。 这是一个把flash中的关键帧动画(不是序列帧)导出&#xff0c;然后在iOS&#xff0f;Android原生应用中解…

背景图像位置css_CSS背景图像大小教程–如何对整页背景图像进行编码

背景图像位置cssThis tutorial will show you a simple way to code a full page background image using CSS. And youll also learn how to make that image responsive to your users screen size.本教程将向您展示一种使用CSS编写整页背景图像的简单方法。 您还将学习如何使…