主頁 > 移動端開發 > Android OkHttp使用和原始碼詳解

Android OkHttp使用和原始碼詳解

2021-09-09 13:01:36 移動端開發

介紹

OkHttp 是一套處理 HTTP 網路請求的依賴庫,由 Square 公司設計研發并開源,目前可以在 Java 和 Kotlin 中使用,對于 Android App 來說,OkHttp 現在幾乎已經占據了所有的網路請求操作,RetroFit + OkHttp 實作網路請求似乎成了一種標配,因此它也是每一個 Android 開發工程師的必備技能,了解其內部實作原理可以更好地進行功能擴展、封裝以及優化,

適用于 Android 和 Java 應用程式的 HTTP 和 HTTP/2 客戶端,

OkHttp的4.0.x版本已經全部由java替換到了Kotlin,API的一些使用也會有些不同,

Github傳送門

檔案和 API

要求

支持的版本

4.0.x :Android 5.0+(API 級別 21+)和 Java 8+,

3.12.x :Android 2.3+(API 級別 9+)和 Java 7+,平臺可能不支持 TLSv1.2,(2021-12-31不再支持)

OkHttp有一個庫的依賴Okio,用于高性能I/O一個小型library,它適用于 Okio 1.x(用 Java 實作)或 Okio 2.x(升級到 Kotlin),

本文使用的OkHttp的版本為3.14.2,不是不會接入高版本,主要是4.0.x版本已經全部由java替換到了Kotlin,Kotlin不太熟怕理解錯了,誤匯入民群眾,

dependencies {
    //本文使用
    implementation 'com.squareup.okio:okio:1.15.0'
    implementation 'com.squareup.okhttp3:okhttp:3.14.2'
    
    //高版本
    // define a BOM and its version
    implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.0"))

    // define any required OkHttp artifacts without version
    implementation("com.squareup.okhttp3:okhttp")
    implementation("com.squareup.okhttp3:logging-interceptor")
}


網路請求流程分析

OkHttp 經過幾次迭代后,已經發生了很多變化,更好的 WebSocket 支持、更多的 Interceptor 責任鏈,甚至連最核心的 HttpEngine 也變成了 HttpCodec,本文會重新梳理整個網路請求的流程,以及實作機制,

先看下 OkHttp 的基本使用:

public void okHttp(String url){
        //創建OkHttpClient物件
        OkHttpClient client = new OkHttpClient();
        //創建Request
        Request request = new Request.Builder()
                .url(url)
                .build();
        //創建Call物件client.newCall(request)
        //通過execute()方法獲得請求回應的Response物件
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {}

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if(response.isSuccessful()){
                    String result = response.body().string();
                    //處理UI需要切換到UI執行緒處理
                }
            }
        });
    }

除了直接 new OkHttpClient 之外,還可以使用內部工廠類 Builder 來設定 OkHttpClient,如下所示:

    public void buildHttp(String url){
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(15, TimeUnit.SECONDS)//設定超時
                .addInterceptor(interceptor)    //攔截器
                .proxy(proxy)       //設定代理
                .cache(cache);      //設定快取
        Request request = new Request.Builder()
                .url(url)
                .build();
        builder.build().newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {}

            @Override
            public void onResponse(Call call, Response response) throws IOException {}
        });
    }

請求操作的起點從 OkHttpClient.newCall().enqueue() 方法開始

OkHttpClient.newCall

  @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;
  }

這個方法會回傳一個 RealCall 物件,通過它將網路請求操作添加到請求佇列中,

RealCall.enqueue

  @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));
  }

client.dispatcher()回傳Dispatcher,呼叫 Dispatcher 的 enqueue 方法,執行一個異步網路請求的操作,

Dispatcher 是 OkHttpClient 的調度器,是一種門戶模式,主要用來實作執行、取消異步請求操作,本質上是內部維護了一個執行緒池去執行異步操作,并且在 Dispatcher 內部根據一定的策略,保證最大并發個數、同一 host 主機允許執行請求的執行緒個數等,

Dispatcher.enqueue

  void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }

實際上就是使用執行緒池執行了一個 AsyncCall,而 AsyncCall 繼承了 NamedRunnable,NamedRunnable 實作了 Runnable 介面,因此整個操作會在一個子執行緒(非 UI 執行緒)中執行,

NamedRunnable

/**
 * Runnable implementation which always sets its thread name.
 */
public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

在 run 方法中執行了 一個抽象方法 execute 這個抽象方法被 AsyncCall 實作,

AsyncCall.execute

