今天来顺手分析一下谷歌的volley http通信框架。首先从github上 下载volley的源码,

然后新建你自己的工程以后 选择import module 然后选择volley。 最后还需要更改1个

配置文件

就是我选中的那句话。记得要加。不然会报错。把volley作为一个module 在你的项目中引用的原因是,因为我们要分析源码,需要测试我们心中所想。所以这么做是最方便的。

就相当于eclipse里面的工程依赖。

有关于volley 如何使用的教程 我就不在这写了,请自行谷歌,我们直接看源码。

  1. /*
  2. * Copyright (C) 2012 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16.  
  17. package com.android.volley.toolbox;
  18.  
  19. import android.content.Context;
  20. import android.content.pm.PackageInfo;
  21. import android.content.pm.PackageManager.NameNotFoundException;
  22. import android.net.http.AndroidHttpClient;
  23. import android.os.Build;
  24. import android.util.Log;
  25.  
  26. import com.android.volley.Network;
  27. import com.android.volley.RequestQueue;
  28.  
  29. import java.io.File;
  30.  
  31. public class Volley {
  32.  
  33. /**
  34. * Default on-disk cache directory.
  35. */
  36. private static final String DEFAULT_CACHE_DIR = "volley";
  37.  
  38. /**
  39. * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
  40. *
  41. * @param context A {@link Context} to use for creating the cache dir.
  42. * @param stack An {@link HttpStack} to use for the network, or null for default.
  43. * @return A started {@link RequestQueue} instance.
  44. */
  45. public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
  46. File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
  47. String userAgent = "volley/0";
  48. try {
  49. String packageName = context.getPackageName();
  50. PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
  51. userAgent = packageName + "/" + info.versionCode;
  52. } catch (NameNotFoundException e) {
  53. }
  54.  
  55. /**
  56. * 注意android 2.3之前一般用httpcilent进行网络交互 2.3包括2.3以后才使用HttpURLConnection
  57. * 这里面 实际上hurlstack就是 HttpURLConnection的一个变种
  58. */
  59. if (stack == null) {
  60. if (Build.VERSION.SDK_INT >= 9) {
  61.  
  62. stack = new HurlStack();
  63. } else {
  64. // Prior to Gingerbread, HttpUrlConnection was unreliable.
  65. // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
  66. stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
  67. }
  68. }
  69.  
  70. Network network = new BasicNetwork(stack);
  71.  
  72. //从下面这行语句来看,我们的RequestQueue 是由一个硬盘缓存和BasicNetwork 来组成的
  73. RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
  74. queue.start();
  75.  
  76. return queue;
  77. }
  78.  
  79. /**
  80. * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
  81. *
  82. * @param context A {@link Context} to use for creating the cache dir.
  83. * @return A started {@link RequestQueue} instance.
  84. */
  85. public static RequestQueue newRequestQueue(Context context) {
  86. return newRequestQueue(context, null);
  87. }
  88. }

我们知道一般我们在使用volley的时候 第一句话就是

  1. RequestQueue queue = Volley.newRequestQueue(this);
  2.  
  3. 实际上他的调用主要过程就是上面的45-77行。
  4.  
  5. 46-54 主要是在构造volley的缓存目录,实际上你最后打印出来可以发现volley的缓存目录 一般都在data/data/你程序的包名/volley下。这么做有一个好处就是一般情况下别的app是无法清除你的缓存的。除非root。有一些网络或者是图片框架的 磁盘缓存喜欢放在sd卡上,但是这么做有的时候会被误删除 不是很安全。
  6.  
  7. 59-70 主要是在构造http请求类,注意70行的BasicNetwork这个类,他就是最终发送http请求的地方。他的构造函数实际上是接受一个接口
  1. /*
  2. * Copyright (C) 2011 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16.  
  17. package com.android.volley.toolbox;
  18.  
  19. import com.android.volley.AuthFailureError;
  20. import com.android.volley.Request;
  21.  
  22. import org.apache.http.HttpResponse;
  23.  
  24. import java.io.IOException;
  25. import java.util.Map;
  26.  
  27. /**
  28. * An HTTP stack abstraction.
  29. */
  30. public interface HttpStack {
  31. /**
  32. * Performs an HTTP request with the given parameters.
  33. *
  34. * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
  35. * and the Content-Type header is set to request.getPostBodyContentType().</p>
  36. *
  37. * @param request the request to perform
  38. * @param additionalHeaders additional headers to be sent together with
  39. * {@link Request#getHeaders()}
  40. * @return the HTTP response
  41. */
  42. public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
  43. throws IOException, AuthFailureError;
  44.  
  45. }

所以在这里你们就应该明白 我们可以自己构造喜欢的http请求实体类,只要他实现了HttpStack这个接口即可,甚至于连NetWork我们都可以自己定义一个实现类。扩展性非常好。

73行 透露了两个信息,一个是volley使用的硬盘缓存类是DiskBasedCache,另外就是告诉我们requestqueue是从哪来的。

我们来简单看一下这个DiskBasedCache 因为代码过多 我只说重要的。他首先规定了一个最大的缓存空间 。

  1. /**
  2. * Default maximum disk usage in bytes.
  3. */
  4. private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

