Android 面試開源框架篇

一隻有交流障礙的醜程式猿發表於2018-05-13

本文是Android面試題整理中的一篇,內容包括

  • LeakCanary
  • Okhttp
  • Retrofit
  • Fresco 使用
  • EventBus

LeakCanary


  1. 在Application中註冊一個ActivityLifecycleCallbacks來監聽Activity的銷燬
  2. 通過IdleHandler在主執行緒空閒時進行檢測
  3. 檢測是通過WeakReference實現的,如果沒有被回收會再次呼叫gc再確認一遍
  4. 確認有洩漏後,dump hprof檔案,並開啟一個程式IntentService通過HAHA進行分析

OkHttp(基於3.9版本)


使用

1. 在gradle中新增依賴

compile 'com.squareup.okhttp3:okhttp:3.9.0'
compile 'com.squareup.okio:okio:1.13.0'
複製程式碼

2. 建立OkHttpClient,並對timeout等進行設定

File sdcache = getExternalCacheDir();
int cacheSize = 10 * 1024 * 1024;
OkHttpClient.Builder builder = new OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .cache(new Cache(sdcache.getAbsoluteFile(), cacheSize));
OkHttpClient mOkHttpClient=builder.build();
複製程式碼

3. 建立Request

  • get請求
Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .build();
複製程式碼
  • post請求(post需要傳入requsetBody)
RequestBody formBody = new FormEncodingBuilder()
            .add("size", "10")
            .build();
    Request request = new Request.Builder()
            .url("http://api.1-blog.com/biz/bizserver/article/list.do")
            .post(formBody)
            .build();
複製程式碼

4. 建立Call並執行(okHttp的返回結果並沒有在ui執行緒)

Call call = mOkHttpClient.newCall(request);
複製程式碼
  • 同步執行
Response mResponse=call.execute();
        if (mResponse.isSuccessful()) {     
           return mResponse.body().string();
       } else {
           throw new IOException("Unexpected code " + mResponse);
       }
複製程式碼
  • 非同步執行
call.enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
        }
        @Override
        public void onResponse(Response response) throws IOException {
            String str = response.body().string();
            Log.i("wangshu", str);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(getApplicationContext(), "請求成功", Toast.LENGTH_SHORT).show();
                }
            });
        }
    });
複製程式碼

4. 封裝

因為以下原因,所以我們需要封裝:

  • 避免重複程式碼編寫
  • 請求的回撥改為UI執行緒
  • 其他需要的邏輯:例如加解密等

OkHttp中的設計模式

  1. Builder模式:OkHttpClient 和Request等都是通過Builder模式建立的
  2. 責任鏈模式:攔截器通過責任鏈模式進行工作
  3. 門面模式:整體採用門面模式,OkHttpClient為門面,向子系統委派任務
  4. 享元模式:連線池等採用了享元模式
  5. 其他:工廠模式、代理模式等

原始碼分析

1. Call

  • Call的實現類為RealCall
  • 在執行execute或者enqueue時,會取出okHttpClient中的Dispatcher執行對應的方法
client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
複製程式碼

2. Diapatcher

  • Diapatcher在OkHttpClient build時進行初始化
  • Dispatcher負責進行任務排程,內部維護一個執行緒池,處理併發請求
  • Dispatcher內部有三個佇列
/** 將要執行的非同步請求佇列 */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/**正在執行的非同步請求佇列 */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** 正在執行的同步請求佇列 */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
複製程式碼
  • 執行時,執行緒會呼叫AsyncCall的excute方法

3. AsyncCall

  • AsyncCall是RealCall的一個內部類,實現了Runnalbe介面
  • AsyncCall 通過 getResponseWithInterceptorChain方法取得Response
  • 執行完畢後通過client.dispatcher().finished(this);將自身從dispatcher佇列中取出,並取出下一個加入相應佇列
