当前位置:网站首页>Volley source code analysis
Volley source code analysis
2022-07-03 20:50:00 【xiaopangcame】
summary
This article is based on Volley 1.1.1 Version of the source .
Volley yes Google An official set of small and ingenious asynchronous request library , The framework is very extensible , Support HttpClient、HttpUrlConnection, Even support OkHttp.Volley It is not suitable for downloading a large amount of content or streaming operation , Because in the process of parsing ,Volley All responses will be stored in memory , therefore Volley Not suitable for uploading and downloading large files .
Add dependency :
1 | implementation 'com.android.volley:volley:1.1.1' |
Use :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | RequestQueue queue = Volley.newRequestQueue(context); String url ="http://www.google.com"; StringRequest stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() { @Override public void onResponse(String response) { // in UI Thread } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // in UI Thread } }); stringRequest.setTag(TAG); // Add the request to the RequestQueue. queue.add(stringRequest); // onStop queue.cancelAll(TAG); |
Create request queue
Volley Class has multiple overloaded newRequestQueue Static methods :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public class Volley { private static final String DEFAULT_CACHE_DIR = "volley"; public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, (BaseHttpStack) null); } public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) { BasicNetwork network; if (stack == null) { if (Build.VERSION.SDK_INT >= 9) { network = new BasicNetwork(new HurlStack()); } else { // ... } } else { network = new BasicNetwork(stack); } return newRequestQueue(context, network); } private static RequestQueue newRequestQueue(Context context, Network network) { final Context appContext = context.getApplicationContext(); DiskBasedCache.FileSupplier cacheSupplier = new DiskBasedCache.FileSupplier() { private File cacheDir = null; @Override public File get() { if (cacheDir == null) { cacheDir = new File(appContext.getCacheDir(), DEFAULT_CACHE_DIR); } return cacheDir; } }; RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheSupplier), network); queue.start(); return queue; } } |
RequestQueue Class construction method is as follows :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class RequestQueue { private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; private final Cache mCache; private final Network mNetwork; private final ResponseDelivery mDelivery; private final NetworkDispatcher[] mDispatchers; private CacheDispatcher mCacheDispatcher; public RequestQueue( Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) { mCache = cache; mNetwork = network; mDispatchers = new NetworkDispatcher[threadPoolSize]; mDelivery = delivery; } public RequestQueue(Cache cache, Network network, int threadPoolSize) { this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper()))); } public RequestQueue(Cache cache, Network network) { this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); } } |
RequestQueue#start()
The method is as follows , It can be seen that a Cache Dispatcher And a specified number Network Dispatcher:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** Starts the dispatchers in this queue. */ public void start() { // Make sure any currently running dispatchers are stopped. stop(); // Create the cache dispatcher and start it. mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); mCacheDispatcher.start(); // Create network dispatchers (and corresponding threads) up to the pool size. for (int i = 0; i < mDispatchers.length; i++) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); mDispatchers[i] = networkDispatcher; networkDispatcher.start(); } } /** Stops the cache and network dispatchers. */ public void stop() { if (mCacheDispatcher != null) { mCacheDispatcher.quit(); } for (final NetworkDispatcher mDispatcher : mDispatchers) { if (mDispatcher != null) { mDispatcher.quit(); } } } |
RequestQueue event
stay RequestQueue Class defines some events :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public @interface RequestEvent { /** The request was added to the queue. */ public static final int REQUEST_QUEUED = 0; /** Cache lookup started for the request. */ public static final int REQUEST_CACHE_LOOKUP_STARTED = 1; /** Cache lookup finished for the request and cached response is delivered or request is queued for network dispatching. */ public static final int REQUEST_CACHE_LOOKUP_FINISHED = 2; /** Network dispatch started for the request. */ public static final int REQUEST_NETWORK_DISPATCH_STARTED = 3; /** The network dispatch finished for the request and response (if any) is delivered. */ public static final int REQUEST_NETWORK_DISPATCH_FINISHED = 4; /** All the work associated with the request is finished and request is removed from all the queues. */ public static final int REQUEST_FINISHED = 5; } public interface RequestEventListener { void onRequestEvent(Request<?> request, @RequestEvent int event); } private final List<RequestEventListener> mEventListeners = new ArrayList<>(); |
These events can be manipulated in the following ways :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void sendRequestEvent(Request<?> request, @RequestEvent int event) { synchronized (mEventListeners) { for (RequestEventListener listener : mEventListeners) { listener.onRequestEvent(request, event); } } } public void addRequestEventListener(RequestEventListener listener) { synchronized (mEventListeners) { mEventListeners.add(listener); } } public void removeRequestEventListener(RequestEventListener listener) { synchronized (mEventListeners) { mEventListeners.remove(listener); } } |
Send a request
RequestQueue#add()
The method is as follows :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private final AtomicInteger mSequenceGenerator = new AtomicInteger(); private final Set<Request<?>> mCurrentRequests = new HashSet<>(); private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<>(); private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<>(); public <T> Request<T> add(Request<T> request) { // Tag the request as belonging to this queue and add it to the set of current requests. request.setRequestQueue(this); synchronized (mCurrentRequests) { mCurrentRequests.add(request); } // Process requests in the order they are added. request.setSequence(getSequenceNumber()); request.addMarker("add-to-queue"); sendRequestEvent(request, RequestEvent.REQUEST_QUEUED); // If the request does not need caching , Directly give it to the network request queue to execute , The default is to cache if (!request.shouldCache()) { mNetworkQueue.add(request); return request; } mCacheQueue.add(request); return request; } |
In this method, the request is handed over to the cache queue or network queue for processing .
CacheDispatcher
CacheDispatcher Inherited from Thread, stay RequestQueue#start()
Method has been started , So look directly run Method :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | public class CacheDispatcher extends Thread { private final BlockingQueue<Request<?>> mCacheQueue; private final BlockingQueue<Request<?>> mNetworkQueue; private final Cache mCache; private final ResponseDelivery mDelivery; private volatile boolean mQuit = false; /** Manage list of waiting requests and de-duplicate requests with same cache key. */ private final WaitingRequestManager mWaitingRequestManager; public CacheDispatcher( BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue, Cache cache, ResponseDelivery delivery) { mCacheQueue = cacheQueue; mNetworkQueue = networkQueue; mCache = cache; mDelivery = delivery; mWaitingRequestManager = new WaitingRequestManager(this); } public void quit() { mQuit = true; interrupt(); } @Override public void run() { if (DEBUG) VolleyLog.v("start new dispatcher"); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // Make a blocking call to initialize the cache. mCache.initialize(); while (true) { try { processRequest(); } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { Thread.currentThread().interrupt(); return; } VolleyLog.e("Ignoring spurious interrupt of CacheDispatcher thread; " + "use quit() to terminate it"); } } } private void processRequest() throws InterruptedException { // Blocking queues final Request<?> request = mCacheQueue.take(); processRequest(request); } void processRequest(final Request<?> request) throws InterruptedException { request.addMarker("cache-queue-take"); request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED); try { // If the request has been canceled, don't bother dispatching it. if (request.isCanceled()) { request.finish("cache-discard-canceled"); return; } // Attempt to retrieve this item from cache. Cache.Entry entry = mCache.get(request.getCacheKey()); if (entry == null) { request.addMarker("cache-miss"); // Cache miss; send off to the network dispatcher. if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { mNetworkQueue.put(request); } return; } // If it is completely expired, just send it to the network. if (entry.isExpired()) { request.addMarker("cache-hit-expired"); request.setCacheEntry(entry); if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { mNetworkQueue.put(request); } return; } // We have a cache hit; parse its data for delivery back to the request. request.addMarker("cache-hit"); Response<?> response = request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders)); request.addMarker("cache-hit-parsed"); if (!response.isSuccess()) { request.addMarker("cache-parsing-failed"); mCache.invalidate(request.getCacheKey(), true); request.setCacheEntry(null); if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { mNetworkQueue.put(request); } return; } if (!entry.refreshNeeded()) { // Cache does not need to be refreshed , Give it directly to ResponseDelivery To deal with mDelivery.postResponse(request, response); } else { // Soft expired cache hits , You can distribute cached response, But we also need to send the request to the network to refresh . request.addMarker("cache-hit-refresh-needed"); request.setCacheEntry(entry); // Mark the response as intermediate. response.intermediate = true; if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { // take intermediate The response is sent back to the user , Then forward the request to the network . mDelivery.postResponse( request, response, new Runnable() { @Override public void run() { try { // If the cache needs to be refreshed , This place needs to re request the network mNetworkQueue.put(request); } catch (InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); } } }); } else { // The request has been added to the list of waiting requests , To receive the network response from the first request when the first request returns . mDelivery.postResponse(request, response); } } } finally { request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED); } } } |
If you have a cache , Take it out and deal with it , No or expired , Join the network request queue to request , There are several places that need network requests :
- Requests do not require caching
- Request not found in cache
- The requested cache has expired
- The requested cache needs to be refreshed
NetworkDispatcher
NetworkDispatcher
NetworkDispatcher Also inherit from Thread, They are RequestQueue#start()
Method has been started , So look directly run Method :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | public class NetworkDispatcher extends Thread { private final BlockingQueue<Request<?>> mQueue; private final Network mNetwork; private final Cache mCache; private final ResponseDelivery mDelivery; private volatile boolean mQuit = false; public NetworkDispatcher( BlockingQueue<Request<?>> queue, Network network, Cache cache, ResponseDelivery delivery) { mQueue = queue; mNetwork = network; mCache = cache; mDelivery = delivery; } public void quit() { mQuit = true; interrupt(); } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); while (true) { try { processRequest(); } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { Thread.currentThread().interrupt(); return; } VolleyLog.e("Ignoring spurious interrupt of NetworkDispatcher thread; " + "use quit() to terminate it"); } } } private void processRequest() throws InterruptedException { Request<?> request = mQueue.take(); processRequest(request); } void processRequest(Request<?> request) { long startTimeMs = SystemClock.elapsedRealtime(); request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED); try { request.addMarker("network-queue-take"); if (request.isCanceled()) { request.finish("network-discard-cancelled"); request.notifyListenerResponseNotUsable(); return; } addTrafficStatsTag(request); // Perform network requests NetworkResponse networkResponse = mNetwork.performRequest(request); request.addMarker("network-http-complete"); // If the server returned 304 AND we delivered a response already, // we're done -- don't deliver a second identical response. if (networkResponse.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); request.notifyListenerResponseNotUsable(); return; } // Parse the response here on the worker thread. Response<?> response = request.parseNetworkResponse(networkResponse); request.addMarker("network-parse-complete"); // Write to cache if applicable. // TODO: Only update cache metadata instead of entire record for 304s. if (request.shouldCache() && response.cacheEntry != null) { mCache.put(request.getCacheKey(), response.cacheEntry); request.addMarker("network-cache-written"); } // Post the response back. request.markDelivered(); mDelivery.postResponse(request, response); request.notifyListenerResponseReceived(response); } catch (VolleyError volleyError) { volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); parseAndDeliverNetworkError(request, volleyError); request.notifyListenerResponseNotUsable(); } catch (Exception e) { VolleyLog.e(e, "Unhandled exception %s", e.toString()); VolleyError volleyError = new VolleyError(e); volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); mDelivery.postError(request, volleyError); request.notifyListenerResponseNotUsable(); } finally { request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED); } } } |
You can see , stay NetworkDispatcher in , The most important thing is to call mNetwork.performRequest(request)
Perform network requests , And in the Network It's an interface , The concrete implementation class is BasicNetwork.
BasicNetwork
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { // ... try { // ... httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); // return NetworkResponse } catch (SocketTimeoutException e) { attemptRetryOnException("socket", request, new TimeoutError()); } catch (MalformedURLException e) { throw new RuntimeException("Bad URL " + request.getUrl(), e); } catch (IOException e) { int statusCode; if (httpResponse != null) { statusCode = httpResponse.getStatusCode(); } else { throw new NoConnectionError(e); } VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); NetworkResponse networkResponse; if (responseContents != null) { networkResponse = new NetworkResponse( statusCode, responseContents, /* notModified= */ false, SystemClock.elapsedRealtime() - requestStart, responseHeaders); if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED || statusCode == HttpURLConnection.HTTP_FORBIDDEN) { attemptRetryOnException( "auth", request, new AuthFailureError(networkResponse)); } else if (statusCode >= 400 && statusCode <= 499) { // Don't retry other client errors. throw new ClientError(networkResponse); } else if (statusCode >= 500 && statusCode <= 599) { if (request.shouldRetryServerErrors()) { attemptRetryOnException( "server", request, new ServerError(networkResponse)); } else { throw new ServerError(networkResponse); } } else { // 3xx? No reason to retry. throw new ServerError(networkResponse); } } else { attemptRetryOnException("network", request, new NetworkError()); } } } } // Request to retry private static void attemptRetryOnException( String logPrefix, Request<?> request, VolleyError exception) throws VolleyError { RetryPolicy retryPolicy = request.getRetryPolicy(); int oldTimeout = request.getTimeoutMs(); try { retryPolicy.retry(exception); } catch (VolleyError e) { request.addMarker(String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); throw e; } request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); } public class DefaultRetryPolicy implements RetryPolicy { @Override public void retry(VolleyError error) throws VolleyError { mCurrentRetryCount++; mCurrentTimeoutMs += (int) (mCurrentTimeoutMs * mBackoffMultiplier); if (!hasAttemptRemaining()) { throw error; } } /** Returns true if this policy has attempts remaining, false otherwise. */ protected boolean hasAttemptRemaining() { return mCurrentRetryCount <= mMaxNumRetries; } } |
stay BasicNetwork#performRequest() Called in httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
, there mBaseHttpStack That's what we created earlier RequestQueue It's new HurlStack and HttpClientStack(@Deprecated, above RequestQueue There is also a new one in the constructor of HttpClientStack Object method , But it has been abandoned , No parsing ). So in HttpStack Of executeRequest() Is the specific network request .
HurlStack
1 2 3 4 5 6 7 | public class HurlStack extends BaseHttpStack { @Override public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError { // This class uses HttpURLConnection To realize the real network request , You can customize } } |
ResponseDelivery
ResponseDelivery It's an interface :
1 2 3 4 5 6 7 8 9 10 11 12 13 | public interface ResponseDelivery { /** Parses a response from the network or cache and delivers it. */ void postResponse(Request<?> request, Response<?> response); /** * Parses a response from the network or cache and delivers it. The provided Runnable will be * executed after delivery. */ void postResponse(Request<?> request, Response<?> response, Runnable runnable); /** Posts an error for the given request. */ void postError(Request<?> request, VolleyError error); } |
The concrete implementation class is ExecutorDelivery, It ensures that the callback called by the client runs in UI Threads :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | public class ExecutorDelivery implements ResponseDelivery { private final Executor mResponsePoster; // From the above analysis, we can see that handle = new Handler(Looper.getMainLooper()) public ExecutorDelivery(final Handler handler) { mResponsePoster = new Executor() { @Override public void execute(Runnable command) { handler.post(command); } }; } public ExecutorDelivery(Executor executor) { mResponsePoster = executor; } @Override public void postResponse(Request<?> request, Response<?> response) { postResponse(request, response, null); } @Override public void postResponse(Request<?> request, Response<?> response, Runnable runnable) { request.markDelivered(); request.addMarker("post-response"); mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); } @Override public void postError(Request<?> request, VolleyError error) { request.addMarker("post-error"); Response<?> response = Response.error(error); mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null)); } private static class ResponseDeliveryRunnable implements Runnable { private final Request mRequest; private final Response mResponse; private final Runnable mRunnable; public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { mRequest = request; mResponse = response; mRunnable = runnable; } @SuppressWarnings("unchecked") @Override public void run() { if (mRequest.isCanceled()) { mRequest.finish("canceled-at-delivery"); return; } // Here we will call create Request Callback passed in when if (mResponse.isSuccess()) { mRequest.deliverResponse(mResponse.result); } else { mRequest.deliverError(mResponse.error); } if (mResponse.intermediate) { mRequest.addMarker("intermediate-response"); } else { mRequest.finish("done"); } // If there is a task to be performed , For example, the previous cache needs to be refreshed , It will be called in this place if (mRunnable != null) { mRunnable.run(); } } } } |
summary
Volley These are the main classes in :
- Volley: Provides build RequestQueue A unified approach , We can also not pass Volley Class, but build it yourself RequestQueue;
- RequestQueue: Responsible for distributing requests to different request queues ;
- CacheDispatcher: Cache request processing ;
- NetworkDispatcher: Handle network requests ;
- ResponseDelivery: Get the request and process it , The specific implementation class is ExecutorDelivery;
- Cache: Cache interface , The specific implementation classes are DiskBasedCache;
- Network: Network interface , The specific implementation classes are BasicNetwork;
- HttpStack: Actually execute the request , The specific implementation classes are HttpClientStack( obsolete ),BaseHttpStack( Subclasses have HurlStack,AdaptedHttpStack etc. );
- Request: Encapsulate the request information and process the callback , The specific implementation classes are StringRequest,JsonRequest,ImageRequest etc. ;
- NetworkResponse: Returned when requesting the network response;
- Response: Some callback interfaces are defined , Used to encapsulate the data returned to the client .
Volley The frame is highly removable , Many of our custom classes can be used to extend its functions :
- Request: By default, the framework provides StringRequest,JsonRequest and ImageRequest etc. , We can inherit Request And rewrite deliverResponse() and parseNetworkResponse() Customize request .
- Cache: By default, the framework provides DiskBasedCache Class to handle caching , We can inherit Cache Interface to customize cache policy .
- HttpStack: The framework uses by default HurlStack Class is through HttpUrlConnection and HttpClient To implement network requests , We can replace it with others, such as OkHttp, Just inherit HttpStack And rewrite performRequest() Just process the request .
To sum up Volley Network request process :
- First create a request queue RequestQueue, You can customize related classes , Here will start a Cache Dispatcher And a specified number Network Dispatcher;
- And then in CacheDispatcher Processing requests in , If cache is available , Then directly return to cache , Otherwise request the network ;
- And then NetworkDispatcher Process network requests in , What really works is HttpStack Implementation class ;
- Finally through ResponseDelivery The implementation class returns the request result .
边栏推荐
- Qtablewidget control of QT
- App compliance
- Cannot load driver class: com. mysql. cj. jdbc. Driver
- The "boss management manual" that is wildly spread all over the network (turn)
- Such as the visual appeal of the live broadcast of NBA Finals, can you still see it like this?
- 不同业务场景该如何选择缓存的读写策略?
- Qt6 QML Book/Qt Quick 3D/基础知识
- From the behind the scenes arena of the ice and snow event, see how digital builders can ensure large-scale events
- Instructions for common methods of regular expressions
- Global and Chinese market of full authority digital engine control (FADEC) 2022-2028: Research Report on technology, participants, trends, market size and share
猜你喜欢
Go learning notes (4) basic types and statements (3)
你真的知道自己多大了吗?
强化学习-学习笔记1 | 基础概念
Do you really know how old you are?
How can the outside world get values when using nodejs to link MySQL
Qtablewidget control of QT
2.4 conversion of different data types
不同业务场景该如何选择缓存的读写策略?
Gee calculated area
Hcie security Day10: six experiments to understand VRRP and reliability
随机推荐
[Yugong series] go teaching course 002 go language environment installation in July 2022
Deep search DFS + wide search BFS + traversal of trees and graphs + topological sequence (template article acwing)
XAI+网络安全?布兰登大学等最新《可解释人工智能在网络安全应用》综述,33页pdf阐述其现状、挑战、开放问题和未来方向
Hcie security Day10: six experiments to understand VRRP and reliability
[postgresql]postgresql custom function returns an instance of table type
电子科技大学|强化学习中有效利用的聚类经验回放
Based on laravel 5.5\5.6\5 X solution to the failure of installing laravel ide helper
MySQL dump - exclude some table data - MySQL dump - exclude some table data
Apprentissage intensif - notes d'apprentissage 1 | concepts de base
Qt6 QML Book/Qt Quick 3D/基础知识
Reinforcement learning - learning notes 1 | basic concepts
Global and Chinese market of rubidium standard 2022-2028: Research Report on technology, participants, trends, market size and share
jvm jni 及 pvm pybind11 大批量数据传输及优化
Use of CMD command
2.2 integer
Global and Chinese market of micro positioning technology 2022-2028: Research Report on technology, participants, trends, market size and share
SQL injection - Fundamentals of SQL database operation
The "boss management manual" that is wildly spread all over the network (turn)
Introduction to golang garbage collection
Fingerprint password lock based on Hal Library