然后我们来看看缓存是怎么被存入的

  1. /**
  2. * Puts the entry with the specified key into the cache.
  3. */
  4. @Override
  5. public synchronized void put(String key, Entry entry) {
  6. //这个地方在存入硬盘缓存的时候会先看看是否超过最大容量如果超过要删除
  7. pruneIfNeeded(entry.data.length);
  8. File file = getFileForKey(key);
  9. try {
  10. BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
  11. CacheHeader e = new CacheHeader(key, entry);
  12. boolean success = e.writeHeader(fos);
  13. if (!success) {
  14. fos.close();
  15. VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
  16. throw new IOException();
  17. }
  18. fos.write(entry.data);
  19. fos.close();
  20. putEntry(key, e);
  21. return;
  22. } catch (IOException e) {
  23. }
  24. boolean deleted = file.delete();
  25. if (!deleted) {
  26. VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
  27. }
  28. }

这个地方要说一下 那个参数key 实际上就是我们request的cachekey 也就是url.put的操作其实很简单 我就不过多分析了,我们可以稍微看一下pruneIfNeed这个函数。

  1. /**
  2. * Map of the Key, CacheHeader pairs
  3. */
  4. private final Map<String, CacheHeader> mEntries =
  5. new LinkedHashMap<String, CacheHeader>(16, .75f, true);

这个mEntries就是存放我们硬盘缓存的地方,注意这边使用的是linkedHashMap 他的特点就是最近最少使用的在前面,所以你看下面的代码

在遍历删除时,好处就是删除的都是最少使用的。这个地方很多写硬盘缓存的人可能都想不到,可以学习一下。

  1. /**
  2. * Prunes the cache to fit the amount of bytes specified.
  3. *
  4. * @param neededSpace The amount of bytes we are trying to fit into the cache.
  5. */
  6. private void pruneIfNeeded(int neededSpace) {
  7. if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
  8. return;
  9. }
  10. if (VolleyLog.DEBUG) {
  11. VolleyLog.v("Pruning old cache entries.");
  12. }
  13.  
  14. long before = mTotalSize;
  15. int prunedFiles = 0;
  16. long startTime = SystemClock.elapsedRealtime();
  17.  
  18. Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
  19. while (iterator.hasNext()) {
  20. Map.Entry<String, CacheHeader> entry = iterator.next();
  21. CacheHeader e = entry.getValue();
  22. boolean deleted = getFileForKey(e.key).delete();
  23. if (deleted) {
  24. mTotalSize -= e.size;
  25. } else {
  26. VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
  27. e.key, getFilenameForKey(e.key));
  28. }
  29. iterator.remove();
  30. prunedFiles++;
  31.  
  32. if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
  33. break;
  34. }
  35. }
  36.  
  37. if (VolleyLog.DEBUG) {
  38. VolleyLog.v("pruned %d files, %d bytes, %d ms",
  39. prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
  40. }
  41. }

然后我们接着来看 RequestQueue是怎么构造的。

  1. public RequestQueue(Cache cache, Network network) {
  2. this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
  3. }
  1. /**
  2. * Number of network request dispatcher threads to start.
  3. */
  4. private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
  1. public RequestQueue(Cache cache, Network network, int threadPoolSize) {
  2. this(cache, network, threadPoolSize,
  3. new ExecutorDelivery(new Handler(Looper.getMainLooper())));
  4. }
  1. public RequestQueue(Cache cache, Network network, int threadPoolSize,
  2. ResponseDelivery delivery) {
  3. mCache = cache;
  4. mNetwork = network;
  5. mDispatchers = new NetworkDispatcher[threadPoolSize];
  6. mDelivery = delivery;
  7. }

具体的构造调用链就是这样的,这个地方就出现了2个新类,一个是NetworkDispatcher 他实际上就是工作线程 用来发http请求的。另外还有一个就是ExecutorDelivery 他是传递结果的,就是解析出来的流数据 由他来传递,注意他的写法 实际上表明了

这个消息传递者实在主线程里工作的。

然后看看我们的start函数 这边我就不多分析了 注释里都有 比较好理解。另外着重解释一下 那两个队列

  1. //PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,
  2. //而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.
  3. //对于volley而言 你可以重写request的getPriority方法 这个方法的返回值越高
  4. //在这个队列里的优先级就越高 就越排在前面 并非是FIFO队列
  5. private final PriorityBlockingQueue<Request<?>> mCacheQueue =
  6. new PriorityBlockingQueue<Request<?>>();
  7.  
  8. /**
  9. * The queue of requests that are actually going out to the network.
  10. */
  11. private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
  12. new PriorityBlockingQueue<Request<?>>();
  1. /**
  2. * Starts the dispatchers in this queue.
  3. */
  4. public void start() {
  5. stop(); // Make sure any currently running dispatchers are stopped.
  6. // Create the cache dispatcher and start it.
  7. mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
  8. //启动了一个线程 这个线程是缓存线程 所以缓存线程在volley中只有一个
  9. mCacheDispatcher.start();
  10.  
  11. // Create network dispatchers (and corresponding threads) up to the pool size.
  12. //DEFAULT_NETWORK_THREAD_POOL_SIZE 这个默认值是4 所以mDispatchers的默认大小是4
  13. //这个地方就是在给这个数组赋值 赋值结束以后就直接启动了
  14. for (int i = 0; i < mDispatchers.length; i++) {
  15. NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
  16. mCache, mDelivery);
  17. mDispatchers[i] = networkDispatcher;
  18. networkDispatcher.start();
  19. }
  20. //所以这个函数结束的时候volley一共启动了5个子线程 () 这个地方要注意他们2是公用的mNetworkQueue这个队列
  21. //mCache 磁盘缓存 mDelivery用于分发处理好的结果 mDelivery就是ExecutorDelivery的对象
  22. }