//AsyncCall 的excute方法
@Override protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain(forWebSocket);
    if (canceled) {
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {
    if (signalledCallback) {
      // Do not signal the callback twice!
      logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}

複製程式碼

4. getResponseWithInterceptorChain

getResponseWithInterceptorChain是用責任鏈的方式,執行攔截器,對請求和請求結果進行處理

  • getResponseWithInterceptorChain 中建立攔截器,並建立第一個RealInterceptorChain,執行其proceed方法
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    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, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
複製程式碼
  • RealInterceptorChain的proceed方法中,會取出攔截器,並建立下一個Chain,將其作為引數傳給攔截器的intercept方法
  // If there's another interceptor in the chain, call that.
  if (index < client.interceptors().size()) {
    Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
    //從攔截器列表取出攔截器
    Interceptor interceptor = client.interceptors().get(index);
    Response interceptedResponse = interceptor.intercept(chain);

    if (interceptedResponse == null) {
      throw new NullPointerException("application interceptor " + interceptor
          + " returned null");
    }

    return interceptedResponse;
  }

  // No more interceptors. Do HTTP.
  return getResponse(request, forWebSocket);
}

複製程式碼

攔截器

1. 自定義攔截器

  • 自定義攔截器分為兩類,interceptor和networkInterceptor(區別:networkInterceptor處理網路相關任務,如果response直接從快取返回了,那麼有可能不會執行networkInterceptor)
  • 自定義方式:實現Interceptor,重寫intercept方法,並註冊攔截器

2. 系統攔截器

  • RetryAndFollowUpInterceptor:進行失敗重試和重定向
  • BridgeInterceptor:新增頭部資訊
  • CacheInterceptor:處理快取
  • ConnectInterceptor:獲取可用的connection例項
  • CallServerInterceptor:發起請求

連線池複用

在ConnectInterceptor中,我們獲取到了connection的例項,該例項是從ConnectionPool中取得

1. Connection

  • Connection 是客戶端和伺服器建立的資料通路,一個Connection上可能存在幾個連結
  • Connection的實現類是RealConnection,是socket物理連線的包裝
  • Connection內部維持著一個List<Reference>引用

2. StreamAllocation

StreamAllocation是Connection維護的連線,以下是類內註解

 <ul>
 *     <li><strong>Connections:</strong> physical socket connections to remote servers. These are
 *         potentially slow to establish so it is necessary to be able to cancel a connection
 *         currently being connected.
 *     <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
 *         connections. Each connection has its own allocation limit, which defines how many
 *         concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
 *         at a time, HTTP/2 typically carry multiple.
 *     <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
 *         its follow up requests. We prefer to keep all streams of a single call on the same
 *         connection for better behavior and locality.
 * </ul>
複製程式碼

3. ConnectionPool

ConnectionPool通過Address等來查詢有沒有可以複用的Connection,同時維護一個執行緒池,對Connection做回收工作

Retrofit


Retrofit幫助我們對OkHttp進行了封裝,使網路請求更加方便

使用

1. 新增依賴

dependencies {
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
  }
複製程式碼

2. 建立Retrofit例項

Retrofit retrofit = new Retrofit.Builder() 
 .baseUrl("http://fanyi.youdao.com/") // 設定網路請求的Url地址
 .addConverterFactory(GsonConverterFactory.create()) // 設定資料解析器 
 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支援RxJava平臺 .build();
複製程式碼

3. 建立網路介面

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
複製程式碼

4. 建立Call

 GetRequest_Interface request = retrofit.create(GetRequest_Interface.class);
//對 傳送請求 進行封裝
Call<Reception> call = request.getCall();
複製程式碼

5. 執行Call的請求方法

//傳送網路請求(非同步) call.enqueue(new Callback<Translation>() { 
//請求成功時回撥
 @Override 
public void onResponse(Call<Translation> call, Response<Translation> response) { 
   //請求處理,輸出結果
    response.body().show(); 
 } 
 //請求失敗時候的回撥 
 @Override 
 public void onFailure(Call<Translation> call, Throwable throwable) { 
     System.out.println("連線失敗"); 
 } 
 });
 
 // 傳送網路請求(同步) Response<Reception> response = call.execute();

複製程式碼

原始碼解析

1. Retrofit

Retrofit 通過builder模式建立,我們可以對其進行各種設定:

  • baseUrl:請求地址的頭部,必填
  • callFactory:網路請求工廠(不進行設定的話預設會生成一個OkHttpClient)
  • adapterFactories:網路請求介面卡工廠的集合,這裡有介面卡因為Retrofit不僅支援Android,還支援Ios等其他平臺(不進行設定的話會根據平臺自動生成)
  • converterFactories:資料轉換器工廠的集合(將網路返回的資料轉換成我們需要的類)
  • callbackExecutor:回撥方法執行器(Android平臺預設通過Handler傳送到主執行緒執行)

2. Call

我們的每個method對應一個Call, Call的建立分為兩步:

  • retorfit.create(myInfterfaceClass.class)建立我們網路請求介面類的例項
  • 呼叫對應方法拿到對應網路請求的Call

關鍵在第一步,第一步是通過動態代理實現的

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, Object... args)
            throws Throwable {
          // If the method is a method from Object then defer to normal invocation.
          if (method.getDeclaringClass() == Object.class) {
            return method.invoke(this, args);
          }
          if (platform.isDefaultMethod(method)) {
            return platform.invokeDefaultMethod(method, service, proxy, args);
          }
          ServiceMethod serviceMethod = loadServiceMethod(method);//1
          OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
          return serviceMethod.callAdapter.adapt(okHttpCall);
        }
      });
}
複製程式碼
  • 通過loadServiceMethod方法生成mehtod對應的ServiceMethod
  • 將ServiceMethod和方法引數傳進OkHttpCall生成OkHttpCall
  • 呼叫callAdapter方法對OkHttpCall進行處理並返回
