Android-okhttp详解

目录

一,介绍

二,简单使用

三,流程分析

四,分发器

五,拦截器

[5.1 重试及重定向拦截器](#5.1 重试及重定向拦截器)

[5.1.1 重试](#5.1.1 重试)

[5.1.2 重定向](#5.1.2 重定向)

[5.2 桥接拦截器](#5.2 桥接拦截器)

[5.3 缓存拦截器](#5.3 缓存拦截器)

[5.4 连接拦截器](#5.4 连接拦截器)

[5.5 请求服务器拦截器](#5.5 请求服务器拦截器)


一,介绍

OkHttp是当下Android使用最频繁的网络请求框架,由Square公司开源。Google在Android4.4以后开始将源码中 的HttpURLConnection底层实现替换为OKHttp,同时现在流行的Retrofit框架底层同样是使用OKHttp的。

okhttp的优点是:

1,支持Http1、Http2、Quic以及WebSocket

2,连接池复用底层TCP(Socket),减少请求延时

3,无缝的支持GZIP减少数据流量

4,缓存响应数据减少重复的网络请求

5,请求失败自动重试主机的其他ip,自动重定向

二,简单使用

首先需要添加依赖:

java 复制代码
implementation 'com.squareup.okhttp3:okhttp:3.14.7'
//Okio库 是对Java.io和java.nio的补充,以便能够更加方便,快速的访问、存储和处理你的数据。OkHttp的底层使用该库作为支持。
implementation 'com.squareup.okio:okio:1.17.5'

然后在清单文件中添加需要的权限:网络权限和读写权限都是必不可少的

java 复制代码
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

先来看同步请求:

java 复制代码
public String url ="https://www.wanandroid.com/article/list/0/json";
private static final String TAG = Module1MainActivity.class.getName();


/**
 * 同步请求
 * */
private void syncRequest() throws IOException {
    //创建OkHttpClient
    OkHttpClient okHttpClient =new OkHttpClient();
    //创建request 并将请求url传进去 设置为get请求方式
    Request request =new Request.Builder().url(url).get().build();
    //获得请求的call对象
    Call call = okHttpClient.newCall(request);
    //执行同步请求
    Response response = call.execute();
    //获得响应体
    ResponseBody body = response.body();
    //输出响应体
    Log.d(TAG,body.string());
}

然后异步请求:

java 复制代码
/**
 * 异步请求
 * */
private void asyncRequest(){
    //创建OkHttpClient
    OkHttpClient okHttpClient =new OkHttpClient();
    //创建request 并将请求url传进去 设置为get请求方式
    Request request =new Request.Builder().url(url).get().build();
    //获得请求的call对象
    Call call = okHttpClient.newCall(request);
    //执行异步请求
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            Log.e(TAG,"请求失败:"+e.getMessage());
        }
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            ResponseBody body = response.body();
            String string = body.string();
            byte[] bytes = body.bytes();
            InputStream inputStream = body.byteStream();
            Log.e(TAG,"请求成功:"+string);
        }
    });
}

三,流程分析

OkHttp请求过程中主要用到OkHttpClient、Request、Call、Dispatcher, Response以及拦截器这几个类,他们之间的关系主要如下图:

下面我们来看一下创建Call对象的源码:

java 复制代码
//获得请求的call对象
Call call = okHttpClient.newCall(request);

在OkhttpClient中:

java 复制代码
@Override public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}

接着走到RealCall的newRealCall方法:

java 复制代码
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
  // Safely publish the Call instance to the EventListener.
  RealCall call = new RealCall(client, originalRequest, forWebSocket);
  call.transmitter = new Transmitter(client, call);
  return call;
}

这里会把OkhttpClient对象,Request对象传入到RealCall中,并创建RealCall对象。

同步请求会执行RealCall的execute方法:

java 复制代码
@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  transmitter.timeoutEnter();
  transmitter.callStart();
  try {
    client.dispatcher().executed(this);
    return getResponseWithInterceptorChain();
  } finally {
    client.dispatcher().finished(this);
  }
}

关键看这行代码:

java 复制代码
client.dispatcher().executed(this);