当我们构造完这个队列以后,我们就会构造一个request 然后把这个request对象 add到这个队列里。然后就能看到服务器返回的数据。

那我们就来最终看看add函数 做了哪些操作。这个地方很多人搞不清mWaitingRequests是干嘛的,实际上他也是缓存,只不过他缓存的东西比较特殊。

  1. /**
  2. * 这个地方mWaitingRequests是一个哈希表,注意这个哈希表的key 为request的url,
  3. * 而value则是一个队列,他的作用是 如果请求的url是一样的,那么就把这些请求
  4. * 也就是request放到一个队列里面,举例来说,如果abc三条request的url是一样的,
  5. * 那么假设a是第一个被add的,那么后面的bc将会放到这个map里value里的队列里面
  6. * bc并不会得到真正的执行。真正执行的只有a
  7. * 这么做的好处 其实显而易见 ,比如我们界面上点击某个按钮我们不想让他点击的时候
  8. * 弹进度狂,只想在后台发请求,但是如果你这么做的话,如果用户连续点击误操作,
  9. * 就会一直发请求会浪费很多流量,而在这里 有这个hashmap的保护,多数情况下
  10. * 大部分的重复url请求会被屏蔽掉,只有一开始的才会得到执行
  11. */
  12. private final Map<String, Queue<Request<?>>> mWaitingRequests =
  13. new HashMap<String, Queue<Request<?>>>();
  1. /**
  2. * 实际上这个地方add函数并没有执行网络请求的任何操作,
  3. *
  4. */
  5. public <T> Request<T> add(Request<T> request) {
  6. // Tag the request as belonging to this queue and add it to the set of current requests.
  7. request.setRequestQueue(this);
  8. synchronized (mCurrentRequests) {
  9. mCurrentRequests.add(request);
  10. }
  11.  
  12. // Process requests in the order they are added.
  13. request.setSequence(getSequenceNumber());
  14. request.addMarker("add-to-queue");
  15. // If the request is uncacheable, skip the cache queue and go straight to the network.
  16. //判断是否能缓存 不能缓存 就直接加入mNetworkQueue队列执行 能缓存的话就放在缓存队列里面
  17. //注意这个地方的标志位是我们手动可以设置的,也就是说你写的request想让他使用缓存机制就
  18. //使用默认的,不想使用缓存机制 可以手动设置shouldCache为false然后这条request 会直接进入
  19. //网络请求队列
  20. if (!request.shouldCache()) {
  21. mNetworkQueue.add(request);
  22. return request;
  23. }
  24.  
  25. // Insert request into stage if there's already a request with the same cache key in flight.
  26. synchronized (mWaitingRequests) {
  27. String cacheKey = request.getCacheKey();
  28. if (mWaitingRequests.containsKey(cacheKey)) {
  29. // There is already a request in flight. Queue up.
  30. Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
  31. if (stagedRequests == null) {
  32. stagedRequests = new LinkedList<Request<?>>();
  33. }
  34. stagedRequests.add(request);
  35. mWaitingRequests.put(cacheKey, stagedRequests);
  36. if (VolleyLog.DEBUG) {
  37. VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
  38. }
  39. } else {
  40. // Insert 'null' queue for this cacheKey, indicating there is now a request in
  41. // flight.
  42. mWaitingRequests.put(cacheKey, null);
  43. mCacheQueue.add(request);
  44. }
  45. return request;
  46. }
  47. }

所以这个地方mWaitngRequests的作用就是 如果你在极短的时间内访问同一个url,volley是不会帮你每次都发请求的,只有最初的会得到请求,后面的是不会有任何操作的。

这个地方大家可以写一段代码去试试,我写过一个代码 就是add一个StringRequst的数组,数组大小为20,然后抓包看,实际上最终发的请求 走流量的也就是3-4个,其他都是

直接返回的结果,但是你如果更改了shouldCache的标志位 那就是直接发送请求,会请求20次,所以这个地方要注意。当然你在实际使用的时候 要不要使用这个机制 都是看实际效果的。

那假设 我们是使用的shouldcache默认值,然后这个request 被add了进来(在这之前相同url的request还没有被add) 那这个时候我们真正的缓存队列 mCacheQueue队列就