@Override protected void execute() {
      boolean signalledCallback = false;
      transmitter.timeoutEnter();
      try {
        Response response = getResponseWithInterceptorChain();
        signalledCallback = true;
        responseCallback.onResponse(RealCall.this, response);
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

從上面看出而真正獲取請求結果的方法是在 getResponseWithInterceptorChain 方法中,從名字也能看出其內部是一個攔截器的呼叫鏈,

RealCall.getResponseWithInterceptorChain

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }

Interceptor:攔截器是一種強大的機制,可以監視、重寫和重試呼叫,

每一個攔截器的作用如下:

  • BridgeInterceptor:主要對 Request 中的 Head 設定默認值,比如 Content-Type、Keep-Alive、Cookie 等,

  • CacheInterceptor:負責 HTTP 請求的快取處理,

  • ConnectInterceptor:負責建立與服務器地址之間的連接,也就是 TCP 鏈接,

  • CallServerInterceptor:負責向服務器發送請求,并從服務器拿到遠端資料結果,

  • RetryAndFollowUpInterceptor:此攔截器從故障中恢復,并根據需要執行重定向,如果呼叫被取消,它可能會引發IOException,

在添加上述幾個攔截器之前,會呼叫 client.interceptors 將開發人員設定的攔截器添加到串列當中,

對于 Request 的 Head 以及 TCP 鏈接,我們能控制修改的成分不是很多,所以咱們了解 CacheInterceptorCallServerInterceptor

CacheInterceptor 快取攔截器

CacheInterceptor 主要做以下幾件事情:

1、根據 Request 獲取當前已有快取的 Response(有可能為 null),并根據獲取到的快取 Response,創建 CacheStrategy 物件,

2、 通過 CacheStrategy 判斷當前快取中的 Response 是否有效(比如是否過期),如果快取 Response 可用則直接回傳,否則呼叫 chain.proceed() 繼續執行下一個攔截器,也就是發送網路請求從服務器獲取遠端 Response,

3、如果從服務器端成功獲取 Response,再判斷是否將此 Response 進行快取操作,

CacheInterceptor.intercept

@Override public Response intercept(Chain chain) throws IOException {
  //根據 Request 獲取當前已有快取的 Response(有可能為 null),并根據獲取到的快取 Response
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
       : null;
  //獲取當前時間
  long now = System.currentTimeMillis();
  //創建 CacheStrategy 物件
  //通過 CacheStrategy 來判斷快取是否有效
  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()); // The cache candidate wasn't applicable. Close it.
  }
  //如果我們被禁止使用網路,并且快取不足,則失敗,回傳空相應(Util.EMPTY_RESPONSE)
  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();
  }

  // 如果快取有效,快取 Response 可用則直接回傳
  if (networkRequest == null) {
    return cacheResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .build();
  }
  //沒有快取或者快取失敗,則發送網路請求從服務器獲取Response
  Response networkResponse = null;
  try {
    //執行下一個攔截器,networkRequest
    //發起網路請求
    networkResponse = chain.proceed(networkRequest);
  } finally {
    //如果我們在I/O或其他方面崩潰,請不要泄漏cache body,
    if (networkResponse == null && cacheCandidate != null) {
      closeQuietly(cacheCandidate.body());
    }
  }
  ,,,
  //通過網路獲取最新的Response
  Response response = networkResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();
  //如果開發人員有設定自定義cache,則將最新response快取
  if (cache != null) {
    if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
      // Offer this request to the cache.
      CacheRequest cacheRequest = cache.put(response);
      return cacheWritingResponse(cacheRequest, response);
    }
    //回傳response(快取或網路)
    return response;
  }

通過 Cache 實作快取功能

通過上面快取攔截器的流程可以看出,OkHttp 只是規范了一套快取策略,但是具體使用何種方式將資料快取到本地,以及如何從本地快取中取出資料,都是由開發人員自己定義并實作,并通過 OkHttpClient.Builder 的 cache 方法設定,

OkHttp 提供了一個默認的快取類 Cache.java,我們可以在構建 OkHttpClient 時,直接使用 Cache 來實作快取功能,只需要指定快取的路徑,以及最大可用空間即可,如下所示:

OkHttpClient.Builder builder = new OkHttpClient.Builder();
    builder.connectTimeout(15, TimeUnit.SECONDS)//設定超時
            攔截器
            .addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    return null;
                }
            })
            //設定代理
            .proxy(new Proxy(Proxy.Type.HTTP,null)) 
            //設定快取
            //AppGlobalUtils.getApplication() 通過反射得到Application實體
            //getCacheDir內置 cache 目錄作為快取路徑
            //maxSize 10*1024*1024 設定最大快取10MB
            .cache(new Cache(AppGlobalUtils.getApplication().getCacheDir(),
                        10*1024*1024));

Cache 內部使用了 DiskLruCach 來實作具體的快取功能,如下所示:

 /**
   * Create a cache of at most {@code maxSize} bytes in {@code directory}.
   */
  public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

DiskLruCache 最侄訓將需要快取的資料保存在本地,如果感覺 OkHttp 自帶的這套快取策略太過復雜,我們可以設定使用 DiskLruCache 自己實作快取機制,

LRU:是近期最少使用的演算法(快取淘汰演算法),它的核心思想是當快取滿時,會優先淘汰那些近期最少使用的快取物件,采用LRU演算法的快取有兩種:LrhCache和DisLruCache,分別用于實作記憶體快取和硬碟快取,其核心思想都是LRU快取演算法,

CallServerInterceptor 詳解

CallServerInterceptor 是 OkHttp 中最后一個攔截器,也是 OkHttp 中最核心的網路請求部分,

CallServerInterceptor.intercept

  @Override public Response intercept(Chain chain) throws IOException {
    //獲取RealInterceptorChain
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //獲取Exchange
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();

    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (request.body().isDuplex()) {
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
          // from being reused. Otherwise we're still obligated to transmit the request body to
          // leave the connection in a consistent state.
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      exchange.noRequestBody();
    }

    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

    上面是向服務器端發送請求資料
    
    -----強大的分割線----------
    
    下面是從服務端獲取相應資料
    并構建 Response 物件
    
    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }

    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }

    exchange.responseHeadersEnd(response);

    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }
    ,,,
    return response;
  }

小結

首先 OkHttp 內部是一個門戶模式,所有的下發作業都是通過一個門戶 Dispatcher 來進行分發,

然后在網路請求階段通過責任鏈模式,鏈式的呼叫各個攔截器的 intercept 方法,重點介紹了 2 個比較重要的攔截器:CacheInterceptor 和 CallServerInterceptor,它們分別用來做請求快取和執行網路請求操作,

往期回顧

RecyclerView 繪制流程及Recycler快取

Glide 快取機制及原始碼(二)

Glide 的簡單使用(一)

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/298694.html

標籤:其他

上一篇:OPPO對諾基亞發起5G專利侵權訴訟

下一篇:Android 使用心得 ??| 使用adb命令查看某個應用的 行程優先級!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more