2024年安卓最全Andriod 网络框架 OkHttp 源码解析(4),面试应该掌握哪些技巧

《设计思想解读开源框架》

第一章、 热修复设计

  • 第一节、 AOT/JIT & dexopt 与 dex2oat

  • 第二节、 热修复设计之 CLASS_ISPREVERIFIED 问题

  • 第三节、热修复设计之热修复原理

  • 第四节、Tinker 的集成与使用(自动补丁包生成)

    第二章、 插件化框架设计

  • 第一节、 Class 文件与 Dex 文件的结构解读

  • 第二节、 Android 资源加载机制详解

  • 第三节、 四大组件调用原理

  • 第四节、 so 文件加载机制

  • 第五节、 Android 系统服务实现原理

    第三章、 组件化框架设计

  • 第一节、阿里巴巴开源路由框——ARouter 原理分析

  • 第二节、APT 编译时期自动生成代码&动态类加载

  • 第三节、 Java SPI 机制

  • 第四节、 AOP&IOC

  • 第五节、 手写组件化架构

    第四章、图片加载框架

  • 第一节、图片加载框架选型

  • 第二节、Glide 原理分析

  • 第三节、手写图片加载框架实战

    第五章、网络访问框架设计

  • 第一节、网络通信必备基础

  • 第二节、OkHttp 源码解读

  • 第三节、Retrofit 源码解析

    第六章、 RXJava 响应式编程框架设计

  • 第一节、链式调用

  • 第二节、 扩展的观察者模式

  • 第三节、事件变换设计

  • 第四节、Scheduler 线程控制

    第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期


    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

readyAsyncCalls.add(call);
}
}

当把该请求加入到了正在执行的队列之后,我们会立即使用一个线程池来执行该 AsyncCall。这样这个请求的责任链就会在一个线程池当中被异步地执行了。这里的线程池由 executorService() 方法返回:

public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue(), Util.threadFactory(“OkHttp Dispatcher”, false));
}
return executorService;
}

显然,当线程池不存在的时候会去创建一个线程池。除了上面的这种方式,我们还可以在构建 OkHttpClient 的时候,自定义一个 Dispacher,并在其构造方法中为其指定一个线程池。下面我们类比 OkHttp 的同步请求绘制了一个异步请求的时序图。你可以通过将两个图对比来了解两种实现方式的不同:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以上就是分发器 Dispacher 的逻辑,看上去并没有那么复杂。并且从上面的分析中,我们可以看出实际请求的执行过程并不是在这里完成的,这里只能决定在哪个线程当中执行请求并把请求用双端队列缓存下来,而实际的请求执行过程是在责任链中完成的。下面我们就来分析一下 OkHttp 里的责任链的执行过程。

2.3 责任链的执行过程

在典型的责任链设计模式里,很多对象由每一个对象对其下级的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。责任链在现实生活中的一种场景就是面试,当某轮面试官觉得你没有资格进入下一轮的时候可以否定你,不然会让下一轮的面试官继续面试。

在 OkHttp 里面,责任链的执行模式与之稍有不同。这里我们主要来分析一下在 OkHttp 里面,责任链是如何执行的,至于每个链条里面的具体逻辑,我们会在随后一一说明。

回到 2.1 的代码,有两个地方需要我们注意:

  1. 是当创建一个责任链 RealInterceptorChain 的时候,我们传入的第 5 个参数是 0。该参数名为 index,会被赋值给 RealInterceptorChain 实例内部的同名全局变量。
  2. 当启用责任链的时候,会调用它的 proceed(Request) 方法。

下面是 proceed(Request) 方法的定义:

@Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}

这里又调用了内部的重载的 proceed() 方法。下面我们对该方法进行了简化:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
// …
// 调用责任链的下一个拦截器
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// …
return response;
}

注意到这里使用责任链进行处理的时候,会新建下一个责任链并把 index+1 作为下一个责任链的 index。然后,我们使用 index 从拦截器列表中取出一个拦截器,调用它的 intercept() 方法,并把下一个执行链作为参数传递进去。