add了一个request值,所以这个时候我们去看看我们的缓存线程都干了些什么

  1. @Override
  2. public void run() {
  3. if (DEBUG) VolleyLog.v("start new dispatcher");
  4. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  5.  
  6. // Make a blocking call to initialize the cache.
  7. mCache.initialize();
  8. //这里证明缓存线程是一直无限执行下去的
  9. while (true) {
  10. try {
  11. // Get a request from the cache triage queue, blocking until
  12. // at least one is available.
  13. //从缓存队列里面取出一个request
  14. final Request<?> request = mCacheQueue.take();
  15. request.addMarker("cache-queue-take");
  16.  
  17. // If the request has been canceled, don't bother dispatching it.
  18. if (request.isCanceled()) {
  19. request.finish("cache-discard-canceled");
  20. continue;
  21. }
  22.  
  23. // Attempt to retrieve this item from cache.
  24. Cache.Entry entry = mCache.get(request.getCacheKey());
  25. //如果为空就放到请求队列中
  26. if (entry == null) {
  27. request.addMarker("cache-miss");
  28. // Cache miss; send off to the network dispatcher.
  29. mNetworkQueue.put(request);
  30. continue;
  31. }
  32.  
  33. // If it is completely expired, just send it to the network.
  34. //如果entry不为空的话 判断这个缓存是否过期 如果过期也要放到网络请求队列中
  35. if (entry.isExpired()) {
  36. request.addMarker("cache-hit-expired");
  37. request.setCacheEntry(entry);
  38. mNetworkQueue.put(request);
  39. continue;
  40. }
  41.  
  42. //这下面的就说明不需要发网络请求 可以直接解析了
  43. // We have a cache hit; parse its data for delivery back to the request.
  44. request.addMarker("cache-hit");
  45. Response<?> response = request.parseNetworkResponse(
  46. new NetworkResponse(entry.data, entry.responseHeaders));
  47. request.addMarker("cache-hit-parsed");
  48.  
  49. //判断是否过期以后还要判断是否需要刷新
  50. if (!entry.refreshNeeded()) {
  51. // Completely unexpired cache hit. Just deliver the response.
  52. //不需要刷新就直接解析
  53. mDelivery.postResponse(request, response);
  54. } else {
  55. //如果需要刷新的话则必须重新将这个request放到mNetworkQueue里面去请求一次
  56. // Soft-expired cache hit. We can deliver the cached response,
  57. // but we need to also send the request to the network for
  58. // refreshing.
  59. request.addMarker("cache-hit-refresh-needed");
  60. request.setCacheEntry(entry);
  61.  
  62. // Mark the response as intermediate.
  63. response.intermediate = true;
  64.  
  65. // Post the intermediate response back to the user and have
  66. // the delivery then forward the request along to the network.
  67. mDelivery.postResponse(request, response, new Runnable() {
  68. @Override
  69. public void run() {
  70. try {
  71. mNetworkQueue.put(request);
  72. } catch (InterruptedException e) {
  73. // Not much we can do about this.
  74. }
  75. }
  76. });
  77. }
  78.  
  79. } catch (InterruptedException e) {
  80. // We may have been interrupted because it was time to quit.
  81. if (mQuit) {
  82. return;
  83. }
  84. continue;
  85. }
  86. }
  87. }

这个地方注释也比较多,所以大概的流程就是 先判断是否有硬盘缓存,如果没有就直接放到network队列里去,如果有的话 还要判断是否过期,没过期就接着判断是否需要刷新。这边逻辑其实很简单。

所谓的 "过期"  "刷新” 什么的 实际上就是对http协议里面的一些字段的判断罢了,这个地方大家在使用的时候一定要注意和你们服务器的情况结合来使用,否则这边volley的缓存机制 会失效。

比方说一个最简单的场景 很多公司图片缓存的时候 都是根据url来判断 本地是否有缓存图片的,但是volley在这个地方 不是使用的url来判断 他是使用http 协议里面 头部的那些信息来判断的。

这种写法很规范 很标准,但是缺陷就是对于那些不遵守http协议的服务器来说 这边代码是要自己重新写一遍的。

45-46行就是解析出response用的,调用的是虚方法

  1. abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

由此可见是由他的子类 来完成的 我们就看看子类stringrequest是怎么做的。

  1. @Override
  2. protected Response<String> parseNetworkResponse(NetworkResponse response) {
  3. String parsed;
  4. try {
  5. parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
  6. } catch (UnsupportedEncodingException e) {
  7. parsed = new String(response.data);
  8. }
  9. return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
  10. }、

然后我们接着看 如果缓存有效,那我们是如何解析出来然后传递消息的。

  1. /*
  2. * Copyright (C) 2011 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16.  
  17. package com.android.volley;
  18.  
  19. import android.os.Handler;
  20.  
  21. import java.util.concurrent.Executor;
  22.  
  23. /**
  24. * Delivers responses and errors.
  25. */
  26. public class ExecutorDelivery implements ResponseDelivery {
  27. /** Used for posting responses, typically to the main thread. */
  28. private final Executor mResponsePoster;
  29.  
  30. /**
  31. * Creates a new response delivery interface.
  32. * @param handler {@link Handler} to post responses on
  33. */
  34. public ExecutorDelivery(final Handler handler) {
  35. // Make an Executor that just wraps the handler.
  36. mResponsePoster = new Executor() {
  37. @Override
  38. public void execute(Runnable command) {
  39. handler.post(command);
  40. }
  41. };
  42. }
  43.  
  44. /**
  45. * Creates a new response delivery interface, mockable version
  46. * for testing.
  47. * @param executor For running delivery tasks
  48. */
  49. public ExecutorDelivery(Executor executor) {
  50. mResponsePoster = executor;
  51. }
  52.  
  53. @Override
  54. public void postResponse(Request<?> request, Response<?> response) {
  55. postResponse(request, response, null);
  56. }
  57.  
  58. @Override
  59. public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
  60. request.markDelivered();
  61. request.addMarker("post-response");
  62. mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
  63. }
  64.  
  65. @Override
  66. public void postError(Request<?> request, VolleyError error) {
  67. request.addMarker("post-error");
  68. Response<?> response = Response.error(error);
  69. mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
  70. }
  71.  
  72. /**
  73. * A Runnable used for delivering network responses to a listener on the
  74. * main thread.
  75. */
  76. @SuppressWarnings("rawtypes")
  77. private class ResponseDeliveryRunnable implements Runnable {
  78. private final Request mRequest;
  79. private final Response mResponse;
  80. private final Runnable mRunnable;
  81.  
  82. public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
  83. mRequest = request;
  84. mResponse = response;
  85. mRunnable = runnable;
  86. }
  87.  
  88. @SuppressWarnings("unchecked")
  89. @Override
  90. public void run() {
  91. // If this request has canceled, finish it and don't deliver.
  92. if (mRequest.isCanceled()) {
  93. mRequest.finish("canceled-at-delivery");
  94. return;
  95. }
  96.  
  97. // Deliver a normal response or error, depending.
  98. if (mResponse.isSuccess()) {
  99. //这个地方就能看出来这是最终调用子类的方法去传递解析好的数据
  100. mRequest.deliverResponse(mResponse.result);
  101. } else {
  102. mRequest.deliverError(mResponse.error);
  103. }
  104.  
  105. // If this is an intermediate response, add a marker, otherwise we're done
  106. // and the request can be finished.
  107. if (mResponse.intermediate) {
  108. mRequest.addMarker("intermediate-response");
  109. } else {
  110. mRequest.finish("done");
  111. }
  112.  
  113. // If we have been provided a post-delivery runnable, run it.
  114. if (mRunnable != null) {
  115. mRunnable.run();
  116. }
  117. }
  118. }
  119. }

