[Android]如何做一個崩潰率少於千分之三噶應用app(31)-元件化網路請求

Cang_Wang發表於2019-03-02
Android元件化架構
Android元件化架構

以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。

相信很多人都會用過頂頂大名的Retrofit2框架,本篇就介紹元件化網路請求問題。
先說一下重點原理吧

@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {
            // 呼叫介面方法
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            //可忽略
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            //快取服務介面
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
           //觸發回撥
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.adapt(okHttpCall);
          }
        });
  }
複製程式碼

其使用了動態代理的方式來代理介面呼叫,通過serviceMethod來快取介面對應的資訊,然後通過okHttpCall來啟動請求。

當工程有多個業務存在,你有一些業務可能訪問的地址頭會有多個,即baseUrl的切換問題。
解決方案1
@Get , @Post 這些標註到每個介面方法上的註解不僅可以傳相對路徑,還可以傳全路徑,全路徑可以簡單解決,但是這樣只能固定死地址,如果伺服器當機,想換個後備伺服器你要怎麼辦?你要麼只能多備一份後備伺服器地址,如果你非常不走運,都被黑客攻爆了,我只能同情你了。

解決方案2
官方出了@Url的註解用於改變路徑地址的方案,這個還好是傳進去的,是可以解決方案1中的問題,如果業務模組多了,每次都要單獨傳入地址,有點麻煩。

public interface UserService {  
    @GET
    public Call<ResponseBody> profilePicture(@Url String url);
}

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://your.api.url/");
    .build();

UserService service = retrofit.create(UserService.class);  
service.profilePicture("https://s3.amazon.com/profile-picture/path");
實際只會請求@Url中的全地址

複製程式碼

解決方案3
在Base做一個公用的,特別業務自身一個Retrofit例項然後建立ApiService,這樣就能夠間隔不同的業務,其Base底層提供一個複用OkHttpClient。如果模組中有其他特殊地質,使用第二種方案解決。
缺點是Retrofit例項變多。

解決方案4
研究過OkHttp的同學,應該知道OkHttp的攔截器機制,自定義一個攔截器,不清楚Okttp攔截器機制的,可以看這篇文章okhttp3 攔截器原始碼分析。在其請求的時候攔截掉請求,再替換掉Url,這樣有點破壞上層的封裝,你們看到的RetrofitUrlManager就是使用這個方案來完成的,在請求方法加入@Header的註解來標註使用哪個訪問基類地址。

//宣告請求對應的Headers
 public interface ApiService {
     @Headers({"Domain-Name: douban"}) // Add the Domain-Name header
     @GET("/v2/book/{id}")
     Observable<ResponseBody> getBook(@Path("id") int id);
}

// 註冊基類baseUrl
 RetrofitUrlManager.getInstance().putDomain("douban", "https://api.douban.com");

// 切換基類baseUrl
 RetrofitUrlManager.getInstance().setGlobalDomain("your BaseUrl");

複製程式碼
    private RetrofitUrlManager() {
        if (!DEPENDENCY_OKHTTP) { //使用本管理器必須依賴 Okhttp
            throw new IllegalStateException("Must be dependency Okhttp");
        }
        setUrlParser(new DefaultUrlParser()); 
        //攔截器
        this.mInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                if (!isRun()) // 可以在 App 執行時, 隨時通過 setRun(false) 來結束本管理器的執行
                    return chain.proceed(chain.request());
                return chain.proceed(processRequest(chain.request()));
            }
        };
    }

   //掛接攔截器
    public OkHttpClient.Builder with(OkHttpClient.Builder builder) {
        return builder
                .addInterceptor(mInterceptor);
    }

    //掛接到OkHttp的攔截器   
     this.mOkHttpClient = RetrofitUrlManager.getInstance().with(new OkHttpClient.Builder()) //RetrofitUrlManager 初始化
                .readTimeout(5, TimeUnit.SECONDS)
                .connectTimeout(5, TimeUnit.SECONDS)
                .build();

複製程式碼
 public Request processRequest(Request request) {

        Request.Builder newBuilder = request.newBuilder();

        String url = request.url().toString();
        //如果 Url 地址中包含 IDENTIFICATION_IGNORE 識別符號, 管理器將不會對此 Url 進行任何切換 BaseUrl 的操作
        if (url.contains(IDENTIFICATION_IGNORE)) {
            return pruneIdentification(newBuilder, url);
        }
        //通過請求註解獲取
        String domainName = obtainDomainNameFromHeaders(request);

        HttpUrl httpUrl;

        Object[] listeners = listenersToArray();

        // 如果有 header,獲取 header 中 domainName 所對映的 url,若沒有,則檢查全域性的 BaseUrl,未找到則為null
        if (!TextUtils.isEmpty(domainName)) {
            notifyListener(request, domainName, listeners);
            httpUrl = fetchDomain(domainName);
            newBuilder.removeHeader(DOMAIN_NAME);
        } else {
            notifyListener(request, GLOBAL_DOMAIN_NAME, listeners);
            httpUrl = getGlobalDomain();
        }
        //獲取新的baseUrl地址
        if (null != httpUrl) {
            HttpUrl newUrl = mUrlParser.parseUrl(httpUrl, request.url());
            if (debug)
                Log.d(RetrofitUrlManager.TAG, "The new url is { " + newUrl.toString() + " }, old url is { " + request.url().toString() + " }");
          
            if (listeners != null) {
                for (int i = 0; i < listeners.length; i++) {
                    ((onUrlChangeListener) listeners[i]).onUrlChanged(newUrl, request.url()); // 通知監聽器此 Url 的 BaseUrl 已被切換
                }
            }
           //返回鏈式呼叫
            return newBuilder
                    .url(newUrl)
                    .build();
        }

        return newBuilder.build();

    }
複製程式碼

缺點是當極端情況,模組間互動請求Http的時候,伴隨每次都要每個請求都需要先切換Url再請求,以保障url是正確的。

解決方案5
修改Retrofit的原始碼,新增一個HashMap的儲存多個訪問地址頭,封裝介面,需要的時候再取出配置。
缺點是無法跟隨Retrofit版本更新。當然外部去維護一個HashMap,然後可以試著使用Hook的方法來hook掉baseUrl引數(壞笑)

這裡並不存在最好的方案,相對於元件化使用,第三種方案相對解耦程度會高一點,第二種方案會靈活性上可以伺服器動態配置,第四五中方案Retrofit都是單例,記憶體消耗會低一點。


群1已經滿了,學習元件化可以進群2 763094035

[Android]如何做一個崩潰率少於千分之三噶應用app(31)-元件化網路請求


相關文章