Retrofit1.9源码解析(二)
2015-12-17 21:40
661 查看
Retrofit已经出到2.0了,但项目里的是1.9所以就来分析下它的源码。
要讲Retrofit,就必须要知道动态代理模式建造者模式。一开始使用的时,须要设置参数,如果没有设置,就用默认的。
restAdapter
=
new
RestAdapter.Builder()
.setEndpoint(""
)
.setLogLevel(RestAdapter.LogLevel.FULL)
.setClient(new
OkClient(getClient()))
.setLog(new
RestAdapter.Log() {
@Override
public void
log(String msg) {
Log.i(TAG,
msg);
}
})
.build();
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;
}
}
设置好参数之后,就只需要通过以下代码就能得到clazz接口的代理,这里必须是接口。以前对代理模式以为只有在有实现者时才能使用,而Retrofit告诉我不只如此,虽然没有实现类,但通过代理已经实现了每个方法,具体做什么,是由框架自己决定的。
restAdapter
.create(clazz);
从下面代码可以看出,重点是RestHandler.
public
<T>
T
create(Class<
T> service) {
Utils.validateServiceClass(service)
;
return (T)
Proxy. newProxyInstance(service.getClassLoader()
, new
Class<?>[] { service }
,
new RestHandler(getMethodInfoCache(service)))
;
}
首先是获取这个接口的缓存,如果已经解析过,就直接get,否则就新建个map,有个重点类RestMethodInfo。在RestMethodInfo里会判断两种情况,一种是返回值,一种是Callback回调,在这里只能使用一种。
/** Loads {@link
#responseObjectType}. Returns {@code
true} if method is synchronous. */
private
ResponseType
parseResponseType
() {
// Synchronous methods have a non-void return type.
// Observable methods have a return type of Observable.
Type returnType =
method.getGenericReturnType();
// Asynchronous methods should have a Callback type as the last argument.
Type lastArgType =
null;
Class<?>
lastArgClass
= null;
Type[] parameterTypes =
method.getGenericParameterTypes();
if (parameterTypes.
length
>
0
) {
Type typeToCheck = parameterTypes[parameterTypes.length
-
1]
;
lastArgType = typeToCheck
;
if (typeToCheck
instanceof
ParameterizedType) {
typeToCheck = ((ParameterizedType) typeToCheck).getRawType();
}
if
(typeToCheck
instanceof
Class) {
lastArgClass
= (Class<?>) typeToCheck
;
}
}
boolean
hasReturnType = returnType !=
void
.class;
boolean hasCallback =
lastArgClass
!= null
&& Callback.class.isAssignableFrom(lastArgClass
);
// Check for invalid configurations.
if
(hasReturnType && hasCallback) {
throw
methodError("Must have return type or Callback as last
argument, not both.")
;
}
if
(!hasReturnType && !hasCallback) {
throw
methodError("Must have either a return type or Callback
as last argument.");
}
if
(hasReturnType) {
if
(Platform.HAS_RX_JAVA)
{
Class rawReturnType = Types.getRawType
(returnType);
if (RxSupport.
isObservable(rawReturnType)) {
returnType = RxSupport.getObservableType
(returnType,
rawReturnType);
responseObjectType
=
getParameterUpperBound((ParameterizedType) returnType)
;
return ResponseType.
OBSERVABLE;
}
}
responseObjectType
= returnType;
return ResponseType.
OBJECT;
}
lastArgType = Types.getSupertype(lastArgType
,
Types.getRawType(lastArgType),
Callback.
class);
if (lastArgType
instanceof
ParameterizedType) {
responseObjectType
=
getParameterUpperBound((ParameterizedType) lastArgType)
;
return ResponseType.
VOID;
}
throw
methodError("Last parameter must be of type Callback<X>
or Callback<? super X>.")
;
}
暂时只关注CallBack方式,的只需要CallBackRunnable那段代码就行。
private class
RestHandler
implements
InvocationHandler {
private final
Map<Method,
RestMethodInfo>
methodDetailsCache;
RestHandler(Map<Method
,
RestMethodInfo> methodDetailsCache) {
this
.methodDetailsCache
= methodDetailsCache
;
}
@SuppressWarnings
("unchecked")
//
@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.")
;
}
if
(methodInfo.isObservable)
{
if
(rxSupport
==
null) {
if
(Platform.HAS_RX_JAVA)
{
rxSupport
=
new
RxSupport(
httpExecutor,
errorHandler
,
requestInterceptor
);
}
else
{
throw new
IllegalStateException("Observable method found but
no RxJava on classpath.")
;
}
}
return
rxSupport.createRequestObservable(new
RxSupport.Invoker() {
@Override
public
ResponseWrapper
invoke
(RequestInterceptor requestInterceptor) {
return
(ResponseWrapper) invokeRequest(requestInterceptor
,
methodInfo,
args
);
}
});
}
// 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);
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.
}
下面这个是CallBackRunnable的Run代码,注意到一开始就调用了obtainResponse(),也是在这里发送请求的,又交给了RestAdapter类执行。
@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)
;
}
});
}
}
在下面代码里,一开始就去解析这个方法的Annotation跟参数Annotation,得到这个url的请求方式,路径,头部等信息。再经过RequestBuider跟ServerUrl组装起来,最后交给Client请求,在这里目前是OKClient.这个是在PlatForm里得到的。最后就把返回结果转化后回调CallBack,这个是通过Retrofit的线程池来的:
callbackExecutor
.execute(new
Runnable() {
@Override
public void
run
() {
callback
.success((T)
wrapper.responseBody
,
wrapper.
response);
}
});
/**
* Execute an HTTP request.
*
* @return
HTTP response object of specified {@code
type} or {@code
null}.
* @throws
RetrofitError if any error occurs during the HTTP request.
*/
private
Object
invokeRequest(RequestInterceptor requestInterceptor
,
RestMethodInfo methodInfo
,
Object[] args) {
String url = null;
try {
methodInfo.init();
// Ensure all relevant method information has been loaded.
String serverUrl =
server.getUrl();
RequestBuilder requestBuilder =
new
RequestBuilder(serverUrl,
methodInfo,
converter
);
requestBuilder.setArguments(args)
;
requestInterceptor.intercept(requestBuilder)
;
Request request = requestBuilder.build()
;
url = request.getUrl()
;
if (!methodInfo.
isSynchronous) {
// If we are executing asynchronously then update the current thread with a useful name.
int
substrEnd = url.indexOf("?",
serverUrl.length());
if (substrEnd == -
1) {
substrEnd = url.length();
}
Thread.currentThread
().setName(THREAD_PREFIX
+ url.substring(serverUrl.length()
,
substrEnd));
}
if
(logLevel.log())
{
// Log the request data.
request = logAndReplaceRequest(
"HTTP",
request
,
args);
}
Object profilerObject =
null;
if (
profiler
!=
null
) {
profilerObject =
profiler
.beforeCall();
}
long
start = System.nanoTime();
Response response =
clientProvider.get().execute(request);
long elapsedTime = TimeUnit.NANOSECONDS
.toMillis(System.nanoTime() - start);
int statusCode = response.getStatus()
;
if (
profiler
!=
null
) {
RequestInformation requestInfo =
getRequestInfo(serverUrl,
methodInfo,
request)
;
//noinspection unchecked
profiler
.afterCall(requestInfo,
elapsedTime
,
statusCode,
profilerObject);
}
if
(logLevel.log())
{
// Log the response data.
response = logAndReplaceResponse(url
,
response,
elapsedTime);
}
Type type = methodInfo.responseObjectType
;
if (statusCode >=
200
&& statusCode <
300
) {
// 2XX == successful request
// Caller requested the raw Response object directly.
if
(type.equals(Response.class))
{
if
(!methodInfo.isStreaming)
{
// Read the entire stream and replace with one backed by a byte[].
response = Utils.
readBodyToBytesIfNecessary(response)
;
}
if
(methodInfo.isSynchronous)
{
return
response;
}
return new
ResponseWrapper(response,
response)
;
}
TypedInput body = response.getBody();
if (body ==
null) {
if
(methodInfo.isSynchronous)
{
return null;
}
return new
ResponseWrapper(response, null)
;
}
ExceptionCatchingTypedInput wrapped =
new
ExceptionCatchingTypedInput(body)
;
try {
Object convert =
converter
.fromBody(wrapped,
type)
;
logResponseBody(body
,
convert);
if (methodInfo.
isSynchronous) {
return
convert;
}
return new
ResponseWrapper(response,
convert)
;
}
catch
(ConversionException e) {
// If the underlying input stream threw an exception, propagate that rather than
// indicating that it was a conversion exception.
if
(wrapped.threwException()) {
throw
wrapped.getThrownException();
}
// The response body was partially read by the converter. Replace it with null.
response = Utils.
replaceResponseBody(response
, null);
throw RetrofitError.
conversionError(url
,
response,
converter,
type
,
e);
}
}
response = Utils.readBodyToBytesIfNecessary
(response);
throw RetrofitError.
httpError(url
,
response,
converter,
type)
;
}
catch
(RetrofitError e) {
throw
e;
// Pass through our own errors.
}
catch
(IOException e) {
if
(logLevel.log())
{
logException(e,
url);
}
throw
RetrofitError.networkError(url,
e);
}
catch
(Throwable t) {
if
(logLevel.log())
{
logException(t,
url);
}
throw
RetrofitError.unexpectedError(url,
t)
;
}
finally
{
if
(!methodInfo.isSynchronous)
{
Thread.currentThread
().setName(IDLE_THREAD_NAME)
;
}
}
}
}
要讲Retrofit,就必须要知道动态代理模式建造者模式。一开始使用的时,须要设置参数,如果没有设置,就用默认的。
restAdapter
=
new
RestAdapter.Builder()
.setEndpoint(""
)
.setLogLevel(RestAdapter.LogLevel.FULL)
.setClient(new
OkClient(getClient()))
.setLog(new
RestAdapter.Log() {
@Override
public void
log(String msg) {
Log.i(TAG,
msg);
}
})
.build();
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;
}
}
设置好参数之后,就只需要通过以下代码就能得到clazz接口的代理,这里必须是接口。以前对代理模式以为只有在有实现者时才能使用,而Retrofit告诉我不只如此,虽然没有实现类,但通过代理已经实现了每个方法,具体做什么,是由框架自己决定的。
restAdapter
.create(clazz);
从下面代码可以看出,重点是RestHandler.
public
<T>
T
create(Class<
T> service) {
Utils.validateServiceClass(service)
;
return (T)
Proxy. newProxyInstance(service.getClassLoader()
, new
Class<?>[] { service }
,
new RestHandler(getMethodInfoCache(service)))
;
}
首先是获取这个接口的缓存,如果已经解析过,就直接get,否则就新建个map,有个重点类RestMethodInfo。在RestMethodInfo里会判断两种情况,一种是返回值,一种是Callback回调,在这里只能使用一种。
/** Loads {@link
#responseObjectType}. Returns {@code
true} if method is synchronous. */
private
ResponseType
parseResponseType
() {
// Synchronous methods have a non-void return type.
// Observable methods have a return type of Observable.
Type returnType =
method.getGenericReturnType();
// Asynchronous methods should have a Callback type as the last argument.
Type lastArgType =
null;
Class<?>
lastArgClass
= null;
Type[] parameterTypes =
method.getGenericParameterTypes();
if (parameterTypes.
length
>
0
) {
Type typeToCheck = parameterTypes[parameterTypes.length
-
1]
;
lastArgType = typeToCheck
;
if (typeToCheck
instanceof
ParameterizedType) {
typeToCheck = ((ParameterizedType) typeToCheck).getRawType();
}
if
(typeToCheck
instanceof
Class) {
lastArgClass
= (Class<?>) typeToCheck
;
}
}
boolean
hasReturnType = returnType !=
void
.class;
boolean hasCallback =
lastArgClass
!= null
&& Callback.class.isAssignableFrom(lastArgClass
);
// Check for invalid configurations.
if
(hasReturnType && hasCallback) {
throw
methodError("Must have return type or Callback as last
argument, not both.")
;
}
if
(!hasReturnType && !hasCallback) {
throw
methodError("Must have either a return type or Callback
as last argument.");
}
if
(hasReturnType) {
if
(Platform.HAS_RX_JAVA)
{
Class rawReturnType = Types.getRawType
(returnType);
if (RxSupport.
isObservable(rawReturnType)) {
returnType = RxSupport.getObservableType
(returnType,
rawReturnType);
responseObjectType
=
getParameterUpperBound((ParameterizedType) returnType)
;
return ResponseType.
OBSERVABLE;
}
}
responseObjectType
= returnType;
return ResponseType.
OBJECT;
}
lastArgType = Types.getSupertype(lastArgType
,
Types.getRawType(lastArgType),
Callback.
class);
if (lastArgType
instanceof
ParameterizedType) {
responseObjectType
=
getParameterUpperBound((ParameterizedType) lastArgType)
;
return ResponseType.
VOID;
}
throw
methodError("Last parameter must be of type Callback<X>
or Callback<? super X>.")
;
}
暂时只关注CallBack方式,的只需要CallBackRunnable那段代码就行。
private class
RestHandler
implements
InvocationHandler {
private final
Map<Method,
RestMethodInfo>
methodDetailsCache;
RestHandler(Map<Method
,
RestMethodInfo> methodDetailsCache) {
this
.methodDetailsCache
= methodDetailsCache
;
}
@SuppressWarnings
("unchecked")
//
@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.")
;
}
if
(methodInfo.isObservable)
{
if
(rxSupport
==
null) {
if
(Platform.HAS_RX_JAVA)
{
rxSupport
=
new
RxSupport(
httpExecutor,
errorHandler
,
requestInterceptor
);
}
else
{
throw new
IllegalStateException("Observable method found but
no RxJava on classpath.")
;
}
}
return
rxSupport.createRequestObservable(new
RxSupport.Invoker() {
@Override
public
ResponseWrapper
invoke
(RequestInterceptor requestInterceptor) {
return
(ResponseWrapper) invokeRequest(requestInterceptor
,
methodInfo,
args
);
}
});
}
// 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);
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.
}
下面这个是CallBackRunnable的Run代码,注意到一开始就调用了obtainResponse(),也是在这里发送请求的,又交给了RestAdapter类执行。
@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)
;
}
});
}
}
在下面代码里,一开始就去解析这个方法的Annotation跟参数Annotation,得到这个url的请求方式,路径,头部等信息。再经过RequestBuider跟ServerUrl组装起来,最后交给Client请求,在这里目前是OKClient.这个是在PlatForm里得到的。最后就把返回结果转化后回调CallBack,这个是通过Retrofit的线程池来的:
callbackExecutor
.execute(new
Runnable() {
@Override
public void
run
() {
callback
.success((T)
wrapper.responseBody
,
wrapper.
response);
}
});
/**
* Execute an HTTP request.
*
* @return
HTTP response object of specified {@code
type} or {@code
null}.
* @throws
RetrofitError if any error occurs during the HTTP request.
*/
private
Object
invokeRequest(RequestInterceptor requestInterceptor
,
RestMethodInfo methodInfo
,
Object[] args) {
String url = null;
try {
methodInfo.init();
// Ensure all relevant method information has been loaded.
String serverUrl =
server.getUrl();
RequestBuilder requestBuilder =
new
RequestBuilder(serverUrl,
methodInfo,
converter
);
requestBuilder.setArguments(args)
;
requestInterceptor.intercept(requestBuilder)
;
Request request = requestBuilder.build()
;
url = request.getUrl()
;
if (!methodInfo.
isSynchronous) {
// If we are executing asynchronously then update the current thread with a useful name.
int
substrEnd = url.indexOf("?",
serverUrl.length());
if (substrEnd == -
1) {
substrEnd = url.length();
}
Thread.currentThread
().setName(THREAD_PREFIX
+ url.substring(serverUrl.length()
,
substrEnd));
}
if
(logLevel.log())
{
// Log the request data.
request = logAndReplaceRequest(
"HTTP",
request
,
args);
}
Object profilerObject =
null;
if (
profiler
!=
null
) {
profilerObject =
profiler
.beforeCall();
}
long
start = System.nanoTime();
Response response =
clientProvider.get().execute(request);
long elapsedTime = TimeUnit.NANOSECONDS
.toMillis(System.nanoTime() - start);
int statusCode = response.getStatus()
;
if (
profiler
!=
null
) {
RequestInformation requestInfo =
getRequestInfo(serverUrl,
methodInfo,
request)
;
//noinspection unchecked
profiler
.afterCall(requestInfo,
elapsedTime
,
statusCode,
profilerObject);
}
if
(logLevel.log())
{
// Log the response data.
response = logAndReplaceResponse(url
,
response,
elapsedTime);
}
Type type = methodInfo.responseObjectType
;
if (statusCode >=
200
&& statusCode <
300
) {
// 2XX == successful request
// Caller requested the raw Response object directly.
if
(type.equals(Response.class))
{
if
(!methodInfo.isStreaming)
{
// Read the entire stream and replace with one backed by a byte[].
response = Utils.
readBodyToBytesIfNecessary(response)
;
}
if
(methodInfo.isSynchronous)
{
return
response;
}
return new
ResponseWrapper(response,
response)
;
}
TypedInput body = response.getBody();
if (body ==
null) {
if
(methodInfo.isSynchronous)
{
return null;
}
return new
ResponseWrapper(response, null)
;
}
ExceptionCatchingTypedInput wrapped =
new
ExceptionCatchingTypedInput(body)
;
try {
Object convert =
converter
.fromBody(wrapped,
type)
;
logResponseBody(body
,
convert);
if (methodInfo.
isSynchronous) {
return
convert;
}
return new
ResponseWrapper(response,
convert)
;
}
catch
(ConversionException e) {
// If the underlying input stream threw an exception, propagate that rather than
// indicating that it was a conversion exception.
if
(wrapped.threwException()) {
throw
wrapped.getThrownException();
}
// The response body was partially read by the converter. Replace it with null.
response = Utils.
replaceResponseBody(response
, null);
throw RetrofitError.
conversionError(url
,
response,
converter,
type
,
e);
}
}
response = Utils.readBodyToBytesIfNecessary
(response);
throw RetrofitError.
httpError(url
,
response,
converter,
type)
;
}
catch
(RetrofitError e) {
throw
e;
// Pass through our own errors.
}
catch
(IOException e) {
if
(logLevel.log())
{
logException(e,
url);
}
throw
RetrofitError.networkError(url,
e);
}
catch
(Throwable t) {
if
(logLevel.log())
{
logException(t,
url);
}
throw
RetrofitError.unexpectedError(url,
t)
;
}
finally
{
if
(!methodInfo.isSynchronous)
{
Thread.currentThread
().setName(IDLE_THREAD_NAME)
;
}
}
}
}
相关文章推荐
- ios 算法题
- ZJPC-CTF_2015.11~12_writeup_by_GoldsNow
- [leetcode] 61. Rotate List
- python爬取返利网(完善)
- ViewDragHelper
- EditPlus64的安装配置
- Quartz定时任务时间设置
- [LeetCode]Convert Sorted List to Binary Search Tree
- Android如何设置背景透明、开关输入法、改变标题栏颜色、layer-list
- 一张图,给做嵌入式的小伙伴们打鸡血(∩_∩)
- 自己对PretranslateMessage的一点理解
- 支持向量机: Maximum Margin Classifier
- 利用Qt Assistant 定制帮助文档
- 一直以来伴随我的一些学习习惯(part1)
- Android SlidingMenu 开源项目 侧拉菜单的使用(详细配置)
- “杀京东”京东价格监控软件项目开发日志一
- 黑马程序员--C语言自学笔记---06函数简介和简单UNIX指令
- LeetCode——Valid Sudoku
- C++11原子操作性能测试
- 时间标签