实际上消息的传递 就是在77-119这个runnable里面做的。注意100行代码 那个deliverResponse实际上是一个虚方法。是留给子类来重写的。

我们就看一下stringrequest的这个方法吧

  1. //可以看到这request的子类 StringRequest 的deliverResponse 方法里面是一个回调
  2. //并没有真正的实现它
  3. @Override
  4. protected void deliverResponse(String response) {
  5. mListener.onResponse(response);
  6. }
  1. private final Listener<String> mListener;

这个地方实际上就是回调了,留给我们自己写的。

比如

  1. new StringRequest(Request.Method.GET, url,
  2. new Response.Listener<String>() {
  3. @Override
  4. public void onResponse(String response) {
  5. // Display the first 500 characters of the response string.
  6. }
  7. }, new Response.ErrorListener() {
  8. @Override
  9. public void onErrorResponse(VolleyError error) {
  10.  
  11. }
  12. }
  13. );

到此,缓存处理线程的流程就介绍完毕了,我们最后再看看实际上的工作线程NetWorkDispatcher

  1. @Override
  2. public void run() {
  3. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  4. while (true) {
  5. long startTimeMs = SystemClock.elapsedRealtime();
  6. Request<?> request;
  7. try {
  8. // Take a request from the queue.
  9. request = mQueue.take();
  10. } catch (InterruptedException e) {
  11. // We may have been interrupted because it was time to quit.
  12. if (mQuit) {
  13. return;
  14. }
  15. continue;
  16. }
  17.  
  18. try {
  19. request.addMarker("network-queue-take");
  20.  
  21. // If the request was cancelled already, do not perform the
  22. // network request.
  23. if (request.isCanceled()) {
  24. request.finish("network-discard-cancelled");
  25. continue;
  26. }
  27.  
  28. addTrafficStatsTag(request);
  29.  
  30. // Perform the network request.
  31. //这个地方就是实际发请求的地方
  32. NetworkResponse networkResponse = mNetwork.performRequest(request);
  33. request.addMarker("network-http-complete");
  34.  
  35. // If the server returned 304 AND we delivered a response already,
  36. // we're done -- don't deliver a second identical response.
  37. if (networkResponse.notModified && request.hasHadResponseDelivered()) {
  38. request.finish("not-modified");
  39. continue;
  40. }
  41.  
  42. // Parse the response here on the worker thread.
  43. //这个地方要注意 networkResponse是交给request来解析的 但是我们的request会有很多子类
  44. //parseNetworkResponse的方法重写以后在这里最终解析数据
  45. Response<?> response = request.parseNetworkResponse(networkResponse);
  46. request.addMarker("network-parse-complete");
  47.  
  48. // Write to cache if applicable.
  49. // TODO: Only update cache metadata instead of entire record for 304s.
  50. if (request.shouldCache() && response.cacheEntry != null) {
  51. mCache.put(request.getCacheKey(), response.cacheEntry);
  52. request.addMarker("network-cache-written");
  53. }
  54.  
  55. // Post the response back.
  56. request.markDelivered();
  57. //回调传递数据
  58. mDelivery.postResponse(request, response);
  59. } catch (VolleyError volleyError) {
  60. volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
  61. parseAndDeliverNetworkError(request, volleyError);
  62. } catch (Exception e) {
  63. VolleyLog.e(e, "Unhandled exception %s", e.toString());
  64. VolleyError volleyError = new VolleyError(e);
  65. volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
  66. mDelivery.postError(request, volleyError);
  67. }
  68. }
  69. }

