Glide-源码详解
前言:
之前的文章中,笔者介绍了很多Glide的使用方法,但是由于Glide框架封装得太好了,很多人在使用的时候,只是知其然不知其所以然,为了不要仅仅成为”cv工程师”,只会复制粘贴,所以这篇文章我们就一起来研究一下Glide的源码,看看Glide到底是怎么将一张图片加载出来的~
Glide 系列目录
- 1.Glide-入门教程
- 2.Glide-占位图以及加载动画
- 3.Glide-加载本地图片
- 4.Glide-加载Gif
- 5.Glide-绑定生命周期
- 6.Glide-内存缓存与磁盘缓存
- 7.Glide-通过Modules定制Glide
- 8.Glide-自定义缓存
- 9.Glide-图片的压缩
- 10.Glide-图片预处理(圆角,高斯模糊等)
- 11.Glide-图片的剪裁(ScaleType)
- 12.Glide-源码详解
前方高能预警,本文篇幅较长,阅读需要耐心
本文基于Glide 3.7.0版本
一.Glide的构造
//Glide.javaGlide(Engine engine, MemoryCache memoryCache, BitmapPool bitmapPool, Context context, DecodeFormat decodeFormat) {...}
Glide是通过GlideBuilder中的createGlide方法生成的(核心代码如下)
//GlideBuilder.javaGlide createGlide() {...return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);}
Glide的构造参数主要有四个,都是通过createGlide生成的.
MemoryCache 内存缓存
BitmapPool 图片池
DecodeFormat 图片格式
Engine 引擎类
1.MemoryCache :内存缓存 LruResourceCache
//MemorySizeCalculator.javafinal int maxSize = getMaxSize(activityManager);private static int getMaxSize(ActivityManager activityManager) {//每个进程可用的最大内存final int memoryClassBytes = activityManager.getMemoryClass() * 1024 * 1024;//判断是否低配手机final boolean isLowMemoryDevice = isLowMemoryDevice(activityManager);return Math.round(memoryClassBytes* (isLowMemoryDevice ? LOW_MEMORY_MAX_SIZE_MULTIPLIER : MAX_SIZE_MULTIPLIER));
}
最大内存:如果是低配手机,就每个进程可用的最大内存乘以0.33,否则就每个进程可用的最大内存乘以0.4
//MemorySizeCalculator.javaint screenSize = screenDimensions.getWidthPixels() * screenDimensions.getHeightPixels()* BYTES_PER_ARGB_8888_PIXEL;(宽*高*4)
int targetPoolSize = screenSize * BITMAP_POOL_TARGET_SCREENS;(宽*高*4*4)
int targetMemoryCacheSize = screenSize * MEMORY_CACHE_TARGET_SCREENS;(宽*高*4*2)//判断是否超过最大值,否则就等比缩小
if (targetMemoryCacheSize + targetPoolSize <= maxSize) {memoryCacheSize = targetMemoryCacheSize;bitmapPoolSize = targetPoolSize;
} else {int part = Math.round((float) maxSize / (BITMAP_POOL_TARGET_SCREENS + MEMORY_CACHE_TARGET_SCREENS));memoryCacheSize = part * MEMORY_CACHE_TARGET_SCREENS;bitmapPoolSize = part * BITMAP_POOL_TARGET_SCREENS;
}
targetPoolSize 和 targetMemoryCacheSize 之和不能超过maxSize 否则就等比缩小
//GlideBuilder.javamemoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
内存缓存用的是targetMemoryCacheSize (即一般是缓存大小是屏幕的宽 * 高 * 4 * 2)
2.BitmapPool 图片池 LruBitmapPool
int size = calculator.getBitmapPoolSize();
bitmapPool = new LruBitmapPool(size);
图片池用的是targetPoolSize(即一般是缓存大小是屏幕的宽*高*4*4)
3.DecodeFormat 图片格式
DecodeFormat DEFAULT = PREFER_RGB_565
默认是RGB_565
4.Engine 引擎类
//GlideBuilder.javaengine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
engine 里面主要参数
- 内存缓存 memoryCache
- 本地缓存 diskCacheFactory
- 处理源资源的线程池 sourceService
- 处理本地缓存的线程池 diskCacheService
(1)memoryCache:内存缓存 LruBitmapPool
上面已经做了介绍
(2)diskCacheFactory:本地缓存 DiskLruCacheFactory
//DiskCache.java/** 250 MB of cache. */int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
默认大小:250 MB
默认目录:image_manager_disk_cache
(3)sourceService 处理源资源的线程池 (ThreadPoolExecutor的子类)
final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());//获得可用的处理器个数sourceService = new FifoPriorityThreadPoolExecutor(cores);
线程池的核心线程数量等于获得可用的处理器个数
(4)diskCacheService 处理本地缓存的线程池 (ThreadPoolExecutor的子类)
diskCacheService = new FifoPriorityThreadPoolExecutor(1);
线程池的核心线程数量为1
二.with方法
with方法有很多重载,最后会返回一个RequestManager
//Glide.java/*** @see #with(android.app.Activity)* @see #with(android.app.Fragment)* @see #with(android.support.v4.app.Fragment)* @see #with(android.support.v4.app.FragmentActivity)** @param context Any context, will not be retained.* @return A RequestManager for the top level application that can be used to start a load.*/
public static RequestManager with(Context context) {RequestManagerRetriever retriever = RequestManagerRetriever.get();return retriever.get(context);
}
就算你传入的是Context ,这里也会根据你Context 实际的类型,走不同的分支
//RequestManagerRetriever.javapublic RequestManager get(Context context) {if (context == null) {throw new IllegalArgumentException("You cannot start a load on a null Context");} else if (Util.isOnMainThread() && !(context instanceof Application)) {if (context instanceof FragmentActivity) {return get((FragmentActivity) context);} else if (context instanceof Activity) {return get((Activity) context);} else if (context instanceof ContextWrapper) {return get(((ContextWrapper) context).getBaseContext());}}return getApplicationManager(context);
}
这里以FragmentActivity为例,最后会创建一个无界面的Fragment,即SupportRequestManagerFragment ,让请求和你的activity的生命周期同步
//RequestManagerRetriever.javapublic RequestManager get(FragmentActivity activity) {if (Util.isOnBackgroundThread()) {return get(activity.getApplicationContext());} else {assertNotDestroyed(activity);FragmentManager fm = activity.getSupportFragmentManager();return supportFragmentGet(activity, fm);}
}
RequestManager supportFragmentGet(Context context, FragmentManager fm) {SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm);RequestManager requestManager = current.getRequestManager();if (requestManager == null) {requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());current.setRequestManager(requestManager);}return requestManager;
}
这里需要注意一下,如果你是在子线程调用with方法,或者传入的Context是Application的话,请求是跟你的Application的生命周期同步
//RequestManagerRetriever.javaprivate RequestManager getApplicationManager(Context context) {// Either an application context or we're on a background thread.if (applicationManager == null) {synchronized (this) {if (applicationManager == null) {// Normally pause/resume is taken care of by the fragment we add to the fragment or activity.// However, in this case since the manager attached to the application will not receive lifecycle// events, we must force the manager to start resumed using ApplicationLifecycle.applicationManager = new RequestManager(context.getApplicationContext(),new ApplicationLifecycle(), new EmptyRequestManagerTreeNode());}}}return applicationManager;
}
三.load方法
这里方法也有很多重载
//RequestManager.javapublic DrawableTypeRequest<String> load(String string) {return (DrawableTypeRequest<String>) fromString().load(string);
}
但是最后都会返回一个DrawableTypeRequest (继承了DrawableRequestBuilder)
DrawableRequestBuilder就是支持链式调用的一个类,我们平时有类似的需求的时候也可以模仿这样的处理方式,把一些非必须参数用链式调用的方式来设置
四.into方法
//GenericRequestBuilder.java public Target<TranscodeType> into(ImageView view) {Util.assertMainThread();if (view == null) {throw new IllegalArgumentException("You must pass in a non null View");}if (!isTransformationSet && view.getScaleType() != null) {switch (view.getScaleType()) {case CENTER_CROP:applyCenterCrop();break;case FIT_CENTER:case FIT_START:case FIT_END:applyFitCenter();break;//$CASES-OMITTED$default:// Do nothing.}}return into(glide.buildImageViewTarget(view, transcodeClass));}
这里有三点需要注意的:
1.Util.assertMainThread();这里会检查是否主线程,不是的话会抛出异常,所以into方法必须在主线程中调用.
2.当你没有调用transform方法,并且你的ImageView设置了ScaleType,那么他会根据你的设置,对图片做处理(具体处理可以查看DrawableRequestBuilder的applyCenterCrop或者applyFitCenter方法,我们自己自定义BitmapTransformation也可以参考这里的处理).
3.view在这里被封装成一个Target.
//GenericRequestBuilder.java public <Y extends Target<TranscodeType>> Y into(Y target) {Util.assertMainThread();if (target == null) {throw new IllegalArgumentException("You must pass in a non null Target");}if (!isModelSet) {throw new IllegalArgumentException("You must first set a model (try #load())");}Request previous = target.getRequest();if (previous != null) {previous.clear();requestTracker.removeRequest(previous);previous.recycle();}Request request = buildRequest(target);target.setRequest(request);lifecycle.addListener(target);requestTracker.runRequest(request);return target; }
这里可以看到控件封装成的Target能够获取自身绑定的请求,当发现之前的请求还在的时候,会把旧的请求清除掉,绑定新的请求,这也就是为什么控件复用时不会出现图片错位的问题(这点跟我在Picasso源码中看到的处理方式很相像).
接着在into里面会调用buildRequest方法来创建请求
//GenericRequestBuilder.java private Request buildRequest(Target<TranscodeType> target) {if (priority == null) {priority = Priority.NORMAL;}return buildRequestRecursive(target, null);}
//GenericRequestBuilder.javaprivate Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {if (thumbnailRequestBuilder != null) {...Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);...Request thumbRequest = thumbnailRequestBuilder.buildRequestRecursive(target, coordinator);...coordinator.setRequests(fullRequest, thumbRequest);return coordinator;} else if (thumbSizeMultiplier != null) { ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);Request thumbnailRequest = obtainRequest(target, thumbSizeMultiplier, getThumbnailPriority(), coordinator);coordinator.setRequests(fullRequest, thumbnailRequest);return coordinator;} else {// Base case: no thumbnail.return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);}}
1.这里就是请求的生成,buildRequestRecursive里面if有三个分支,这里是根据你设置thumbnail的情况来判断的,第一个是设置缩略图为新的请求的情况,第二个是设置缩略图为float的情况,第三个就是没有设置缩略图的情况.
前两个设置了缩略图的是有两个请求的,fullRequest和thumbnailRequest,没有设置缩略图则肯定只有一个请求了.
2.请求都是通过obtainRequest方法生成的(这个简单了解一下就行)
//GenericRequestBuilder.javaprivate Request obtainRequest(Target<TranscodeType> target, float sizeMultiplier, Priority priority,RequestCoordinator requestCoordinator) {return GenericRequest.obtain(...);}
REQUEST_POOL是一个队列,当队列中有,那么就从队列中取,没有的话就新建一个GenericRequest
//GenericRequest.javapublic static <A, T, Z, R> GenericRequest<A, T, Z, R> obtain(...) {GenericRequest<A, T, Z, R> request = (GenericRequest<A, T, Z, R>) REQUEST_POOL.poll();if (request == null) {request = new GenericRequest<A, T, Z, R>();}request.init(...);return request;}
回到into方法:当创建了请求后runRequest会调用Request的begin方法,即调用GenericRequest的begin方法
//GenericRequestBuilder.javapublic <Y extends Target<TranscodeType>> Y into(Y target) {Request request = buildRequest(target);...requestTracker.runRequest(request);...}
//GenericRequest.javapublic void begin() {...if (Util.isValidDimensions(overrideWidth, overrideHeight)) {onSizeReady(overrideWidth, overrideHeight);} else {target.getSize(this);}...}
最终会调用Engine的load方法
//GenericRequest.javapublic void onSizeReady(int width, int height) {...loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,priority, isMemoryCacheable, diskCacheStrategy, this);...}
我们先看load方法的前面一段:
1.首先会尝试从cache里面取,这里cache就是Glide的构造函数里面的MemoryCache(是一个LruResourceCache),如果取到了,就从cache里面删掉,然后加入activeResources中
2.如果cache里面没取到,就会从activeResources中取,activeResources是一个以弱引用为值的map,他是用于存储使用中的资源.之所以在内存缓存的基础上又多了这层缓存,是为了当内存不足而清除cache中的资源中,不会影响使用中的资源.
//Engine.java public <T, Z, R> LoadStatus load(...) {...EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);if (cached != null) {cb.onResourceReady(cached);if (Log.isLoggable(TAG, Log.VERBOSE)) {logWithTimeAndKey("Loaded resource from cache", startTime, key);}return null;}EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);if (active != null) {cb.onResourceReady(active);if (Log.isLoggable(TAG, Log.VERBOSE)) {logWithTimeAndKey("Loaded resource from active resources", startTime, key);}return null;}...}
load方法接着会通过EngineJobFactory创建一个EngineJob,里面主要管理里两个线程池,diskCacheService和sourceService,他们就是Glide构造函数中Engine里面创建的那两个线程池.
//Engine.javapublic <T, Z, R> LoadStatus load(...) {...EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);...}
//Engine.java
static class EngineJobFactory {private final ExecutorService diskCacheService;private final ExecutorService sourceService;private final EngineJobListener listener;public EngineJobFactory(ExecutorService diskCacheService, ExecutorService sourceService,EngineJobListener listener) {this.diskCacheService = diskCacheService;this.sourceService = sourceService;this.listener = listener;}public EngineJob build(Key key, boolean isMemoryCacheable) {return new EngineJob(key, diskCacheService, sourceService, isMemoryCacheable, listener);}
}
接着说load方法,前面创建了EngineJob,接着调用EngineJob的start方法,并将EngineRunnable放到diskCacheService(处理磁盘缓存的线程池里面运行),接着线程池就会调用EngineRunnable的run方法.
//Engine.javapublic <T, Z, R> LoadStatus load(...) {...EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);jobs.put(key, engineJob);engineJob.addCallback(cb);engineJob.start(runnable);...}
//EngineJob.javapublic void start(EngineRunnable engineRunnable) {this.engineRunnable = engineRunnable;future = diskCacheService.submit(engineRunnable);
}
//EngineRunnable.javapublic void run() {...try {resource = decode();} catch (Exception e) {if (Log.isLoggable(TAG, Log.VERBOSE)) {Log.v(TAG, "Exception decoding", e);}exception = e;}...if (resource == null) {onLoadFailed(exception);} else {onLoadComplete(resource);}}
run里面调用的是decode()方法,里面会尝试先从磁盘缓存中读取,如果不行就从源资源中读取
//EngineRunnable.java private Resource<?> decode() throws Exception {if (isDecodingFromCache()) {//第一次会走这return decodeFromCache();//从磁盘缓存中读取} else {return decodeFromSource();//从源资源中读取}
}
我们先来看从磁盘中读取的策略
//EngineRunnable.javaprivate Resource<?> decodeFromCache() throws Exception {Resource<?> result = null;try {result = decodeJob.decodeResultFromCache();} catch (Exception e) {if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "Exception decoding result from cache: " + e);}}if (result == null) {result = decodeJob.decodeSourceFromCache();}return result;
}
我们可以看到这里先尝试读取处理后的图片(Result),然后再尝试读取原图,但是这里面具体逻辑会根据你设置的磁盘缓存策略来决定是否真的会读取处理图和原图
那么我们再回到EngineRunnable的run()方法中
public void run() {...try {resource = decode();} catch (Exception e) {if (Log.isLoggable(TAG, Log.VERBOSE)) {Log.v(TAG, "Exception decoding", e);}exception = e;}...if (resource == null) {onLoadFailed(exception);} else {onLoadComplete(resource);}}
第一次走decode的时候会先尝试从磁盘中获取,如果获取的为null,那么在onLoadFailed方法里面又会把这个run再次放入线程池中,但是这次是放入sourceService(处理源资源的线程池)
//EngineRunnable.javaprivate void onLoadFailed(Exception e) {if (isDecodingFromCache()) {stage = Stage.SOURCE;manager.submitForSource(this);} else {manager.onException(e);}
}
//EngineJob.java@Override
public void submitForSource(EngineRunnable runnable) {future = sourceService.submit(runnable);
}
接着sourceService里面又会调用调用EngineRunnable的run方法,这次decode里面会走从源资源读取的那条分支
//EngineRunnable.java private Resource<?> decode() throws Exception {if (isDecodingFromCache()) {//第一次会走这return decodeFromCache();//从磁盘缓存中读取} else {//第二次会走这return decodeFromSource();//从源资源读取}
}//DecodeJob.javapublic Resource<Z> decodeFromSource() throws Exception {Resource<T> decoded = decodeSource();//获取数据,并解码return transformEncodeAndTranscode(decoded);//处理图片
}
里面主要做了两件事,一个是获取图片,一个是处理图片
1.我们先来看获取图片的decodeSource方法
//DecodeJob.javaprivate Resource<T> decodeSource() throws Exception {...//拉取数据final A data = fetcher.loadData(priority);...//解码,并保存源资源到磁盘decoded = decodeFromSourceData(data);...return decoded;
}
//DecodeJob.java
private Resource<T> decodeFromSourceData(A data) throws IOException {final Resource<T> decoded;if (diskCacheStrategy.cacheSource()) {//解码并保存源资源(图片)到磁盘缓存中decoded = cacheAndDecodeSourceData(data);} else {long startTime = LogTime.getLogTime();decoded = loadProvider.getSourceDecoder().decode(data, width, height);if (Log.isLoggable(TAG, Log.VERBOSE)) {logWithTimeAndKey("Decoded from source", startTime);}}return decoded;
}
这里调用了DataFetcher的loadData方法来获取数据,DataFetcher有很多实现类,一般来说我们都是从网络中读取数据,我们这边就以HttpUrlFetcher为例
//HttpUrlFetcher.java@Override
public InputStream loadData(Priority priority) throws Exception {return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
}private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)throws IOException {if (redirects >= MAXIMUM_REDIRECTS) {throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");} else {// Comparing the URLs using .equals performs additional network I/O and is generally broken.// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.try {if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {throw new IOException("In re-direct loop");}} catch (URISyntaxException e) {// Do nothing, this is best effort.}}urlConnection = connectionFactory.build(url);for (Map.Entry<String, String> headerEntry : headers.entrySet()) {urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());}urlConnection.setConnectTimeout(2500);urlConnection.setReadTimeout(2500);urlConnection.setUseCaches(false);urlConnection.setDoInput(true);// Connect explicitly to avoid errors in decoders if connection fails.urlConnection.connect();if (isCancelled) {return null;}final int statusCode = urlConnection.getResponseCode();if (statusCode / 100 == 2) {//请求成功return getStreamForSuccessfulRequest(urlConnection);} else if (statusCode / 100 == 3) {String redirectUrlString = urlConnection.getHeaderField("Location");if (TextUtils.isEmpty(redirectUrlString)) {throw new IOException("Received empty or null redirect url");}URL redirectUrl = new URL(url, redirectUrlString);return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);} else {if (statusCode == -1) {throw new IOException("Unable to retrieve response code from HttpUrlConnection.");}throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());}
}
2.看完了获取图片的方法,我们再来看看处理图片的方法transformEncodeAndTranscode
//DecodeJob.javaprivate Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {...//对图片做剪裁等处理Resource<T> transformed = transform(decoded);...//将处理后的图片写入磁盘缓存(会根据配置来决定是否写入)writeTransformedToCache(transformed);...//转码,转为需要的类型Resource<Z> result = transcode(transformed);...return result;
}
Glide的整个加载流程就基本上走完了,整个篇幅还是比较长的,第一次看得时候可能会有点懵逼,需要结合着源码多走几遍才能够更加熟悉.
阅读源码并不是我们最终的目的,我们阅读源码主要有两个目的
一个是能够更加熟悉这个框架,那么使用的时候就更加得心应手了,比如我从源码中发现了很多文章都说错了默认的缓存策略
一个是学习里面的设计模式和思想,应用到我们自己的项目中,比如说面对接口编程,对于不同的数据,用DataFetcher的不同实现类来拉取数据
热门文章
- 活用productFlavors
- onTouch事件传递
- 那些年我们解决滑动冲突时遇过的坑
- 进程间通信–AIDL
- 序列化–Serializable与Parcelable
- 如何解决内存溢出以及内存泄漏
- Okhttputils终极封装
- FaceBook推出的调试神器
- Android代码优化工具
- Glide-入门教程
- Glide-图片预处理(圆角,高斯模糊等)
- Glide-图片的压缩
- Glide-内存缓存与磁盘缓存
- Glide-自定义缓存
Glide-源码详解相关推荐
- 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...
- 【Live555】live555源码详解系列笔记
[Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...
- 【Live555】live555源码详解(八):testRTSPClient
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的testRTSPClient实现的三个类所在的位置: ourRTSPClient.StreamClient ...
- 【Live555】live555源码详解(七):GenericMediaServer、RTSPServer、RTSPClient
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: GenericMediaServer.RTSPServer.RTSPClient 14 ...
- 【Live555】live555源码详解(六):FramedSource、RTPSource、RTPSink
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: FramedSource.RTPSource.RTPSink 11.FramedSou ...
- 【Live555】live555源码详解(五):MediaSource、MediaSink、MediaSession、MediaSubsession
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的四个类所在的位置: MediaSource.MediaSink.MediaSession.MediaSub ...
- 【Live555】live555源码详解(四):Medium媒体基础类
[Live555]live555源码详解系列笔记 7.Media Medai所依赖关系图 依赖Medai关系图 Media和UsageEnvironment关联图
- 【Live555】live555源码详解(二):BasicHashTable、DelayQueue、HandlerSet
[Live555]live555源码详解系列笔记 3.BasicHashTable 哈希表 协作图: 3.1 BasicHashTable BasicHashTable 继承自 HashTable 重 ...
- 【Live555】live555源码详解(一):BasicUsageEnvironment、UsageEnvironment
[Live555]live555源码详解系列笔记 类关系图 1.UsageEnvironment 详解 1.1 BasicUsageEnvironment BasicUsageEnvironment ...
- udhcp源码详解(五) 之DHCP包--options字段
中间有很长一段时间没有更新udhcp源码详解的博客,主要是源码里的函数太多,不知道要不要一个一个讲下去,要知道讲DHCP的实现理论的话一篇博文也就可以大致的讲完,但实现的源码却要关心很多的问题,比如说 ...
最新文章
- MySQL查询的进阶操作--联合查询
- 超时锁定计算机,就会发现多了一个控制台锁定显示关闭超时选项
- 二级MS Office公共基础知识错题本(1)
- 客座编辑:杜小勇(1963-),男,博士,中国人民大学信息学院教授、博士生导师。...
- Spark GraphX算法 - Pregel算法
- 商人的盈利方式非你所想
- Android移植之自定义ProgressBar
- SQL 数据库 函数
- Unity+Android GET和POST方式的简单实现API请求(人像动漫化)
- 一阶微分方程的物理意义_如何从物理意义上理解NS方程?
- Java开发实用的面试题及参考答案
- 九爷带你了解 mctop: 监视 Memcache 流量
- iPhone/iPad用iTunes“同步”不等于“备份”
- tcp state linux,Linux Kernel ‘tcp_rcv_state_process()’函数拒绝服务漏洞
- [Design]国粹京剧 脸谱表情 值得收藏
- element UI 模态层dialog自定义大小
- Android SIM卡联系人操作总结
- MBA-day24 最值问题
- 懒人之家-QQ客服右侧
- 百度视频发布年度大数据报告 揭晓热播影视综艺动漫
热门文章
- 数据结构与算法(一):什么是数据结构?
- 微信 SHA1 签名_微信公众号自动回复功能开发
- docker 使用tar安装mysql_Docker安装MySQL
- 如何解决收到网监大队信息系统安全等级保护限期整改通知书
- os.path.dirname用法
- python 工程进度计划_从零开始的项目实战(7)——项目进度述职报告
- SSE(服务器推送事件)的介绍、问题及解决
- 微信公众号添加Word文档附件教程_公众号添加Excel、PDF、PPT、Zip等附件教程
- 实验一-Hadoop的安装与使用
- opensuse安装face_recognition全记录