这里会直接执行分发器的executed方法,然后调用责任链模式的拦截器方法:

java 复制代码
return getResponseWithInterceptorChain();

异步请求会执行RealCall的enqueue方法:

java 复制代码
@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  transmitter.callStart();
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

这里只是调用了分发器的enqueue方法。

四,分发器

分发器Dispatcher就是来调配请求任务的,内部包含一个线程池,一个异步请求等待队列readyAsyncCalls,一个异步请求正在执行队列runningAsyncCalls,一个同步请求正在执行队列runningSyncCalls

java 复制代码
public final class Dispatcher {
  //异步请求同时存在的最大请求
  private int maxRequests = 64;

  //异步请求同一域名同时存在的最大请求
  private int maxRequestsPerHost = 5;

  //闲置任务(没有请求时可执行一些任务,由使用者设置)
  private @Nullable Runnable idleCallback;

  //异步请求使用的线程池
  private @Nullable ExecutorService executorService;

  //异步请求等待执行队列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  //异步请求正在执行队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  //同步请求正在执行队列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();


}

那我们接着来看上面的同步请求执行到分发器的executed方法是怎么做的:

java 复制代码
synchronized void executed(RealCall call) {
  runningSyncCalls.add(call);
}

这里只是将call加入到了同步请求正在执行队列,因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。

然后来看看异步请求的enqueue方法:

java 复制代码
void enqueue(AsyncCall call) {
  synchronized (this) {
    //加入到异步请求准备队列
    readyAsyncCalls.add(call);
    // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
    // the same host.
    if (!call.get().forWebSocket) {
      AsyncCall existingCall = findExistingCallWithHost(call.host());
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
    }
  }
  promoteAndExecute();
}

首先将call加入到了异步请求准备队列,然后执行了promoteAndExecute();

java 复制代码
private boolean promoteAndExecute() {
  assert (!Thread.holdsLock(this));
  List<AsyncCall> executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall asyncCall = i.next();
      if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
      i.remove();
      asyncCall.callsPerHost().incrementAndGet();
      executableCalls.add(asyncCall);
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }
  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    AsyncCall asyncCall = executableCalls.get(i);
    asyncCall.executeOn(executorService());
  }
  return isRunning;
}

这里面的逻辑是当正在执行的任务未超过最大限制64,同时runningCallsForHost(call) < maxRequestsPerHost同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。加入正在执行队列的任务直接执行,但是如果加入等待队列后,就需要等待有空闲名额才开始执行。所以会执行asyncCall.executeOn(executorService());方法:

java 复制代码
void executeOn(ExecutorService executorService) {
  assert (!Thread.holdsLock(client.dispatcher()));
  boolean success = false;
  try {
    executorService.execute(this);
    success = true;
  } catch (RejectedExecutionException e) {
    InterruptedIOException ioException = new InterruptedIOException("executor rejected");
    ioException.initCause(e);
    transmitter.noMoreExchanges(ioException);
    responseCallback.onFailure(RealCall.this, ioException);
  } finally {
    if (!success) {
      client.dispatcher().finished(this); // This call is no longer running!
    }
  }
}

这里不管是执行请求成功还是失败,都会走到finally里面的分发器的finished方法:

java 复制代码
 //异步请求调用
void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
 }
 //同步请求调用
void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
 }
 
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
        //不管异步还是同步,执行完后都要从队列移除(runningSyncCalls/runningAsyncCalls)
        if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
        if (promoteCalls) promoteCalls();
        //异步任务和同步任务正在执行的和
        runningCallsCount = runningCallsCount();
        idleCallback = this.idleCallback;
    }
    // 没有任务执行执行闲置任务
    if (runningCallsCount == 0 && idleCallback != null) {
        idleCallback.run();
    }
 }

只有异步任务才会存在限制与等待,所以在执行完了移除正在执行队列中的元素后,异步任务结束会 执行promoteCalls():