这个地方也非常简单,主要是看32行 最终发请求,这个地方是交给basicnetwork来发送的,我们来看

  1. @Override
  2. public NetworkResponse performRequest(Request<?> request) throws VolleyError {
  3. long requestStart = SystemClock.elapsedRealtime();
  4. while (true) {
  5. HttpResponse httpResponse = null;
  6. byte[] responseContents = null;
  7. Map<String, String> responseHeaders = Collections.emptyMap();
  8. try {
  9. // Gather headers.
  10. Map<String, String> headers = new HashMap<String, String>();
  11. addCacheHeaders(headers, request.getCacheEntry());
  12. httpResponse = mHttpStack.performRequest(request, headers);
  13. StatusLine statusLine = httpResponse.getStatusLine();
  14. int statusCode = statusLine.getStatusCode();
  15.  
  16. responseHeaders = convertHeaders(httpResponse.getAllHeaders());
  17. // Handle cache validation.
  18. //自动上次请求后 请求的网页没有修改过 服务器返回网页时 就不会返回实际内容
  19. if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
  20.  
  21. Entry entry = request.getCacheEntry();
  22. if (entry == null) {
  23. return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
  24. responseHeaders, true,
  25. SystemClock.elapsedRealtime() - requestStart);
  26. }
  27.  
  28. // A HTTP 304 response does not have all header fields. We
  29. // have to use the header fields from the cache entry plus
  30. // the new ones from the response.
  31. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
  32. entry.responseHeaders.putAll(responseHeaders);
  33. return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
  34. entry.responseHeaders, true,
  35. SystemClock.elapsedRealtime() - requestStart);
  36. }
  37.  
  38. // Some responses such as 204s do not have content. We must check.
  39. if (httpResponse.getEntity() != null) {
  40. responseContents = entityToBytes(httpResponse.getEntity());
  41. } else {
  42. // Add 0 byte response as a way of honestly representing a
  43. // no-content request.
  44. responseContents = new byte[0];
  45. }
  46.  
  47. // if the request is slow, log it.
  48. long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
  49. logSlowRequests(requestLifetime, request, responseContents, statusLine);
  50.  
  51. if (statusCode < 200 || statusCode > 299) {
  52. throw new IOException();
  53. }
  54. return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
  55. SystemClock.elapsedRealtime() - requestStart);
  56. } catch (SocketTimeoutException e) {
  57. attemptRetryOnException("socket", request, new TimeoutError());
  58. } catch (ConnectTimeoutException e) {
  59. attemptRetryOnException("connection", request, new TimeoutError());
  60. } catch (MalformedURLException e) {
  61. throw new RuntimeException("Bad URL " + request.getUrl(), e);
  62. } catch (IOException e) {
  63. int statusCode = 0;
  64. NetworkResponse networkResponse = null;
  65. if (httpResponse != null) {
  66. statusCode = httpResponse.getStatusLine().getStatusCode();
  67. } else {
  68. throw new NoConnectionError(e);
  69. }
  70. VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
  71. if (responseContents != null) {
  72. networkResponse = new NetworkResponse(statusCode, responseContents,
  73. responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
  74. if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
  75. statusCode == HttpStatus.SC_FORBIDDEN) {
  76. attemptRetryOnException("auth",
  77. request, new AuthFailureError(networkResponse));
  78. } else {
  79. // TODO: Only throw ServerError for 5xx status codes.
  80. throw new ServerError(networkResponse);
  81. }
  82. } else {
  83. throw new NetworkError(networkResponse);
  84. }
  85. }
  86. }
  87. }

所以这个地方 我们要注意2点。

第一点,这个函数是最终我们发起请求的地方也是解析出response的地方,但是要注意他返回的是NetworkResponse。你回过头看45行发现通常是由子类的parseNetworkResponse

来把我们basicnetwork得到的networkresponse 解析成 Response<?> response 这个泛型的! 这个顺序要理清楚。比如你看strinrequst的这个方法

  1. @Override
  2. protected Response<String> parseNetworkResponse(NetworkResponse response) {
  3. String parsed;
  4. try {
  5. parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
  6. } catch (UnsupportedEncodingException e) {
  7. parsed = new String(response.data);
  8. }
  9. Log.v("burning", "parsed=" + parsed);
  10. return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
  11. }

再看看JSONrequest这个方法

  1. @Override
  2. protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
  3. try {
  4. String jsonString = new String(response.data,
  5. HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
  6. return Response.success(new JSONObject(jsonString),
  7. HttpHeaderParser.parseCacheHeaders(response));
  8. } catch (UnsupportedEncodingException e) {
  9. return Response.error(new ParseError(e));
  10. } catch (JSONException je) {
  11. return Response.error(new ParseError(je));
  12. }
  13. }

就能明白这个调用过程的先后顺序了。

50-52行 就是操作硬盘缓存的。实际上你看他调用链最终就是由networkresponse来解析出缓存所需要的entry 这个地方就是对http标准协议里 头部许多字段的解析了,