1. ServiceMethod

loadServiceMethod方法會首先在快取裡查詢是否有該method對應的ServiceMethod,沒有的話呼叫build方法建立一個

ServiceMethod loadServiceMethod(Method method) {
 ServiceMethod result; 
 // 設定執行緒同步鎖 
 synchronized (serviceMethodCache) { 
 result = serviceMethodCache.get(method);
  // ServiceMethod類物件採用了單例模式進行建立 
  // 即建立ServiceMethod物件前,先看serviceMethodCache有沒有快取之前建立過的網路請求例項 
  // 若沒快取,則通過建造者模式建立 
  serviceMethod 物件 if (result == null) { 
  // 下面會詳細介紹ServiceMethod生成例項的過程 
  result = new ServiceMethod.Builder(this, method).build(); 
  serviceMethodCache.put(method, result); 
   } 
  }
   
  return result;
}

複製程式碼

ServiceMethod的建立過程即是對method的解析過程,解析過程包括:對註解的解析,尋找合適的CallAdapter和Convert等

2. OkHttpCall

OkHttpCall實現了Call介面,當執行excute或enqueue請求命令時,內部通過傳入的CallFactory(OkHttpClient)執行網路請求

3. callAdapter

如果我們沒有對CallAdapter進行設定,它的值將是Android平臺的預設設定,其adapt方法如下