java 复制代码
private void promoteCalls() {
    //如果任务满了直接返回
    if (runningAsyncCalls.size() >= maxRequests) return; 
    //没有等待执行的任务,返回
    if (readyAsyncCalls.isEmpty()) return; 
    //遍历等待执行队列
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall call = i.next();
        //等待任务想要执行,还需要满足:这个等待任务请求的Host不能已经存在5个了
        if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            runningAsyncCalls.add(call);
            executorService().execute(call);
        }
 
        if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
 }

在满足条件下,会把等待队列中的任务移动到runningAsyncCalls并交给线程池执行。

上面的异步请求流程可以总结为下图:

需要注意的是,okhttp的线程池的等待队列采用的是SynchronousQueue,这样就避免了任务的等待问题,所以它是无等待,最大并发的。线程池这里不详细讲解,可以查看文章Android 多线程并发详解_android多线程并发处理-CSDN博客

Android多线程讲解二_android线程专题讲解-CSDN博客

五,拦截器

Okhttp的拦截器除了我们自定义的拦截器之外,主要有五大拦截器:

1、重试拦截器:在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后 ,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。

2、桥接拦截器:在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的 行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。 4、连接拦截器:在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后 不进行额外的处理。

5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

Okhttp的拦截器主要采用了责任链的设计模式,关于责任链设计模式,请查考文章Android设计模式--责任链模式_android 责任链模式-CSDN博客

其流程如下:

5.1 重试及重定向拦截器

RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试与重定向。

5.1.1 重试

请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。

RouteException:

java 复制代码
 catch (RouteException e) {
    //路由异常,连接未成功,请求还没发出去
    if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
        throw e.getLastConnectException();
    }
    releaseConnection = false;
    continue;
 } 

IOException:

java 复制代码
catch (IOException e) {
    //请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
    // HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true
    boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
    if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
 }

两个异常都是根据recover 方法判断是否能够进行重试,如果返回true,则表示允许重试。

java 复制代码
private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);
    //1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
    if (!client.retryOnConnectionFailure()) return false;
    //2、如果是RouteException,不用管这个条件,
    // 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
        return false;
 
    //3、不是属于重试的异常 不重试
    if (!isRecoverable(e, requestSendStarted)) return false;
 
    //4、没有可以用来连接的路由路线 不重试
    if (!streamAllocation.hasMoreRoutes()) return false;

    return true;
 }

所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在isRecoverable中进行判断:

java 复制代码
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
 // 出现协议异常,不能重试
if (e instanceof ProtocolException) {
 return false;
 }
 // 如果不是超时异常,不能重试
if (e instanceof InterruptedIOException) {
 return e instanceof SocketTimeoutException && !requestSendStarted;
 }
 // SSL握手异常中,证书出现问题,不能重试
if (e instanceof SSLHandshakeException) {
 if (e.getCause() instanceof CertificateException) {
 return false;
 }
 }
 // SSL握手未授权异常 不能重试
if (e instanceof SSLPeerUnverifiedException) {
 return false;
 }
 return true;
 }

5.1.2 重定向

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重 定向的判断。重定向的判断位于 followUpRequest 方法:

java 复制代码
private Request followUpRequest(Response userResponse) throws IOException {
 if (userResponse == null) throw new IllegalStateException();
 Connection connection = streamAllocation.connection();
 Route route = connection != null
 ? connection.route()
 : null;
 int responseCode = userResponse.code();
 final String method = userResponse.request().method();
 switch (responseCode) {
 // 407 客户端使用了HTTP代理服务器,在请求头中添加 "Proxy-Authorization",让代理服务器授权
case HTTP_PROXY_AUTH:
 Proxy selectedProxy = route != null
享学课堂
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using 
proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);
      // 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 "Authorization" 
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向 
      // 307 临时重定向
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303 
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 如果用户不允许重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 从响应头取出location 
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 根据location 配置新的请求 url
        HttpUrl url = userResponse.request().url().resolve(location);
        // 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
        if (url == null) return null;
        // 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;
 
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        /**
         *  重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,
         *  即只有 PROPFIND 请求才能有请求体
         */
        //请求不是get与head
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
           // 除了 PROPFIND 请求之外都改成GET请求
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          // 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉
          if (!maintainBody) {
享学课堂
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }
 
        // 在跨主机重定向时,删除身份验证请求头
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }
 
        return requestBuilder.url(url).build();
 
      // 408 客户端请求超时 
      case HTTP_CLIENT_TIMEOUT:
        // 408 算是连接失败了,所以判断用户是不是允许重试
        if (!client.retryOnConnectionFailure()) {
            return null;
        }
        // UnrepeatableRequestBody实际并没发现有其他地方用到
        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
            return null;
        }
        // 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求
