快速Android开发系列网络篇之Retrofit
Retrofit是一个不错的网络请求库,用官方自己的介绍就是:
A type-safe REST client for Android and Java
看官网的介绍用起来很省事,不过如果不了解它是怎么实现的也不太敢用,不然出问题了就不知道怎么办了。这几天比较闲就下下来看了一下,了解一下大概实现方法,细节就不追究了。先来看一个官网的例子,详细说明去网官看
简单示例
首先定义请求接口,即程序中都需要什么请求操作
public interface GitHubService {
@GET("/users/{user}/repos")
List<Repo> listRepos(@Path("user") String user);
}
然后通过RestAdapter
生成一个刚才定义的接口的实现类,使用的是动态代理。
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build(); GitHubService service = restAdapter.create(GitHubService.class);
现在就可以调用接口进行请求了
List<Repo> repos = service.listRepos("octocat");
使用就是这么简单,请求时直接调用接口就行了,甚至不用封装参数,因为参数的信息已经在定义接口时通过Annotation定义好了。
从上面的例子可以看到接口直接返回了需要的Java类型,而不是byte[]或String,解析数据的地方就是Converter
,这个是可以自定义的,默认是用Gson
解析,也就是说默认认为服务器返回的是Json数据,可以通过指定不同的Convert
使用不同的解析方法,如用Jackson
解析Json,或自定义XmlConvert解析xml数据。
Retrofit的使用就是以下几步:
- 定义接口,参数声明,Url都通过Annotation指定
- 通过
RestAdapter
生成一个接口的实现类(动态代理) - 调用接口请求数据
接口的定义要用用Rtrofit定义的一些Annotation,所以先看一下Annotation的。
Annotation
以上面的示例中的接口来看
@GET("/group/{id}/users")
List<User> groupList(@Path("id") int groupId);
先看@GET
/** Make a GET request to a REST path relative to base URL. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod("GET")
public @interface GET {
String value();
}
@GET本身也被几个Anotation注解,@Target表示@GET注解是用于方法的,value方法就返回这个注解的value值,在上例中就是/group/{id}/users,然后就是@RestMethod
@Documented
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
public @interface RestMethod {
String value();
boolean hasBody() default false;
}
RestMethod
是一个用于Annotation的Annotation,比如上面的例子中用来注解的@GET,value方法就返回GET,hasBody表示是否有Body,对于POST这个方法就返回true
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod(value = "POST", hasBody = true)
public @interface POST {
String value();
}
Retrofit的Annotation包含请求方法相关的@GET、@POST、@HEAD、@PUT、@DELETA、@PATCH,和参数相关的@Path、@Field、@Multipart等。
定义了Annotation要就有解析它的方法,在Retrofit中解析的位置就是RestMethodInfo
,但在这之前需要先看哪里使用了RestMethodInfo
,前面说了Retrofit使用了动态代理生成了我们定义的接口的实现类,而这个实现类是通过RestAdapter.create
返回的,所以使用动态代理的位置就是RestAdapter
,接下来就看一下RestAdapter
。
RestAdapter
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build(); GitHubService service = restAdapter.create(GitHubService.class); public RestAdapter build() {
if (endpoint == null) {
throw new IllegalArgumentException("Endpoint may not be null.");
} ensureSaneDefaults(); return new RestAdapter(endpoint, clientProvider, httpExecutor, callbackExecutor,
requestInterceptor, converter, profiler, errorHandler, log, logLevel);
}
setEndPoint
就不说了,接口中定义的都是相对Url,EndPoint就是域名,build
方法调用ensureSaneDefaults()
方法,然后就构造了一个RestAdapter对象,构造函数的参数中传入了EndPoint外的几个对象,这几个对象就是在ensureSaneDefaults()
中初始化的。
private void ensureSaneDefaults() {
if (converter == null) { converter = Platform.get().defaultConverter(); }
if (clientProvider == null) { clientProvider = Platform.get().defaultClient(); }
if (httpExecutor == null) { httpExecutor = Platform.get().defaultHttpExecutor(); }
if (callbackExecutor == null) { callbackExecutor = Platform.get().defaultCallbackExecutor(); }
if (errorHandler == null) { errorHandler = ErrorHandler.DEFAULT; }
if (log == null) { log = Platform.get().defaultLog(); }
if (requestInterceptor == null) { requestInterceptor = RequestInterceptor.NONE; }
}
ensureSaneDefaults()
中初始化了很多成员,errorHandler、log就不看了,其他的除了requestInterceptor
都是通过Platform
对象获得的,所以要先看下Platform
Platform
private static final Platform PLATFORM = findPlatform();
static final boolean HAS_RX_JAVA = hasRxJavaOnClasspath(); static Platform get() {
return PLATFORM;
} private static Platform findPlatform() {
try {
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
} if (System.getProperty("com.google.appengine.runtime.version") != null) {
return new AppEngine();
} return new Base();
}
使用了单例的PLATFORM
,通过findPlatform()
初始化实例,如果是Android平台就使用Platform.Android
,如果是Google AppEngine就使用Platform.AppEngine
,否则使用Platform.Base
,这些都是Platform
的子类,其中AppEngine
又是Base
的子类。
Platform
是一个抽象类,定义了以下几个抽象方法,这几个方法的作用就是返回一些RestAdapter
中需要要用到成员的默认实现
abstract Converter defaultConverter(); // 默认的Converter,用于将请求结果转化成需要的数据,如GsonConverter将JSON请求结果用Gson解析成Java对象
abstract Client.Provider defaultClient(); // Http请求类,如果是AppEngine就使用`UrlFetchClient`,否则如果有OKHttp就使用OKHttp,如果是Android,2.3以后使用HttpURLConnection,2.3以前使用HttpClient
abstract Executor defaultHttpExecutor(); // 用于执行Http请求的Executor
abstract Executor defaultCallbackExecutor(); // Callback调用中用于执行Callback的Executor(可能是同步的)
abstract RestAdapter.Log defaultLog(); // Log接口,用于输出Log
看完Platform
的接口再看ensureSaneDefaults
就清楚了,初始化转化数据的Converter、执行请求的Client、执行请求的Executor、执行Callback的Executor、Log输出类、错误处理类和用于在请求前添加额外处理的拦截请求的Interceptor。
Converter
默认都是用的GsonConverter
,就不看了,defaultClient
返回执行网络请求的Client
Platform.Android
@Override Client.Provider defaultClient() {
final Client client;
if (hasOkHttpOnClasspath()) {
client = OkClientInstantiator.instantiate();
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
client = new AndroidApacheClient();
} else {
client = new UrlConnectionClient();
}
return new Client.Provider() {
@Override public Client get() {
return client;
}
};
}
@Override Client.Provider defaultClient() {
final Client client;
if (hasOkHttpOnClasspath()) {
client = OkClientInstantiator.instantiate();
} else {
client = new UrlConnectionClient();
}
return new Client.Provider() {
@Override public Client get() {
return client;
}
};
}
Platform.AppEngine
@Override Client.Provider defaultClient() {
final UrlFetchClient client = new UrlFetchClient();
return new Client.Provider() {
@Override public Client get() {
return client;
}
};
}
对于Android,优先使用OKHttp,否则2.3以后使用HttpUrlConnection,2.3以前使用HttpClient
defaultHttpExecutor
就是返回一个Executor,执行请求的线程在这个Executor中执行,就做了一件事,把线程设置为后台线程
defaultCallbackExecutor
用于执行Callback类型的请求时,提供一个Executor执行Callback的Runnable
Platform.Base
@Override Executor defaultCallbackExecutor() {
return new Utils.SynchronousExecutor();
}
Platform.Android
@Override Executor defaultCallbackExecutor() {
return new MainThreadExecutor();
}
SynchronousExecutor
static class SynchronousExecutor implements Executor {
@Override public void execute(Runnable runnable) {
runnable.run();
}
}
public final class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper()); @Override public void execute(Runnable r) {
handler.post(r);
}
}
Platform
看完了,RestAdapter的成员初始化完成,就要看怎么通过RestAdapter.create
生成我们定义的接口的实现类了
RestAdapter.create
public <T> T create(Class<T> service) {
Utils.validateServiceClass(service);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new RestHandler(getMethodInfoCache(service)));
} Map<Method, RestMethodInfo> getMethodInfoCache(Class<?> service) {
synchronized (serviceMethodInfoCache) {
Map<Method, RestMethodInfo> methodInfoCache = serviceMethodInfoCache.get(service);
if (methodInfoCache == null) {
methodInfoCache = new LinkedHashMap<Method, RestMethodInfo>();
serviceMethodInfoCache.put(service, methodInfoCache);
}
return methodInfoCache;
}
}
使用了动态代理,InvocationHandler
是RestHandler
,RestHandler
有一个参数,是Method
->RestMethodInfo
的映射,初始化时这个映射是空的。重点就是这两个了:RestHandler
,RestMethodInfo
,
@Override public Object invoke(Object proxy, Method method, final Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) { //
return method.invoke(this, args);
} // Load or create the details cache for the current method.
final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method); // if (methodInfo.isSynchronous) { //
try {
return invokeRequest(requestInterceptor, methodInfo, args);
} catch (RetrofitError error) {
Throwable newError = errorHandler.handleError(error);
if (newError == null) {
throw new IllegalStateException("Error handler returned null for wrapped exception.",
error);
}
throw newError;
}
} if (httpExecutor == null || callbackExecutor == null) {
throw new IllegalStateException("Asynchronous invocation requires calling setExecutors.");
} // Apply the interceptor synchronously, recording the interception so we can replay it later.
// This way we still defer argument serialization to the background thread.
final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
requestInterceptor.intercept(interceptorTape); // if (methodInfo.isObservable) { //
if (rxSupport == null) {
if (Platform.HAS_RX_JAVA) {
rxSupport = new RxSupport(httpExecutor, errorHandler);
} else {
throw new IllegalStateException("Observable method found but no RxJava on classpath");
}
} return rxSupport.createRequestObservable(new Callable<ResponseWrapper>() {
@Override public ResponseWrapper call() throws Exception {
return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
}
});
} Callback<?> callback = (Callback<?>) args[args.length - 1]; //
httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
@Override public ResponseWrapper obtainResponse() {
return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
}
}); return null; // Asynchronous methods should have return type of void.
}
执行请求时会调用RestHandler
的invoke
方法,如上所示,主要是上面代码中标注有6点
- 如果调用的是Object的方法,不做处理直接调用。
- 通过
getMethodInfo
获取调用的Method
对应的RestMethodInfo
,前面说了,构造RestHandler
对象时传进来了一个Method
->RestMethodInfo
的映射,初始时是空的。
static RestMethodInfo getMethodInfo(Map<Method, RestMethodInfo> cache, Method method) {
synchronized (cache) {
RestMethodInfo methodInfo = cache.get(method);
if (methodInfo == null) {
methodInfo = new RestMethodInfo(method);
cache.put(method, methodInfo);
}
return methodInfo;
}
在getMethodInfo
中判断如果相应的映射不存在,就建立这个映射,并如名字所示缓存起来
3. 如果是同步调用(接口中直接返回数据,不通过Callback或Observe),直接调用invokeRequest
4. 如果是非同步调用,先通过RequestInterceptorTape
记录拦截请求,记录后在后台线程做实际拦截,后面会提到。
5. 如果是Observe请求(RxJava),执行第5步,对RxJava不了解,略过
6. 如果是Callback形式,交由线程池执行
接口中的每一个Method有一个对应的RestMethodInfo,关于接口中Annotation信息的处理就都在这里了
RestMethodInfo
private enum ResponseType {
VOID,
OBSERVABLE,
OBJECT
}
RestMethodInfo(Method method) {
this.method = method;
responseType = parseResponseType();
isSynchronous = (responseType == ResponseType.OBJECT);
isObservable = (responseType == ResponseType.OBSERVABLE);
}
在构造函数中调用了parseResponseType
,parseResponseType
解析了方法签名,根据方法的返回值类型及最后一个参数的类型判断方法的类型是哪种ResponseType
无论是哪种ResponseType,最终都是调用invokeRequest
执行实际的请求,接下来依次看下invokeRequest
的执行步骤
RestAdapter.invokeRequest
第一步是调用methodInfo.init()
解析调用的方法,方法里有做判断,只在第一次调用时解析,因为处一次解析后这个对象就被缓存起来了,下次调同一个方法时可以直接使用
synchronized void init() {
if (loaded) return; parseMethodAnnotations();
parseParameters(); loaded = true;
}
在RestMethodInfo.init
中分别调用
parseMethodAnnotations()
:解析所有方法的AnnotationparseParameters()
:解析所有参数的Annotation
for (Annotation methodAnnotation : method.getAnnotations()) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
RestMethod methodInfo = null;
// Look for a @RestMethod annotation on the parameter annotation indicating request method.
for (Annotation innerAnnotation : annotationType.getAnnotations()) {
if (RestMethod.class == innerAnnotation.annotationType()) {
methodInfo = (RestMethod) innerAnnotation;
break;
}
}
...
}
在parseMethodAnnotations
中,会获取方法所有的Annotation并遍历:
- 对于每一个Annotation,也会获取它的Annotation,看它是否是被RestMethod注解的Annotation,如果是,说明是@GET,@POST类型的注解,就调用
parsePath
解析请求的Url,requestParam(URL中问号后的内容)及Url中需要替换的参数名(Url中大括号括起来的部分) - 寻找Headers Annotation解析Header参数
- 解析RequestType:SIMPLE,MULTIPART,FORM_URL_ENCODED
parseParameters
解析请求参数,即参数的Annotation,@PATH
、@HEADER
、@FIELD
等
第二步是RequestBuilder和Interceptor,这两个是有关联的,所以一起看。
RequestBuilder requestBuilder = new RequestBuilder(serverUrl, methodInfo, converter);
requestBuilder.setArguments(args);
requestInterceptor.intercept(requestBuilder);
Request request = requestBuilder.build();
先说RequestInterceptor,作用很明显,当执行请求时拦截请求以做一些特殊处理,比如添加一些额外的请求参数。
/** Intercept every request before it is executed in order to add additional data. */
public interface RequestInterceptor {
/** Called for every request. Add data using methods on the supplied {@link RequestFacade}. */
void intercept(RequestFacade request); interface RequestFacade {
void addHeader(String name, String value);
void addPathParam(String name, String value);
void addEncodedPathParam(String name, String value);
void addQueryParam(String name, String value);
void addEncodedQueryParam(String name, String value);
} /** A {@link RequestInterceptor} which does no modification of requests. */
RequestInterceptor NONE = new RequestInterceptor() {
@Override public void intercept(RequestFacade request) {
// Do nothing.
}
};
}
RequestInterceptor
只有一个方法intercept
,接收一个RequestFacade
参数,RequestFacade
是RequestInterceptor
内部的一个接口,这个接口的方法就是添加请求参数,Query、Header什么的。大概可以看出RequestInterceptor
的作用了,如果RequestFacade
表示一个请求相关的数据,RequestInteceptor.intercept
的作用就是向这个RequestFacade
中添加额外Header,Param等参数。
RequestFacade
的一个子类叫RequestBuilder
,用来处理Request
请求参数,在invokeRequest
中会对RequestBuilder
调用intercept
方法向RequestBuilder
添加额外的参数。
有一个叫RequestInterceptorTape
的类,同时实现了RequestFacade
与RequestInterceptor
,它的作用是:
- 当作为
RequestFacade
使用时作为参数传给一个RequestInteceptor
,这个RequestInterceptor
调用它的addHeader
等方法时,它把这些调用及参数记录下来 - 然后作为
RequestInterceptor
使用时,将之前记录的方法调用及参数重新应用到它的intercept
参数RequestFacade
中
在RestHandler.invoke
中,如果判断方法的调用不是同步调用,就通过下面的两行代码将用户设置的interceptor需要添加的参数记录到RequestInterceptorTape
,然后在invokeRequest
中再实际执行参数的添加。
// Apply the interceptor synchronously, recording the interception so we can replay it later.
// This way we still defer argument serialization to the background thread.
final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
requestInterceptor.intercept(interceptorTape);
RequestBuilder.setArguments()
解析调用接口时的实际参数。然后通过build()
方法生成一个Request
对象
第三步执行请求,Response response = clientProvider.get().execute(request);
第四步就是解析并分发请求结果了,成功请求时返回结果,解析失败调用ErrorHandler
给用户一个自定义异常的机会,但最终都是通过异常抛出到invoke()
中的,如果是同步调用,直接抛异常,如果是Callback调用,会回调Callback.failure
CallbackRunnable
请求类型有同步请求,Callback请求,Observable请求,来看下Callback请求:
Callback<?> callback = (Callback<?>) args[args.length - 1];
httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
@Override public ResponseWrapper obtainResponse() {
return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
}
});
Callback请求中函数最后一个参数是一个Callback的实例,httpExecutor是一个Executor,用于执行Runnable请求,我们看到,这里new了一个CallbackRunnable执行,并实现了它的obtainResponse方法,看实现:
abstract class CallbackRunnable<T> implements Runnable {
private final Callback<T> callback;
private final Executor callbackExecutor;
private final ErrorHandler errorHandler; CallbackRunnable(Callback<T> callback, Executor callbackExecutor, ErrorHandler errorHandler) {
this.callback = callback;
this.callbackExecutor = callbackExecutor;
this.errorHandler = errorHandler;
} @SuppressWarnings("unchecked")
@Override public final void run() {
try {
final ResponseWrapper wrapper = obtainResponse();
callbackExecutor.execute(new Runnable() {
@Override public void run() {
callback.success((T) wrapper.responseBody, wrapper.response);
}
});
} catch (RetrofitError e) {
Throwable cause = errorHandler.handleError(e);
final RetrofitError handled = cause == e ? e : unexpectedError(e.getUrl(), cause);
callbackExecutor.execute(new Runnable() {
@Override public void run() {
callback.failure(handled);
}
});
}
} public abstract ResponseWrapper obtainResponse();
}
就是一个普通的Runnable,在run方法中首先执行obtailResponse,从名字可以看到是执行请求返回Response,这个从前面可以看到执行了invokeRequest,和同步调用中一样执行请求。
紧接着就提交了一个Runnable至callbackExecutor,在看Platform
时看到了callbackExecotor是通过Platform.get().defaultCallbackExecutor()
返回的,Android中是向主线程的一个Handler发消息
值得注意的事,对于同步调用,如果遇到错误是直接抛异常,而对于异步调用,是调用Callback.failure()
Mime
执行网络请求,需要向服务端发送请求参数,如表单数据,上传的文件等,同样需要解析服务端返回的数据,在Retrofit中对这些做了封装,位于Mime包中,也只有封装了,才好统一由指定的Converter执行数据的转换
TypedInput
和TypedOutput
表示输入输出的数据,都包含mimeType,并分别支持读入一个InputStream或写到一个OutputStrem
/**
* Binary data with an associated mime type.
*
* @author Jake Wharton (jw@squareup.com)
*/
public interface TypedInput { /** Returns the mime type. */
String mimeType(); /** Length in bytes. Returns {@code -1} if length is unknown. */
long length(); /**
* Read bytes as stream. Unless otherwise specified, this method may only be called once. It is
* the responsibility of the caller to close the stream.
*/
InputStream in() throws IOException;
} /**
* Binary data with an associated mime type.
*
* @author Bob Lee (bob@squareup.com)
*/
public interface TypedOutput {
/** Original filename.
*
* Used only for multipart requests, may be null. */
String fileName(); /** Returns the mime type. */
String mimeType(); /** Length in bytes or -1 if unknown. */
long length(); /** Writes these bytes to the given output stream. */
void writeTo(OutputStream out) throws IOException;
}
TypedByteArray
,内部数据是一个Byte数组
private final byte[] bytes; @Override public long length() {
return bytes.length;
} @Override public void writeTo(OutputStream out) throws IOException {
out.write(bytes);
} @Override public InputStream in() throws IOException {
return new ByteArrayInputStream(bytes);
}
TypedString
,继承自TypedByteArray
,内部表示是一样的
public TypedString(String string) {
super("text/plain; charset=UTF-8", convertToBytes(string));
} private static byte[] convertToBytes(String string) {
try {
return string.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
其他的也一样,从名字很好理解:TypedFile
,MultipartTypedOutput
,FormEncodedTypedOutput
。
其他
Retrofit对输入和输出做了封装,通过TypedOutput
向服务器发送数据,通过TypedInput
读取服务器返回的数据。
通过MultipartTypedOutput
支持文件上传,读取服务器数据时,如果要求直接返回未解析的Response,Restonse会被转换为TypedByteArray,所以不能是大文件类的
Retrofit支持不同的Log等级,当为LogLevel.Full时会把Request及Response的Body打印出来,所以如果包含文件就不行了。
Retrofit默认使用GsonConverter,所以要想获取原始数据不要Retrofit解析,要么自定义Conveter,要么直接返回Response了,返回Response也比较麻烦
总体来说Retrofit看起来很好用,不过要求服务端返回数据最好要规范,不然如果请求成功返回一种数据结构,请求失败返回另一种数据结构,不好用Converter解析,接口的定义也不好定义,除非都返回Response,或自定义Converter所有接口都返回String
在Twitter上JakeWharton这么说:
Gearing up towards a Retrofit 1.6.0 release and then branching 1.x so we can push master towards a 2.0 and fix long-standing design issues.
要出2.0了,内部API会改,接口应该不怎么变
快速Android开发系列网络篇之Retrofit的更多相关文章
- 快速Android开发系列网络篇之Volley
Volley是Google推出的一个网络请求库,已经被放到了Android源码中,地址在这里,先看使用方法 RequestQueue mRequestQueue = Volley.newRequest ...
- 快速Android开发系列网络篇之Android-Async-Http
先来看一下最基本的用法 AsyncHttpClient client = new AsyncHttpClient(); client.get("http://www.google.com&q ...
- 快速Android开发系列通信篇之EventBus
先吐槽一下博客园的MarkDown编辑器,推出的时候还很高兴博客园支持MarkDown了,试用了下发现支持不完善就没用了,这次这篇是在其他编辑器下写的,复制过来后发现..太烂了.怎么着作为一个技术博客 ...
- 高速Android开发系列通信篇之EventBus
概述及基本概念 **EventBus**是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间.组件与后台线程间的通信.比方请求网络,等网络返回时通过Hand ...
- 【转载】Android Metro风格的Launcher开发系列第二篇
前言: 各位小伙伴们请原谅我隔了这么久才开始写这一系列的第二篇博客,没办法忙新产品发布,好了废话不说了,先回顾一下:在我的上一篇博客Android Metro风格的Launcher开发系列第一篇写了如 ...
- Android 开发系列教程之(一)Android基础知识
什么是Android Android一词最早是出现在法国作家维里耶德利尔·亚当1986年发表的<未来夏娃>这部科幻小说中,作者利尔·亚当将外表像人类的机器起名为Android,这就是And ...
- android开发获取网络状态,wifi,wap,2g,3g.工具类(一)
android开发获取网络状态整理: package com.gzcivil.utils; import android.content.Context; import android.net.Con ...
- Android Metro风格的Launcher开发系列第二篇
前言: 各位小伙伴们请原谅我隔了这么久才开始写这一系列的第二篇博客,没办法忙新产品发布,好了废话不说了,先回顾一下:在我的上一篇博客http://www.cnblogs.com/2010wuhao/p ...
- 微信小程序购物商城系统开发系列-工具篇
微信小程序开放公测以来,一夜之间在各种技术社区中就火起来啦.对于它 估计大家都不陌生了,对于它未来的价值就不再赘述,简单一句话:可以把小程序简单理解为一个新的操作系统.新的生态,未来大部分应用场景都将 ...
随机推荐
- C语言 · 高精度加法
问题描述 输入两个整数a和b,输出这两个整数的和.a和b都不超过100位. 算法描述 由于a和b都比较大,所以不能直接使用语言中的标准数据类型来存储.对于这种问题,一般使用数组来处理. 定义一个数组A ...
- 创建 OVS flat network - 每天5分钟玩转 OpenStack(134)
上一节完成了 flat 的配置工作,今天创建 OVS flat network.Admin -> Networks,点击 "Create Network" 按钮. 显示创建页 ...
- rnandroid环境搭建
react-native 环境搭建具体步骤这个大家已经玩烂了,这个主要是记录下来自己做win7系统遇到的坑 1.com.android.ddmlib.installexception 遇到这个问题,在 ...
- json与JavaScript对象互换
1,json字符串转化为JavaScript对象: 方法:JSON.parse(string) eg:var account = '{"name":"jaytan&quo ...
- 前端学HTTP之报文首部
前面的话 首部和方法配合工作,共同决定了客户端和服务器能做什么事情.在请求和响应报文中都可以用首部来提供信息,有些首部是某种报文专用的,有些首部则更通用一些.本文将详细介绍HTTP报文中的首部 结构 ...
- SQL Server-聚焦NOT IN VS NOT EXISTS VS LEFT JOIN...IS NULL性能分析(十八)
前言 本节我们来综合比较NOT IN VS NOT EXISTS VS LEFT JOIN...IS NULL的性能,简短的内容,深入的理解,Always to review the basics. ...
- Android如何制作漂亮的自适布局的键盘
最近做了个自定义键盘,但面对不同分辨率的机型其中数字键盘不能根据界面大小自已铺满,但又不能每种机型都做一套吧,所以要做成自适应,那这里主讲思路. 这里最上面的titlebar高度固定,下面输入的金额高 ...
- CSS 3学习——transition 过渡
以下内容根据官方规范翻译以及自己的理解整理. 1.介绍 这篇文档介绍能够实现隐式过渡的CSS新特性.文档中介绍的CSS新特性描述了CSS属性的值如何在给定的时间内平滑地从一个值变为另一个值. 2.过渡 ...
- [原] 利用 OVS 建立 VxLAN 虚拟网络实验
OVS 配置 VxLAN HOST A ------------------------------------------ | zh-veth0(10.1.1.1) VM A | | ---|--- ...
- [EasyUI美化换肤]更换EasyUi图标
前言 本篇文章主要是记录一些换EasyUI皮肤的过程,备忘.也欢迎美工大神各路UI给点好意见,EasyUI我就不介绍了,自行百度吧..(So..所以别问我是不是响应式..本身EasyUI就不是响应式. ...