public <R> Call<R> adapt(Call<R> call) { 
    return new ExecutorCallbackCall<>(callbackExecutor, call); 
} 


ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {

 this.delegate = delegate; 
 // 把上面建立並配置好引數的OkhttpCall物件交給靜態代理delegate 
 // 靜態代理和動態代理都屬於代理模式 
 // 靜態代理作用:代理執行被代理者的方法,且可在要執行的方法前後加入自己的動作,進行對系統功能的擴充 
 
 this.callbackExecutor = callbackExecutor; 
 // 傳入上面定義的回撥方法執行器 
 // 用於進行執行緒切換 }

複製程式碼

ExecutorCallbackCall對OkHttpCall進行了裝飾,會呼叫CallBackExcutor對OkHttpCall執行的返回結果進行處理,使其位於主執行緒

自定義Convert和CallAdapter

Fresco


Fresco是一個圖片載入庫,可以幫助我們載入圖片顯示,控制多執行緒,以及管理快取和記憶體等

Fresco使用

  1. 引入依賴
dependencies {
  // 其他依賴
  compile 'com.facebook.fresco:fresco:0.12.0'
   // 在 API < 14 上的機器支援 WebP 時,需要新增
  compile 'com.facebook.fresco:animated-base-support:0.12.0'

  // 支援 GIF 動圖,需要新增
  compile 'com.facebook.fresco:animated-gif:0.12.0'

  // 支援 WebP (靜態圖+動圖),需要新增
  compile 'com.facebook.fresco:animated-webp:0.12.0'
  compile 'com.facebook.fresco:webpsupport:0.12.0'

  // 僅支援 WebP 靜態圖,需要新增
  compile 'com.facebook.fresco:webpsupport:0.12.0'
}

複製程式碼
  1. 初始化
Fresco.initialize(Context context);
複製程式碼
  1. 使用SimpleView
<com.facebook.drawee.view.SimpleDraweeView
    android:id="@+id/my_image_view"
    android:layout_width="130dp"
    android:layout_height="130dp"
    fresco:placeholderImage="@drawable/my_drawable"
  />
複製程式碼
  1. 載入圖片
Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

複製程式碼
  1. 以上是Fresco的基本載入流程,此外,我們可以定製載入和顯示的各個環節

Fresco由兩部分組成,Drawees負責圖片的呈現,ImagePipeline負責圖片的下載解碼和記憶體管理

Drawees

Drawees 負責圖片的呈現。它由三個元素組成,有點像MVC模式。

DraweeView

  • 繼承於 View, 負責圖片的顯示。

  • 一般情況下,使用 SimpleDraweeView 即可。 你可以在 XML 或者在 Java 程式碼中使用它,通過 setImageUri 給它設定一個 URI 來使用,這裡有簡單的入門教學:開始使用

  • 你可以使用 XML屬性來達到各式各樣的效果。

DraweeHierarchy

  • DraweeHierarchy 用於組織和維護最終繪製和呈現的 Drawable 物件,相當於MVC中的M。

  • 你可以通過它來在Java程式碼中自定義圖片的展示

DraweeController

  • DraweeController 負責和 image loader 互動( Fresco 中預設為 image pipeline, 當然你也可以指定別的),可以建立一個這個類的例項,來實現對所要顯示的圖片做更多的控制。

  • 如果你還需要對Uri載入到的圖片做一些額外的處理,那麼你會需要這個類的。

DraweeControllerBuilder

  • DraweeControllers 由 DraweeControllerBuilder 採用 Builder 模式建立,建立之後,不可修改。具體參見: 使用ControllerBuilder。

Listeners

  • 使用 ControllerListener 的一個場景就是設定一個 Listener監聽圖片的下載。

ImagePipeline

  • Fresco 的 Image Pipeline 負責圖片的獲取和管理。圖片可以來自遠端伺服器,本地檔案,或者Content Provider,本地資源。壓縮後的檔案快取在本地儲存中,Bitmap資料快取在記憶體中。

  • 在5.0系統以下,Image Pipeline 使用 pinned purgeables 將Bitmap資料避開Java堆記憶體,存在ashmem中。這要求圖片不使用時,要顯式地釋放記憶體

  • SimpleDraweeView自動處理了這個釋放過程,所以沒有特殊情況,儘量使用SimpleDraweeView,在特殊的場合,如果有需要,也可以直接控制Image Pipeline。

  • ImagePipeline載入圖片流程

  1. 檢查記憶體快取,如有,返回
  1. 後臺執行緒開始後續工作
  2. 檢查是否在未解碼記憶體快取中。如有,解碼,變換,返回,然後快取到記憶體快取中。
  3. 檢查是否在磁碟快取中,如果有,變換,返回。快取到未解碼快取和記憶體快取中。
  4. 從網路或者本地載入。載入完成後,解碼,變換,返回。存到各個快取中。

Android 面試開源框架篇

ImagePipeline的執行緒池

Image pipeline 預設有3個執行緒池:

  1. 3個執行緒用於網路下載
  1. 2個執行緒用於磁碟操作: 本地檔案的讀取,磁碟快取操作。
  2. 2個執行緒用於CPU相關的操作: 解碼,轉換,以及後處理等後臺操作。

ImagePipeline的 快取

ImagePipeLine有三級快取

  1. 解碼後的Bitmap快取
  2. 未解碼圖片的記憶體快取
  3. 磁碟快取

對比

功能

Fresco 相對於Glide/Picaso等擁有更多的功能,如圖片的漸進式載入/動圖/圓角等,

效能

Fresco採用三級快取:

  1. 解碼後的Bitmap快取
  2. 未解碼圖片的記憶體快取
  3. 磁碟快取

Glide兩級快取:

  1. 根據ImageView控制元件尺寸獲得對應的大小的bitmap來展示,可以快取原始資料或者resize後資料
  2. 磁碟快取

使用

Fresco通過CloseableReference管理圖片,通過圖片控制元件DraweeView來顯示圖片和控制圖片釋放,雖然擴充套件性高,但是擴充套件起來麻煩;對專案有一定侵入性

EventBus


EventBus使用了觀察者模式,方便我們專案中進行資料傳遞和通訊

使用

  1. 新增依賴
compile 'org.greenrobot:eventbus:3.0.0'
複製程式碼
  1. 註冊和解綁
EventBus.getDefault().register(this);

EventBus.getDefault().unregister(this);
複製程式碼
  1. 新增訂閱訊息方法
@Subscribe(threadMode = ThreadMode.MAIN) 
public void onEvent(MessageEvent event) {
    /* Do something */
}
複製程式碼
  1. 傳送訊息
EventBus.getDefault().post(new MessageEvent("Hello !....."));
    
複製程式碼

@Subscribe註解

該註解內部有三個成員,分別是threadMode、sticky、priority。

  1. threadMode代表訂閱方法所執行的執行緒
  2. sticky代表是否是粘性事件
  3. priority代表優先順序

threadMode

  1. POSTING:表示訂閱方法執行在傳送事件的執行緒。
  2. MAIN:表示訂閱方法執行在UI執行緒,由於UI執行緒不能阻塞,因此當使用MAIN的時候,訂閱方法不應該耗時過長。
  3. BACKGROUND:表示訂閱方法執行在後臺執行緒,如果傳送的事件執行緒不是UI執行緒,那麼就使用該執行緒;如果傳送事件的執行緒是UI執行緒,那麼新建一個後臺執行緒來呼叫訂閱方法。
  4. ASYNC:訂閱方法與傳送事件始終不在同一個執行緒,即訂閱方法始終會使用新的執行緒來執行。

sticky 粘性事件

在註冊之前便把事件發生出去,等到註冊之後便會收到最近傳送的粘性事件(必須匹配)。注意:只會接收到最近傳送的一次粘性事件,之前的會接受不到,demo

原始碼解析

參見連結

效能

  1. EventBus通過反射的方式對@Subscribe方法進行解析。
  2. 預設情況下,解析是執行時進行的,但是我們也可以通過設定和載入依賴庫,使其編譯時形成索引,其效能會大大提升

相關文章