根据解析出来的值判断缓存是否需要刷新 是否过期等,在你自定义volley的时候 需要根据服务器的实际情况来重写这一部分

  1. public class HttpHeaderParser {
  2.  
  3. /**
  4. * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
  5. *
  6. * @param response The network response to parse headers from
  7. * @return a cache entry for the given response, or null if the response is not cacheable.
  8. */
  9. public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
  10. long now = System.currentTimeMillis();
  11.  
  12. Map<String, String> headers = response.headers;
  13.  
  14. long serverDate = 0;
  15. long lastModified = 0;
  16. long serverExpires = 0;
  17. long softExpire = 0;
  18. long finalExpire = 0;
  19. long maxAge = 0;
  20. long staleWhileRevalidate = 0;
  21. boolean hasCacheControl = false;
  22. boolean mustRevalidate = false;
  23.  
  24. String serverEtag = null;
  25. String headerValue;
  26.  
  27. headerValue = headers.get("Date");
  28. if (headerValue != null) {
  29. serverDate = parseDateAsEpoch(headerValue);
  30. }
  31.  
  32. headerValue = headers.get("Cache-Control");
  33. if (headerValue != null) {
  34. hasCacheControl = true;
  35. String[] tokens = headerValue.split(",");
  36. for (int i = 0; i < tokens.length; i++) {
  37. String token = tokens[i].trim();
  38. if (token.equals("no-cache") || token.equals("no-store")) {
  39. return null;
  40. } else if (token.startsWith("max-age=")) {
  41. try {
  42. maxAge = Long.parseLong(token.substring(8));
  43. } catch (Exception e) {
  44. }
  45. } else if (token.startsWith("stale-while-revalidate=")) {
  46. try {
  47. staleWhileRevalidate = Long.parseLong(token.substring(23));
  48. } catch (Exception e) {
  49. }
  50. } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
  51. mustRevalidate = true;
  52. }
  53. }
  54. }
  55.  
  56. headerValue = headers.get("Expires");
  57. if (headerValue != null) {
  58. serverExpires = parseDateAsEpoch(headerValue);
  59. }
  60.  
  61. headerValue = headers.get("Last-Modified");
  62. if (headerValue != null) {
  63. lastModified = parseDateAsEpoch(headerValue);
  64. }
  65.  
  66. serverEtag = headers.get("ETag");
  67.  
  68. // Cache-Control takes precedence over an Expires header, even if both exist and Expires
  69. // is more restrictive.
  70. if (hasCacheControl) {
  71. softExpire = now + maxAge * 1000;
  72. finalExpire = mustRevalidate
  73. ? softExpire
  74. : softExpire + staleWhileRevalidate * 1000;
  75. } else if (serverDate > 0 && serverExpires >= serverDate) {
  76. // Default semantic for Expire header in HTTP specification is softExpire.
  77. softExpire = now + (serverExpires - serverDate);
  78. finalExpire = softExpire;
  79. }
  80.  
  81. Cache.Entry entry = new Cache.Entry();
  82. entry.data = response.data;
  83. entry.etag = serverEtag;
  84. entry.softTtl = softExpire;
  85. entry.ttl = finalExpire;
  86. entry.serverDate = serverDate;
  87. entry.lastModified = lastModified;
  88. entry.responseHeaders = headers;
  89.  
  90. return entry;
  91. }

第二点也是额外我想多讲的一点 对于basicnetwork的performrequest方法来说:

他们的超时策略是很重要的东西,这个对于我们app的定制化 和性能体验来说非常重要,

大家一定要弄明白。在volley中默认是会使用 默认的超时策略的。代码如下:

你看requst 这个类的构造方法

  1. public Request(int method, String url, Response.ErrorListener listener) {
  2. mMethod = method;
  3. mUrl = url;
  4. mErrorListener = listener;
  5. setRetryPolicy(new DefaultRetryPolicy());
  6.  
  7. mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
  8. }

我们就去看看volley 给我们提供的这个默认超时策略

  1. /*
  2. * Copyright (C) 2011 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16.  
  17. package com.android.volley;
  18.  
  19. /**
  20. * Default retry policy for requests.
  21. */
  22. public class DefaultRetryPolicy implements RetryPolicy {
  23. /** The current timeout in milliseconds. */
  24. private int mCurrentTimeoutMs;
  25.  
  26. /** The current retry count. */
  27. private int mCurrentRetryCount;
  28.  
  29. /** The maximum number of attempts. */
  30. private final int mMaxNumRetries;
  31.  
  32. /** The backoff multiplier for the policy. */
  33. private final float mBackoffMultiplier;
  34.  
  35. /** The default socket timeout in milliseconds */
  36. public static final int DEFAULT_TIMEOUT_MS = 2500;
  37.  
  38. /** The default number of retries */
  39. public static final int DEFAULT_MAX_RETRIES = 1;
  40.  
  41. /** The default backoff multiplier */
  42. public static final float DEFAULT_BACKOFF_MULT = 1f;
  43.  
  44. /**
  45. * Constructs a new retry policy using the default timeouts.
  46. */
  47. public DefaultRetryPolicy() {
  48. this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
  49. }
  50.  
  51. /**
  52. * Constructs a new retry policy.
  53. * @param initialTimeoutMs The initial timeout for the policy.
  54. * @param maxNumRetries The maximum number of retries.
  55. * @param backoffMultiplier Backoff multiplier for the policy.
  56. */
  57. public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
  58. mCurrentTimeoutMs = initialTimeoutMs;
  59. mMaxNumRetries = maxNumRetries;
  60. mBackoffMultiplier = backoffMultiplier;
  61. }
  62.  
  63. /**
  64. * Returns the current timeout.
  65. */
  66. @Override
  67. public int getCurrentTimeout() {
  68. return mCurrentTimeoutMs;
  69. }
  70.  
  71. /**
  72. * Returns the current retry count.
  73. */
  74. @Override
  75. public int getCurrentRetryCount() {
  76. return mCurrentRetryCount;
  77. }
  78.  
  79. /**
  80. * Returns the backoff multiplier for the policy.
  81. */
  82. public float getBackoffMultiplier() {
  83. return mBackoffMultiplier;
  84. }
  85.  
  86. /**
  87. * Prepares for the next retry by applying a backoff to the timeout.
  88. * @param error The error code of the last attempt.
  89. */
  90. @Override
  91. public void retry(VolleyError error) throws VolleyError {
  92. mCurrentRetryCount++;
  93. mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
  94. if (!hasAttemptRemaining()) {
  95. throw error;
  96. }
  97. }
  98.  
  99. /**
  100. * Returns true if this policy has attempts remaining, false otherwise.
  101. */
  102. protected boolean hasAttemptRemaining() {
  103. return mCurrentRetryCount <= mMaxNumRetries;
  104. }
  105. }

他规定了超时时间 超时以后尝试重连的次数等。我就随便分析一下 在超时策略里定义的超时时间 是怎么影响最终http请求的。

超时策略类里面 的这个函数

  1. /**
  2. * Returns the current timeout.
  3. */
  4. @Override
  5. public int getCurrentTimeout() {
  6. return mCurrentTimeoutMs;
  7. }