这样,当下一个拦截器希望自己的下一级继续处理这个请求的时候,可以调用传入的责任链的 proceed() 方法;如果自己处理完毕之后,下一级不需要继续处理,那么就直接返回一个 Response 实例即可。因为,每次都是在当前的 index 基础上面加 1,所以能在调用 proceed() 的时候准确地从拦截器列表中取出下一个拦截器进行处理。

我们还要注意的地方是之前提到过重试拦截器,这种拦截器会在内部启动一个 while 循环,并在循环体中调用执行链的 proceed() 方法来实现请求的不断重试。这是因为在它那里的拦截器链的 index 是固定的,所以能够每次调用 proceed() 的时候,都能够从自己的下一级执行一遍链条。下面就是这个责任链的执行过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

清楚了 OkHttp 的拦截器链的执行过程之后,我们来看一下各个拦截器做了什么逻辑。

2.3 重试和重定向:RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 主要用来当请求失败的时候进行重试,以及在需要的情况下进行重定向。我们上面说,责任链会在进行处理的时候调用第一个拦截器的 intercept() 方法。如果我们在创建 OkHttp 客户端的时候没有加入自定义拦截器,那么 RetryAndFollowUpInterceptor 就是我们的责任链中最先被调用的拦截器。

@Override public Response intercept(Chain chain) throws IOException {
// …
// 注意这里我们初始化了一个 StreamAllocation 并赋值给全局变量,它的作用我们后面会提到
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
// 用来记录重定向的次数
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException(“Canceled”);
}

Response response;
boolean releaseConnection = true;
try {
// 这里从当前的责任链开始执行一遍责任链,是一种重试的逻辑
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// 调用 recover 方法从失败中进行恢复,如果可以恢复就返回true,否则返回false
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// 重试与服务器进行连接
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// 如果 releaseConnection 为 true 则表明中间出现了异常,需要释放资源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}

// 使用之前的响应 priorResponse 构建一个响应,这种响应的响应体 body 为空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder().body(null).build())
.build();
}

// 根据得到的响应进行处理,可能会增加一些认证信息、重定向或者处理超时请求
// 如果该请求无法继续被处理或者出现的错误不需要继续处理,将会返回 null
Request followUp = followUpRequest(response, streamAllocation.route());

// 无法重定向,直接返回之前的响应
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}

// 关闭资源
closeQuietly(response.body());

// 达到了重定向的最大次数,就抛出一个异常
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}

if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException(“Cannot retry streamed HTTP body”, response.code());
}

// 这里判断新的请求是否能够复用之前的连接,如果无法复用,则创建一个新的连接
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response

  • " didn’t close its backing stream. Bad interceptor?");
    }

request = followUp;
priorResponse = response;
}
}

以上的代码主要用来根据错误的信息做一些处理,会根据服务器返回的信息判断这个请求是否可以重定向,或者是否有必要进行重试。如果值得去重试就会新建或者复用之前的连接在下一次循环中进行请求重试,否则就将得到的请求包装之后返回给用户。这里,我们提到了 StreamAllocation 对象,它相当于一个管理类,维护了服务器连接、并发流和请求之间的关系,该类还会初始化一个 Socket 连接对象,获取输入/输出流对象。同时,还要注意这里我们通过 client.connectionPool() 传入了一个连接池对象 ConnectionPool。这里我们只是初始化了这些类,但实际在当前的方法中并没有真正用到这些类,而是把它们传递到下面的拦截器里来从服务器中获取请求的响应。稍后,我们会说明这些类的用途,以及之间的关系。

2.4 BridgeInterceptor

桥拦截器 BridgeInterceptor 用于从用户的请求中构建网络请求,然后使用该请求访问网络,最后从网络响应当中构建用户响应。相对来说这个拦截器的逻辑比较简单,只是用来对请求进行包装,并将服务器响应转换成用户友好的响应:

public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
// 从用户请求中获取网络请求构建者
Request.Builder requestBuilder = userRequest.newBuilder();
// …
// 执行网络请求
Response networkResponse = chain.proceed(requestBuilder.build());
// …
// 从网络响应中获取用户响应构建者
Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);
// …
// 返回用户响应
return responseBuilder.build();
}
}

2.5 使用缓存:CacheInterceptor

缓存拦截器会根据请求的信息和缓存的响应的信息来判断是否存在缓存可用,如果有可以使用的缓存,那么就返回该缓存该用户,否则就继续责任链来从服务器中获取响应。当获取到响应的时候,又会把响应缓存到磁盘上面。以下是这部分的逻辑:

public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;
long now = System.currentTimeMillis();
// 根据请求和缓存的响应中的信息来判断是否存在缓存可用
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest; // 如果该请求没有使用网络就为空
Response cacheResponse = strategy.cacheResponse; // 如果该请求没有使用缓存就为空
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// 请求不使用网络并且不使用缓存,相当于在这里就拦截了,没必要交给下一级(网络请求拦截器)来执行
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message(“Unsatisfiable Request (only-if-cached)”)
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 该请求使用缓存,但是不使用网络:从缓存中拿结果,没必要交给下一级(网络请求拦截器)执行
if (networkRequest == null) {
return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
}
Response networkResponse = null;
try {
// 这里调用了执行链的处理方法,实际就是交给自己的下一级来执行了
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 这里当拿到了网络请求之后调用,下一级执行完毕会交给它继续执行,如果使用了缓存就把请求结果更新到缓存里
if (cacheResponse != null) {
// 服务器返回的结果是304,返回缓存中的结果
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
cache.trackConditionalCacheHit();
// 更新缓存
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 把请求的结果放进缓存里
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
}

对缓存,这里我们使用的是全局变量 cache,它是 InternalCache 类型的变量。InternalCache 是一个接口,在 OkHttp 中只有一个实现类 Cache。在 Cache 内部,使用了 DiskLruCache 来将缓存的数据存到磁盘上。DiskLruCache 以及 LruCache 是 Android 上常用的两种缓存策略。前者是基于磁盘来进行缓存的,后者是基于内存来进行缓存的,它们的核心思想都是 Least Recently Used,即最近最少使用算法。我们会在以后的文章中详细介绍这两种缓存框架,也请继续关注我们的文章。

另外,上面我们根据请求和缓存的响应中的信息来判断是否存在缓存可用的时候用到了 CacheStrategy 的两个字段,得到这两个字段的时候使用了非常多的判断,其中涉及 Http 缓存相关的知识,感兴趣的话可以自己参考源代码。

2.6 连接复用:ConnectInterceptor

连接拦截器 ConnectInterceptor 用来打开到指定服务器的网络连接,并交给下一个拦截器处理。这里我们只打开了一个网络连接,但是并没有发送请求到服务器。从服务器获取数据的逻辑交给下一级的拦截器来执行。虽然,这里并没有真正地从网络中获取数据,而仅仅是打开一个连接,但这里有不少的内容值得我们去关注。因为在获取连接对象的时候,使用了连接池 ConnectionPool 来复用连接。

public final class ConnectInterceptor implements Interceptor {

@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();

boolean doExtensiveHealthChecks = !request.method().equals(“GET”);
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}

这里的 HttpCodec 用来编码请求并解码响应,RealConnection 用来向服务器发起连接。它们会在下一个拦截器中被用来从服务器中获取响应信息。下一个拦截器的逻辑并不复杂,这里万事具备之后,只要它来从服务器中读取数据即可。可以说,OkHttp 中的核心部分大概就在这里,所以,我们就先好好分析一下,这里在创建连接的时候如何借助连接池来实现连接复用的。

根据上面的代码,当我们调用 streamAllocationnewStream() 方法的时候,最终会经过一系列的判断到达 StreamAllocation 中的 findConnection() 方法。

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
// …
synchronized (connectionPool) {
// …
// 尝试使用已分配的连接,已经分配的连接可能已经被限制创建新的流
releasedConnection = this.connection;
// 释放当前连接的资源,如果该连接已经被限制创建新的流,就返回一个Socket以关闭连接
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// 已分配连接,并且该连接可用
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// 如果该连接从未被标记为获得,不要标记为发布状态,reportedAcquired 通过 acquire() 方法修改
releasedConnection = null;
}

if (result == null) {
// 尝试供连接池中获取一个连接
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
// 关闭连接
closeQuietly(toClose);

if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// 如果已经从连接池中获取到了一个连接,就将其返回
return result;
}

boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}