了
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
            return null;
        }
        // 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
        if (retryAfter(userResponse, 0) > 0) {
            return null;
        }
        return userResponse.request();
       // 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
       case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
            return null;
         }
 
         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
            return userResponse.request();
         }
 
         return null;
      default:
        return null;
    }
 }

如果此方法返回空,那就表 示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request ,但是需要注意的是, 我们的 followup 在拦截器中定义的最大次数为20次。

5.2 桥接拦截器

BridgeInterceptor ,连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设 置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。

补全请求头:

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

1、保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的

2、如果使用gzip返回的数据,则使用 GzipSource 包装便于解析。

5.3 缓存拦截器

CacheInterceptor,在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存在Get请求的缓存)

步骤为:

1、从缓存中获得对应请求的响应缓存

2、创建CacheStrategy ,创建时会判断是否能够使用缓存,在CacheStrategy 中存在两个成员: networkRequest 与cacheResponse

3、交给下一个责任链继续处理

4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)

缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓存,或是请求服务器都是通过CacheStrategy判断

总结:

1、如果从缓存获取的Response是null,那就需要使用网络请求获取响应;

2、如果是Https请求,但是又丢失了 握手信息,那也不能使用缓存,需要进行网络请求;

3、如果判断响应码不能缓存且响应头有no-store标识,那 就需要进行网络请求;

4、如果请求头有no-cache标识或者有If-Modified-Since/If-None-Match,那么需要进行 网络请求; 5、如果响应头没有no-cache标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行 网络请求;

6、如果缓存过期了,判断响应头是否设置Etag/Last-Modified/Date,没有那就直接使用网络请求否 则需要考虑服务器返回304; 并且,只要需要进行网络请求,请求头中就不能包含only-if-cached,否则框架直接返回504!

缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,如果确定需要发起网络请求,则 下一个拦截器为 ConnectInterceptor

5.4 连接拦截器

ConnectInterceptor ,打开与目标服务器的连接,并执行下一个拦截器。

源码如下:

java 复制代码
/**
 * Opens a connection to the target server and proceeds to the next interceptor.
 */
public final class ConnectInterceptor implements Interceptor {
    public final OkHttpClient client;

    public ConnectInterceptor(OkHttpClient client) {
        this.client = client;
    }

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

        // We need the network to satisfy this request. Possibly for validating a conditional GET.
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        RealConnection connection = streamAllocation.connection();

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

首先我们看到的 StreamAllocation 这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在这里。

当一个请求发出,需要建立连接,连接建立后需要使用流用来读写数据 ,而这个StreamAllocation就是协调请 求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。 这里使用的 newStream 方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的 HttpCodec 中包含了 输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。

StreamAllocation 中简单来说就是维护连接: RealConnection ------封装了Socket与一个Socket连接池。

这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。

5.5 请求服务器拦截器

CallServerInterceptor,利用HttpCodec发出请求到服务器并且解析生成Response。

这个拦截器的主要作用就是完成HTTP协议报文的封装与解析

相关推荐
独自破碎E33 分钟前
【BISHI9】田忌赛马
android·java·开发语言
代码s贝多芬的音符2 小时前
android 两个人脸对比 mlkit
android
darkb1rd4 小时前
五、PHP类型转换与类型安全
android·安全·php
gjxDaniel4 小时前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj504 小时前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
峥嵘life5 小时前
Android16 【CTS】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·学习
stevenzqzq6 小时前
Compose 中的状态可变性体系
android·compose
似霰6 小时前
Linux timerfd 的基本使用
android·linux·c++
darling3318 小时前
mysql 自动备份以及远程传输脚本,异地备份
android·数据库·mysql·adb
你刷碗8 小时前
基于S32K144 CESc生成随机数
android·java·数据库