往下走反映到request里的这个函数

  1. public final int getTimeoutMs() {
  2. return mRetryPolicy.getCurrentTimeout();
  3. }

是final函数,我们来看看他被哪些类调用了

然后我们随便打开1个

  1. private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
  2. HttpURLConnection connection = createConnection(url);
  3.  
  4. int timeoutMs = request.getTimeoutMs();
  5. connection.setConnectTimeout(timeoutMs);
  6. connection.setReadTimeout(timeoutMs);
  7. connection.setUseCaches(false);
  8. connection.setDoInput(true);

到这调用链就分析完毕。

之所以要额外分析超时策略 是因为谷歌自带的超时策略 并不是很好用 他默认的超时时间是2.5s  如果你网络情况比较差 又在上传图片的话

这个2.5s超时策略是完全不够的,此时就会引发很多bug了。所以这个地方要单独出来讲一下。

至此,volley 的 大部分源码都分析完毕了。建议读者在看的时候 要多写demo 打日志 来验证想法,除此之外,要真正读懂volley源码

还需要在java 并发编程那边下点功夫~~

  1.  

Android Volley源码分析的更多相关文章

  1. [Android]Volley源码分析(五)

    前面几篇通过源码分析了Volley是怎样进行请求调度及请求是如何被实际执行的,这篇最后来看下请求结果是如何交付给请求者的(一般是Android的UI主线程). 类图:

  2. [Android]Volley源码分析(三)

    上篇看了关于Request的源码,这篇接着来看下RequestQueue的源码. RequestQueue类图:

  3. [Android]Volley源码分析(二)

    上一篇介绍了Volley的使用,主要接触了Request与RequestQueue这两个类,这篇就来了解一下这两个类的具体实现. Request类图:

  4. [Android]Volley源码分析(四)

    上篇中有提到NetworkDispatcher是通过mNetwork(Network类型)来进行网络访问的,现在来看一下关于Network是如何进行网络访问的. Network部分的类图:

  5. [Android]Volley源码分析(一)

    一. 如何使用Volley? 1. 首先定义一个RequestManager类,用来在Android程序启动时对Volley进行初始化.RequestManager为单例类,因为只有在程序启动时调用, ...

  6. Android Volley源码分析及扩展

    转载请标明出处: http://www.cnblogs.com/why168888/p/6681232.html 本文出自:[Edwin博客园] Volley 介绍 Android系统中主要提供了两种 ...

  7. Volley源码分析(2)----ImageLoader

    一:imageLoader 先来看看如何使用imageloader: public void showImg(View view){ ImageView imageView = (ImageView) ...

  8. Appium Android Bootstrap源码分析之启动运行

    通过前面的两篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>和<Appium Android Bootstrap源码分析之命令解析 ...

  9. Appium Android Bootstrap源码分析之命令解析执行

    通过上一篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>我们知道了Appium从pc端发送过来的命令如果是控件相关的话,最终目标控件在b ...

随机推荐

  1. poj 3159(差分约束经典题)

    题目链接:http://poj.org/problem?id=3159思路:题目意思很简单,都与给定的条件dist[b]-dist[a]<=c,求dist[n]-dist[1]的最大值,显然这是 ...

  2. java编译错误:varargs 方法的非 varargs 调用

    转自:http://www.blogjava.net/ideame/archive/2007/03/23/105849.html 警告: 最后一个参数使用了不准确的变量类型的 varargs 方法的非 ...

  3. lintcode:数字组合III

    数字组合III 组给出两个整数n和k,返回从1......n中选出的k个数的组合. 您在真实的面试中是否遇到过这个题? Yes 样例 例如 n = 4 且 k = 2 返回的解为: [[2,4],[3 ...

  4. eclipse导入的工程前面有感叹号是什么意思

    1.尤其是从其他地方拷贝来并且直接加载的工程,刚打开往往会看到工程的图标上有个红色的感叹号,这是因为build path 出错了,里面有缺失或者无法找到的包. 2. 原因:显示红色感叹号是因为jar包 ...

  5. React-非dom属性-ref标签

    <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8& ...

  6. ubuntu下搭建cocos2dx编程环境-下

         前两篇介绍了cocos2d-x 下linux开发环境配置和android 环境配置问题.在这其中遇到很多问题,所以最后一篇分享一下在处理这些问题时,我是如何解决的,是怎么想的.同时总结一些解 ...

  7. CentOS目录树详细解释

    [sdm_download id=”292″ fancy=”1″] /boot 该目录默认下存放的是Linux的启动文件和内核. initramfs-* 系统启动时的模块供应的主要来源 启动系统所需加 ...

  8. SQL Server ->> 分区表上创建唯一分区索引

    今天在读<Oracle高级SQL编程>这本书的时候,在关于Oracle的全局索引的章节里面有一段讲到如果对一张分区表创建一条唯一索引,而索引本身也是分区的,那就必须把分区列也加入到索引列表 ...

  9. 阿里Druid数据库连接池使用

    阿里巴巴推出的国产数据库连接池,据网上测试对比,比目前的DBCP或C3P0数据库连接池性能更好 可以监控连接以及执行的SQL的情况. 加入项目的具体步骤: 1.导入jar <parent> ...

  10. Android 如何处理崩溃的异常

    Android中处理崩溃异常    大家都知道,现在安装Android系统的手机版本和设备千差万别,在模拟器上运行良好的程序安装到某款手机上说不定就出现崩溃的现象,开发者个人不可能购买所有设备逐个调试 ...