synchronized (connectionPool) {
if (canceled) throw new IOException(“Canceled”);

if (newRouteSelection) {
// 根据一系列的 IP 地址从连接池中获取一个链接
List routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
// 从连接池中获取一个连接
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}

if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}

// 创建一个新的连接,并将其分配,这样我们就可以在握手之前进行终端
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}

// 如果我们在第二次的时候发现了一个池连接,那么我们就将其返回
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}

// 进行 TCP 和 TLS 握手
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());

Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;

// 将该连接放进连接池中
Internal.instance.put(connectionPool, result);

// 如果同时创建了另一个到同一地址的多路复用连接,释放这个连接并获取那个连接
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);

eventListener.connectionAcquired(call, result);
return result;
}

该方法会被放置在一个循环当中被不停地调用以得到一个可用的连接。它优先使用当前已经存在的连接,不然就使用连接池中存在的连接,再不行的话,就创建一个新的连接。所以,上面的代码大致分成三个部分:

  1. 判断当前的连接是否可以使用:流是否已经被关闭,并且已经被限制创建新的流;
  2. 如果当前的连接无法使用,就从连接池中获取一个连接;
  3. 连接池中也没有发现可用的连接,创建一个新的连接,并进行握手,然后将其放到连接池中。

在从连接池中获取一个连接的时候,使用了 Internalget() 方法。Internal 有一个静态的实例,会在 OkHttpClient 的静态代码快中被初始化。我们会在 Internalget() 中调用连接池的 get() 方法来得到一个连接。

从上面的代码中我们也可以看出,实际上,我们使用连接复用的一个好处就是省去了进行 TCP 和 TLS 握手的一个过程。因为建立连接本身也是需要消耗一些时间的,连接被复用之后可以提升我们网络访问的效率。那么这些连接被放置在连接池之后是如何进行管理的呢?我们会在下文中分析 OkHttp 的 ConnectionPool 中是如何管理这些连接的。

2.7 CallServerInterceptor

服务器请求拦截器 CallServerInterceptor 用来向服务器发起请求并获取数据。这是整个责任链的最后一个拦截器,这里没有再继续调用执行链的处理方法,而是把拿到的响应处理之后直接返回给了上一级的拦截器:

public final class CallServerInterceptor implements Interceptor {

@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// 获取 ConnectInterceptor 中初始化的 HttpCodec
HttpCodec httpCodec = realChain.httpStream();
// 获取 RetryAndFollowUpInterceptor 中初始化的 StreamAllocation
StreamAllocation streamAllocation = realChain.streamAllocation();
// 获取 ConnectInterceptor 中初始化的 RealConnection
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();

long sentRequestMillis = System.currentTimeMillis();

realChain.eventListener().requestHeadersStart(realChain.call());
// 在这里写入请求头
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);

Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if (“100-continue”.equalsIgnoreCase(request.header(“Expect”))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
// 在这里写入请求体
if (responseBuilder == null) {
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

结语

  • 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
  • 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。

  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

[外链图片转存中…(img-AxIglp51-1715725603075)]

[外链图片转存中…(img-o8Q5fAnK-1715725603076)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值