主頁 > 軟體設計 > Android面試題到處攢,一到面試就不知深淺忘個干凈?快來看看這份超詳細整理!

Android面試題到處攢,一到面試就不知深淺忘個干凈?快來看看這份超詳細整理!

2020-12-22 12:53:47 軟體設計

穩住,今天周一,長文面試題奉上,
更多BAT面試決議資料包內容查看

1、說說View/ViewGroup的繪制流程

View的繪制流程是從ViewRoot的performTraversals開始的,它經過measure,layout,draw三個程序最終將View繪制出來,performTraversals會依次呼叫performMeasure,performLayout,performDraw三個方法,他們會依次呼叫measure,layout,draw方法,然后又呼叫了onMeasure,onLayout,dispatchDraw,

measure :對于自定義的單一view的測量,只需要根據父 view 傳遞的MeasureSpec進行計算大小,

對于ViewGroup的測量,一般要重寫onMeasure方法,在onMeasure方法中,父容器會對所有的子View進行Measure,子元素又會作為父容器,重復對它自己的子元素進行Measure,這樣Measure程序就從DecorView一級一級傳遞下去了,也就是要遍歷所有子View的的尺寸,最終得出出總的viewGroup的尺寸,Layout和Draw方法也是如此,

layout :根據 measure 子 View 所得到的布局大小和布局引數,將子View放在合適的位置上,

對于自定義的單一view,計算本身的位置即可,

對于ViewGroup來說,需要重寫onlayout方法,除了計算自己View的位置,還需要確定每一個子View在父容器的位置以及子view的寬高(getMeasuredWidth和getMeasuredHeight),最后呼叫所有子view的layout方法來設定子view的位置,

draw :把 View 物件繪制到螢屏上,

draw()會依次呼叫四個方法:

1)drawBackground(),根據在 layout 程序中獲取的 View 的位置引數,來設定背景的邊界,2)onDraw(),繪制View本身的內容,一般自定義單一view會重寫這個方法,實作一些繪制邏輯,

3) dispatchDraw(),繪制子View 4)onDrawScrollBars(canvas),繪制裝飾,如 滾動指示器、滾動條、和前景.

2、說說你理解的MeasureSpec

MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過簡單的計算得出一個針對子View的測量要求,這個測量要求就是MeasureSpec,

首先,MeasureSpec是一個大小跟模式的組合值,MeasureSpec中的值是一個整型(32位)將size和mode打包成一個Int型,其中高兩位是mode,后面30位存的是size

// 獲取測量模式
  int specMode = MeasureSpec.getMode(measureSpec)

  // 獲取測量大小
  int specSize = MeasureSpec.getSize(measureSpec)

  // 通過Mode 和 Size 生成新的SpecMode
  int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

其次,每個子View的MeasureSpec值根據子View的布局引數和父容器的MeasureSpec值計算得來的,所以就有一個父布局測量模式,子視圖布局引數,以及子view本身的MeasureSpec關系圖:

其實也就是getChildMeasureSpec方法的原始碼邏輯,會根據子View的布局引數和父容器的MeasureSpec計算出來單個子view的MeasureSpec,

最后是實際應用時:

對于自定義的單一view,一般可以不處理onMeasure方法,如果要對寬高進行自定義,就重寫onMeasure方法,并將算好的寬高通過setMeasuredDimension方法傳進去,對于自定義的ViewGroup,一般需要重寫onMeasure方法,并且呼叫measureChildren方法遍歷所有子View并進行測量(measureChild方法是測量具體某一個view的寬高),然后可以通過getMeasuredWidth/getMeasuredHeight獲取寬高,最后通過setMeasuredDimension方法存盤本身的總寬高,

3、Scroller是怎么實作View的彈性滑動?

  • 在MotionEvent.ACTION_UP事件觸發時呼叫startScroll()方法,該方法并沒有進行實際的滑動操作,而是記錄滑動相關量(滑動距離、滑動時間)
  • 接著呼叫invalidate/postInvalidate()方法,請求View重繪,導致View.draw方法被執行
  • 當View重繪后會在draw方法中呼叫computeScroll方法,而computeScroll又會去向Scroller獲取當前的scrollX和scrollY;然后通過scrollTo方法實作滑動;接著又呼叫postInvalidate方法來進行第二次重繪,和之前流程一樣,如此反復導致View不斷進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,直到整個滑動過成結束,
mScroller = new Scroller(context);

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            // 滾動開始時X的坐標,滾動開始時Y的坐標,橫向滾動的距離,縱向滾動的距離
            mScroller.startScroll(getScrollX(), 0, dx, 0);
            invalidate();
            break;
    }
    return super.onTouchEvent(event);
}

@Override
public void computeScroll() {
    // 重寫computeScroll()方法,并在其內部完成平滑滾動的邏輯
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate();
    }
}

4、OKHttp有哪些攔截器,分別起什么作用

OKHTTP的攔截器是把所有的攔截器放到一個list里,然后每次依次執行攔截器,并且在每個攔截器分成三部分:

  • 預處理攔截器內容
  • 通過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);
    return chain.proceed(originalRequest);
}

根據原始碼可知,一共七個攔截器:

  • addInterceptor(Interceptor),這是由開發者設定的,會按照開發者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共引數,Header都可以在這里添加,
  • RetryAndFollowUpInterceptor,這里會對連接做一些初始化作業,以及請求失敗的充實作業,重定向的后續請求作業,跟他的名字一樣,就是做重試作業還有一些連接跟蹤作業,
  • BridgeInterceptor,這里會為用戶構建一個能夠進行網路訪問的請求,同時后續作業將網路請求回來的回應Response轉化為用戶可用的Response,比如添加檔案型別,content-length計算添加,gzip解包,
  • CacheInterceptor,這里主要是處理cache相關處理,會根據OkHttpClient物件的配置以及快取策略對請求值進行快取,而且如果本地有了可?的Cache,就可以在沒有網路互動的情況下就回傳快取結果,
  • ConnectInterceptor,這里主要就是負責建立連接了,會建立TCP連接或者TLS連接,以及負責編碼解碼的HttpCodec
  • networkInterceptors,這里也是開發者自己設定的,所以本質上和第一個攔截器差不多,但是由于位置不同,所以用處也不同,這個位置添加的攔截器可以看到請求和回應的資料了,所以可以做一些網路除錯,
  • CallServerInterceptor,這里就是進行網路資料的請求和回應了,也就是實際的網路I/O操作,通過socket讀寫資料,

5、OkHttp怎么實作連接池

為什么需要連接池?

頻繁的進行建立Sokcet連接和斷開Socket是非常消耗網路資源和浪費時間的,所以HTTP中的keepalive連接對于降低延遲和提升速度有非常重要的作用,

keepalive機制是什么呢?

也就是可以在一次TCP連接中可以持續發送多份資料而不會斷開連接,所以連接的多次使用,也就是復用就變得格外重要了,而復用連接就需要對連接進行管理,于是就有了連接池的概念,

OkHttp中使用ConectionPool實作連接池,默認支持5個并發KeepAlive,默認鏈路生命為5分鐘,

怎么實作的?

1)首先,ConectionPool中維護了一個雙端佇列Deque,也就是兩端都可以進出的佇列,用來存盤連接,

2)然后在ConnectInterceptor,也就是負責建立連接的攔截器中,首先會找可用連接,也就是從連接池中去獲取連接,具體的就是會呼叫到ConectionPool的get方法,

RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

也就是遍歷了雙端佇列,如果連接有效,就會呼叫acquire方法計數并回傳這個連接,

3)如果沒找到可用連接,就會創建新連接,并會把這個建立的連接加入到雙端佇列中,同時開始運行執行緒池中的執行緒,其實就是呼叫了ConectionPool的put方法,

public final class ConnectionPool {
    void put(RealConnection connection) {
        if (!cleanupRunning) {
         //沒有連接的時候呼叫
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
}

4)其實這個執行緒池中只有一個執行緒,是用來清理連接的,也就是上述的cleanupRunnable

private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                //執行清理,并回傳下次需要清理的時間,
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        //在timeout時間內釋放鎖
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

這個runnable會不停的呼叫cleanup方法清理執行緒池,并回傳下一次清理的時間間隔,然后進入wait等待,

怎么清理的呢?看看原始碼:

long cleanup(long now) {
    synchronized (this) {
      //遍歷連接
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //檢查連接是否是空閑狀態,
        //不是,則inUseConnectionCount + 1
        //是 ,則idleConnectionCount + 1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      //如果超過keepAliveDurationNs或maxIdleConnections,
      //從雙端佇列connections中移除
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {      
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {      //如果空閑連接次數>0,回傳將要到期的時間
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 連接依然在使用中,回傳保持連接的周期5分鐘
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

也就是當如果空閑連接maxIdleConnections超過5個或者keepalive時間大于5分鐘,則將該連接清理掉,

5)這里有個問題,怎樣屬于空閑連接?

其實就是有關剛才說到的一個方法acquire計數方法:

public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

在RealConnection中,有一個StreamAllocation虛參考串列allocations,每創建一個連接,就會把連接對應的StreamAllocationReference添加進該串列中,如果連接關閉以后就將該物件移除,

6)連接池的作業就這么多,并不負責,主要就是管理雙端佇列Deque<RealConnection>,可以用的連接就直接用,然后定期清理連接,同時通過對StreamAllocation的參考計數實作自動回收,

6、OkHttp里面用到了什么設計模式

責任鏈模式

這個不要太明顯,可以說是okhttp的精髓所在了,主要體現就是攔截器的使用,具體代碼可以看看上述的攔截器介紹,

建造者模式

在Okhttp中,建造者模式也是用的挺多的,主要用處是將物件的創建與表示相分離,用Builder組裝各項配置,比如Request:

public class Request {
  public static class Builder {
    @Nullable HttpUrl url;
    String method;
    Headers.Builder headers;
    @Nullable RequestBody body;
    public Request build() {
      return new Request(this);
    }
  }
}

工廠模式

工廠模式和建造者模式類似,區別就在于工廠模式側重點在于物件的生成程序,而建造者模式主要是側重物件的各個引數配置,例子有CacheInterceptor攔截器中又個CacheStrategy物件:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;

  if (cacheResponse != null) {
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
    Headers headers = cacheResponse.headers();
    for (int i = 0, size = headers.size(); i < size; i++) {
      String fieldName = headers.name(i);
      String value = headers.value(i);
      if ("Date".equalsIgnoreCase(fieldName)) {
        servedDate = HttpDate.parse(value);
        servedDateString = value;
      } else if ("Expires".equalsIgnoreCase(fieldName)) {
        expires = HttpDate.parse(value);
      } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
        lastModified = HttpDate.parse(value);
        lastModifiedString = value;
      } else if ("ETag".equalsIgnoreCase(fieldName)) {
        etag = value;
      } else if ("Age".equalsIgnoreCase(fieldName)) {
        ageSeconds = HttpHeaders.parseSeconds(value, -1);
      }
    }
  }
}

觀察者模式

由于webSocket屬于長連接,所以需要進行監聽,這里是用到了觀察者模式:

final WebSocketListener listener;
@Override public void onReadMessage(String text) throws IOException {
    listener.onMessage(this, text);
}

另外有的博客還說到了策略模式,門面模式等,這些大家可以網上搜搜,畢竟每個人的想法看法都會不同,細心找找可能就會發現,

7、介紹一下你們之前做的專案的架構

這個問題大家就真實回答就好,重點是要說完后提出對自己專案架構的認同或不認同的觀點,也就是要有自己的思考和想法,

MVP,MVVM,MVC 區別

MVC

架構介紹

Model:資料模型,比如我們從資料庫或者網路獲取資料View:視圖,也就是我們的xml布局檔案Controller:控制器,也就是我們的Activity

模型聯系

View --> Controller,也就是反應View的一些用戶事件(點擊觸摸事件)到Activity上,Controller --> Model, 也就是Activity去讀寫一些我們需要的資料,Controller --> View, 也就是Activity在獲取資料之后,將更新內容反映到View上,

這樣一個完整的專案架構就出來了,也是我們早期進行開發比較常用的專案架構,

優缺點

這種缺點還是比較明顯的,主要表現就是我們的Activity太重了,經常一寫就是幾百上千行了,造成這種問題的原因就是Controller層和View層的關系太過緊密,也就是Activity中有太多操作View的代碼了,

但是!但是!其實Android這種并稱不上傳統的MVC結構,因為Activity又可以叫View層又可以叫Controller層,所以我覺得這種Android默認的開發結構,其實稱不上什么MVC專案架構,因為他本身就是Android一開始默認的開發形式,所有東西都往Activity中丟,然后能封裝的封裝一下,根本分不出來這些層級,當然這是我個人看法,可以都來討論下,

MVP

架構介紹

之前不就是因為Activity中有操作view,又做Controller作業嗎,所以其實MVP架構就是從原來的Activity層把view和Controller區分開,單獨抽出來一層Presenter作為原來Controller的職位,然后最后演化成,將View層寫成介面的形式,然后Activity去實作View介面,最后在Presenter類中去實作方法,

Model:資料模型,比如我們從資料庫或者網路獲取資料,View:視圖,也就是我們的xml布局檔案和Activity,Presenter:主持人,單獨的類,只做調度作業,

模型聯系

View --> Presenter,反應View的一些用戶事件到Presenter上,Presenter --> Model, Presenter去讀寫操作一些我們需要的資料,Controller --> View, Presenter在獲取資料之后,將更新內容反饋給Activity,進行view更新,

優缺點

這種的優點就是確實大大減少了Activity的負擔,讓Activity主要承擔一個更新View的作業,然后把跟Model互動的作業轉移給了Presenter,從而由Presenter方來控制和互動Model方以及View方,所以讓專案更加明確簡單,順序性思維開發,

缺點也很明顯:首先就是代碼量大大增加了,每個頁面或者說功能點,都要專門寫一個Presenter類,并且由于是面向介面編程,需要增加大量介面,會有大量繁瑣的回呼,其次,由于Presenter里持有了Activity物件,所以可能會導致記憶體泄漏或者view空指標,這也是需要注意的地方,

MVVM

架構介紹

MVVM的特點就是雙向系結,并且有Google官方加持,更新了Jetpack中很多架構組件,比如ViewModel,Livedata,DataBinding等等,所以這個是現在的主流框架和官方推崇的框架,

Model:資料模型,比如我們從資料庫或者網路獲取資料,View:視圖,也就是我們的xml布局檔案和Activity,ViewModel:關聯層,將Model和View系結,使他們之間可以相互系結實時更新

模型聯系

View --> ViewModel -->View,雙向系結,資料改動可以反映到界面,界面的修改可以反映到資料,ViewModel --> Model, 操作一些我們需要的資料,

優缺點

優點就是官方大力支持,所以也更新了很多相關庫,讓MVVM架構更強更好用,而且雙向系結的特點可以讓我們省去很多View和Model的互動,也基本解決了上面兩個架構的問題,

8、具體說說你理解的MVVM

1)先說說MVVM是怎么解決了其他兩個架構所在的缺陷和問題:

  • 解決了各個層級之間耦合度太高的問題,也就是更好的完成了解耦,MVP層中,Presenter還是會持有View的參考,但是在MVVM中,View和Model進行雙向系結,從而使viewModel基本只需要處理業務邏輯,無需關系界面相關的元素了,

  • 解決了代碼量太多,或者模式化代碼太多的問題,由于雙向系結,所以UI相關的代碼就少了很多,這也是代碼量少的關鍵,而這其中起到比較關鍵的組件就是DataBinding,使所有的UI變動都交給了被觀察的資料模型,

  • 解決了可能會有的記憶體泄漏問題,MVVM架構組件中有一個組件是LiveData,它具有生命周期感知能力,可以感知到Activity等的生命周期,所以就可以在其關聯的生命周期遭到銷毀后自行清理,就大大減少了記憶體泄漏問題,

  • 解決了因為Activity停止而導致的View空指標問題,在MVVM中使用了LiveData,那么在需要更新View的時候,如果觀察者的生命周期處于非活躍狀態(如回傳堆疊中的 Activity),則它不會接收任何 LiveData 事件,也就是他會保證在界面可見的時候才會進行回應,這樣就解決了空指標問題,

  • 解決了生命周期管理問題,這主要得益于Lifecycle組件,它使得一些控制元件可以對生命周期進行觀察,就能隨時隨地進行生命周期事件,

2)再說說回應式編程

回應式編程,說白了就是我先構建好事物之間的關系,然后就可以不用管了,他們之間會因為這層關系而互相驅動,其實也就是我們常說的觀察者模式,或者說訂閱發布模式,

為什么說這個呢,因為MVVM的本質思想就是類似這種,不管是雙向系結,還是生命周期感知,其實都是一種觀察者模式,使所有事物變得可觀察,那么我們只需要把這種觀察關系給穩定住,那么專案也就穩健了,

3)最后再說說MVVM為什么這么強大?

我個人覺得,MVVM強大不是因為這個架構本身,而是因為這種回應式編程的優勢比較大,再加上Google官方的大力支持,出了這么多支持的組件,來維系MVVM架構,其實也是官方想進行專案架構的統一,

優秀的架構思想+官方支持=強大

9、ViewModel 是什么,說說你所理解的ViewModel?

ViewModel是MVVM架構的一個層級,用來聯系View和model之間的關系,而我們今天要說的就是官方出的一個框架——ViewModel,

ViewModel 類旨在以注重生命周期的方式存盤和管理界面相關的資料

官方是這么介紹的,這里面有兩個資訊:

注重生命周期的方式,由于ViewModel的生命周期是作用于整個Activity的,所以就節省了一些關于狀態維護的作業,最明顯的就是對于螢屏旋轉這種情況,以前對資料進行保存讀取,而ViewModel則不需要,他可以自動保留資料,

其次,由于ViewModel在生命周期內會保持區域單例,所以可以更方便Activity的多個Fragment之間通信,因為他們能獲取到同一個ViewModel實體,也就是資料狀態可以共享了,

存盤和管理界面相關的資料,

ViewModel層的根本職責,就是負責維護界面上UI的狀態,其實就是維護對應的資料,因為資料會最終體現到UI界面上,所以ViewModel層其實就是對界面相關的資料進行管理,存盤等操作,

ViewModel 為什么被設計出來,解決了什么問題?

在ViewModel組件被設計出來之前,MVVM又是怎么實作ViewModel這一層級的呢?

其實就是自己撰寫類,然后通過介面,內部依賴實作View和資料的雙向系結,所以Google出這個ViewModel組件,無非就是為了規范MVVM架構的實作,并盡量讓ViewModel這一層級只觸及到業務代碼,不去關心VIew層級的參考等,然后配合其他的組件,包括livedata,databindingrang等讓MVVM架構更加完善,規范,健碩,

解決了什么問題呢?

其實上面已經說過一些了,比如:

1)不會因為螢屏旋轉而銷毀,減少了維護狀態的作業 2)由于在作用域內單一實體的特性,使得多個fragment之間可以方便通信,并且維護同一個資料狀態,3)完善了MVVM架構,使得解耦更加純粹,

10、說說ViewModel原理,

首先說說是怎么保存生命周期,

ViewModel2.0之前呢,其實原理是在Activity上add一個HolderFragment,然后設定setRetainInstance(true)方法就能讓這個Fragment在Activity重建時存活下來,也就保證了ViewModel的狀態不會隨Activity的狀態所改變,

2.0之后,其實是用到了Activity的onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()這兩個方法,相當于在橫豎屏切的時候會保存ViewModel的實體,然后恢復,所以也就保證了ViewModel的資料,

再說說怎么保證作用域內唯一實體

首先,ViewModel的實體是通過反射獲取的,反射的時候帶上application的背景關系,這樣就保證了不會持有Activity或者Fragment等View的參考,然后實體創建出來會保存到一個ViewModelStore容器里面,其實也就是一個集合類,這個ViewModelStore 類其實就是保存在界面上的那個實體,而我們的ViewModel就是里面的一個集合類的子元素,

所以我們每次獲取的時候,首先看看這個集合里面有無我們的ViewModel,如果沒有就去實體化,如果有就直接拿到實體使用,這樣就保證了唯一實體,最后在界面銷毀的時候,會去執行ViewModelStore的clear方法,去清除集合里面的ViewModel資料,一小段代碼說明下:

public <T extends ViewModel> T get(Class<T> modelClass) {
      // 先從ViewModelStore容器中去找是否存在ViewModel的實體
      ViewModel viewModel = mViewModelStore.get(key);

      // 若ViewModel已經存在,就直接回傳
      if (modelClass.isInstance(viewModel)) {
            return (T) viewModel;
      }

      // 若不存在,再通過反射的方式實體化ViewModel,并存盤進ViewModelStore
      viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
      mViewModelStore.put(key, viewModel);
      return (T) viewModel;
 }


public class ViewModelStore {
    private final HashMap<String, ViewModel> mMap = new HashMap<>();

     public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}



 @Override
protected void onDestroy() {
    super.onDestroy();

   if (mViewModelStore != null && !isChangingConfigurations()) {
        mViewModelStore.clear();
    }

}

11、ViewModel怎么實作自動處理生命周期?

為什么在旋轉螢屏后不會丟失狀態?為什么ViewModel可以跟隨Activity/Fragment的生命周期而又不會造成記憶體泄漏呢?

這三個問題很類似,都是關于生命周期的問題,其實也就是問為什么ViewModel能管理生命周期,并且不會因為重建等情況造成影響,

ViewModel2.0之前

利用一個無view 的HolderFragment來維持它的生命周期,我們知道ViewModel實體是存盤到一個ViewModelStore容器里的,那么這個空的fragment就可以用來管理這個容器,只要Activity處于活動狀態,HolderFragment也就不會被銷毀,就保證了ViewModel的生命周期,

而且設定setRetainInstance(true)方法可以保證configchange時的生命周期不被改變,讓這個Fragment在Activity重建時存活下來,

總結來說就是用一個空的fragment來管理維護ViewModelStore,然后對應的activity銷毀的時候就去把viewmodel的映射洗掉,就讓ViewModel的生命周期保持和Activity一樣了,這也是很多三方庫用到的巧妙方法,比如Glide,也是建立空的Fragment來管理,

2.0之后,有了androidx支持

其實是用到了Activity的一個子類ComponentActivity,然后重寫了onRetainNonConfigurationInstance()方法保存ViewModelStore,并在需要的時候,也就是重建的Activity中去通過getLastNonConfigurationInstance()方法獲取到ViewModelStore實體,這樣也就保證了ViewModelStore中的ViewModel不會隨Activity的重建而改變,

同時由于實作了LifecycleOwner介面,所以能利用Lifecycles組件組件感知每個頁面的生命周期,就可以通過它來訂閱當Activity銷毀時,且不是因為配置導致的destory情況下,去清除ViewModel,也就是呼叫ViewModelStore的clear方法,

getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                // 判斷是否因為配置更改導致的destroy
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
            }
        }
    });

這里的onRetainNonConfigurationInstance方法再說下,是會在Activity因為配置改變而被銷毀時被呼叫,跟onSaveInstanceState方法呼叫時機比較相像,不同的是onSaveInstanceState保存的是Bundle,Bundle是有型別限制和大小限制的,而且需要在主執行緒進行序列號,而onRetainNonConfigurationInstance方法都沒有限制,所以更傾向于用它,

所以,到這里,第三個問題應該也可以回答了,2.0之前呢,都是通過他們創建了一個空的fragment,然后跟隨這個fragment的生命周期,

2.0之后呢,是因為不管是Activity或者Fragment,都實作了LifecycleOwner介面,所以ViewModel是可以通過Lifecycles感知到他們的生命周期,從而進行實體管理的,

12、LiveData 是什么?

LiveData 是一種可觀察的資料存盤器類,與常規的可觀察類不同,LiveData 具有生命周期感知能力,意指它遵循其他應用組件(如 Activity、Fragment 或 Service)的生命周期,這種感知能力可確保 LiveData 僅更新處于活躍生命周期狀態的應用組件觀察者,

官方介紹如下,其實說的比較清楚了,主要作用在兩點:

  • 資料存盤器類,也就是一個用來存盤資料的類,

  • 可觀察,這個資料存盤類是可以觀察的,也就是比一般的資料存盤類多了這么一個功能,對于資料的變動能進行回應,

主要思想就是用到了觀察者模式思想,讓觀察者和被觀察者解耦,同時還能感知到資料的變化,所以一般被用到ViewModel中,ViewModel負責觸發資料的更新,更新會通知到LiveData,然后LiveData再通知活躍狀態的觀察者,

var liveData = MutableLiveData<String>()

liveData.observe(this, object : Observer<String> {
    override fun onChanged(t: String?) {
    }
})

liveData.setVaile("xixi")
//子執行緒呼叫
liveData.postValue("test")

13、LiveData 為什么被設計出來,解決了什么問題?

LiveData作為一種觀察者模式設計思想,常常被和Rxjava一起比較,觀察者模式的最大好處就是事件發射的上游 和 接收事件的下游 互不干涉,大幅降低了互相持有的依賴關系所帶來的強耦合性,

其次,LiveData還能無縫銜接到MVVM架構中,主要體現在其可以感知到Activity等生命周期,這樣就帶來了很多好處:

  • 不會發生記憶體泄漏 觀察者會系結到 Lifecycle物件,并在其關聯的生命周期遭到銷毀后進行自我清理,

  • 不會因 Activity 停止而導致崩潰 如果觀察者的生命周期處于非活躍狀態(如回傳堆疊中的 Activity),則它不會接收任何 LiveData 事件,

  • 自動判斷生命周期并回呼方法 如果觀察者的生命周期處于 STARTED 或 RESUMED狀態,則 LiveData 會認為該觀察者處于活躍狀態,就會呼叫onActive方法,否則,如果 LiveData 物件沒有任何活躍觀察者時,會呼叫 onInactive()方法,

14、說說LiveData原理

說到原理,其實就是兩個方法:

訂閱方法,也就是observe方法,通過該方法把訂閱者和被觀察者關聯起來,形成觀察者模式,

簡單看看原始碼:

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    //...
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    owner.getLifecycle().addObserver(wrapper);
}

  public V putIfAbsent(@NonNull K key, @NonNull V v) {
    Entry<K, V> entry = get(key);
    if (entry != null) {
        return entry.mValue;
    }
    put(key, v);
    return null;
}

這里putIfAbsent方法是講生命周期相關的wrapper和觀察者observer作為key和value存到了mObservers中,

回呼方法,也就是onChanged方法,通過改變存盤值,來通知到觀察者也就是呼叫onChanged方法,從改變存盤值方法setValue看起:

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}


private void dispatchingValue(@Nullable ObserverWrapper initiator) {
    //...
    do {
        mDispatchInvalidated = false;

        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}


private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
    //
    // we still first check observer.active to keep it as the entrance for events. So even if
    // the observer moved to an active state, if we've not received that event, we better not
    // notify for a more predictable notification order.
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    //noinspection unchecked
    observer.mObserver.onChanged((T) mData);
}

這一套下來邏輯還是比較簡單的,遍歷剛才的map——mObservers,然后找到觀察者observer,如果觀察者不在活躍狀態(活躍狀態,也就是可見狀態,處于 STARTED 或 RESUMED狀態),則直接回傳,不去通知,否則正常通知到觀察者的onChanged方法,

當然,如果想任何時候都能監聽到,都能獲取回呼,呼叫observeForever方法即可,

15、說說DNS,以及存在的問題

DNS用來做域名決議作業的,當輸入一個域名后,需要把域名轉化為IP地址,這個轉換程序就是DNS決議,

但是傳統的DSN決議會有一些問題,比如:

  • 域名快取問題本地做一個快取,直接回傳快取資料,可能會導致全域負載均衡失敗,因為上次進行的快取,不一定是這次離客戶最近的地方,可能會繞遠路,

  • 域名轉發問題如果是A運營商將決議的請求轉發給B運營商,B去權威DNS服務器查詢的話,權威服務器會認為你是B運營商的,就回傳了B運營商的網站地址,結果每次都會跨運營商,

  • 出口NAT問題做了網路地址轉化后,權威的DNS服務器,沒法通過地址來判斷客戶到底是哪個運營商,極有可能誤判運營商,導致跨運營商訪問,

  • 域名更新問題本地DNS服務器是由不同地區,不同運營商獨立部署的,對域名決議快取的處理上,有區別,有的會偷懶忽略決議結果TTL的時間限制,導致服務器沒有更新新的ip而是指向舊的ip,

  • 決議延遲DNS的查詢程序需要遞回遍歷多個DNS服務器,才能獲得最終結果,可能會帶來一定的延時,

  • 域名劫持DNS域名決議服務器有可能會被劫持,或者被偽造,那么正常的訪問就會被決議到錯誤的地址,

  • 不可靠由于DNS決議是運行在UDP協議之上的,而UDP我之前也說過是一種不可靠的協議,他的優勢在于實時性,但是有丟包的可能,

這些問題不僅會讓訪問速度變慢,還有可能會導致訪問例外,訪問頁面被替換等等,

16、怎么優化DNS決議

安全優化

總之DNS還是會有各種問題吧,怎么解決呢?就是用HTTPDNS,

HTTPDNS是一個新概念,他會繞過傳統的運營商DNS服務器,不走傳統的DNS決議,而是換成HTTP協議,直接通過HTTP協議進行請求某個DNS服務器集群,獲取地址,

  • 由于繞過了運營商,所以可以避免域名被劫持,
  • 它是基于訪問的來源ip,所以能獲得更準確的決議結果
  • 會有預決議,決議快取等功能,所以決議延遲也很小

所以首先的優化,針對安全方面,就是要替換成HTTPDNS決議方式,就要借用阿里云和騰訊云等服務,但是這些服務可不是免費的,有沒有免費的呢?有的,七牛云的 happy-dns,添加依賴庫,然后去實作okhttp的DNS介面即可,簡單寫個例子:

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}


private void dispatchingValue(@Nullable ObserverWrapper initiator) {
    //...
    do {
        mDispatchInvalidated = false;

        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}


private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
    //
    // we still first check observer.active to keep it as the entrance for events. So even if
    // the observer moved to an active state, if we've not received that event, we better not
    // notify for a more predictable notification order.
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    //noinspection unchecked
    observer.mObserver.onChanged((T) mData);
}

速度優化

如果在測驗環境,其實我們可以直接配置ip白名單,然后跳過DNS決議流程,直接獲取ip地址,比如:

private static class TestDNS implements Dns{
    @Override
    public List<InetAddress> lookup(@NotNull String hostname) throws UnknownHostException {
        if ("www.test.com".equalsIgnoreCase(hostname)){
            InetAddress byAddress=InetAddress.getByAddress(hostname,new byte[]{(byte)192,(byte)168,1,1});
            return Collections.singletonList(byAddress);
        }else {
            return Dns.SYSTEM.lookup(hostname);
        }
    }
}

17、DNS決議超時怎么辦

當我們在用OKHttp做網路請求時,如果網路設備切換路由,訪問網路出現長時間無回應,很久之后會拋出 UnknownHostException,雖然我們在OkHttp中設定了connectTimeout超時時間,但是它其實對DNS的決議是不起作用的,

這種情況我們就需要在自定義的Dns類中做超時判斷:

public class TimeDns implements Dns {
    private long timeout;

    public TimeDns(long timeout) {
        this.timeout = timeout;
    }

    @Override
    public List<InetAddress> lookup(final String hostname) throws UnknownHostException {
        if (hostname == null) {
            throw new UnknownHostException("hostname == null");
        } else {
            try {
                FutureTask<List<InetAddress>> task = new FutureTask<>(
                        new Callable<List<InetAddress>>() {
                            @Override
                            public List<InetAddress> call() throws Exception {
                                return Arrays.asList(InetAddress.getAllByName(hostname));
                            }
                        });
                new Thread(task).start();
                return task.get(timeout, TimeUnit.MILLISECONDS);
            } catch (Exception var4) {
                UnknownHostException unknownHostException =
                        new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
                unknownHostException.initCause(var4);
                throw unknownHostException;
            }
        }
    }
}

//替換okhttp的dns決議
OkHttpClient okHttpClient = new OkHttpClient.Builder().dns(new TimeDns(5000)).build();

18、注解是什么?有哪些元注解

注解,在我看來它是一種資訊描述,不影響代碼執行,但是可以用來配置一些代碼或者功能,

常見的注解比如@Override,代表重寫方法,看看它是怎么生成的:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以看到Override被@interface所修飾,代表注解,同時上方還有兩個注解@Target和@Retention,這種修飾注解的注解叫做元注解,很好理解吧,就是最基本的注解唄,java中一共有四個元注解:

  • @Target:表示注解物件的作用范圍,
  • @Retention:表示注解保留的生命周期
  • @Inherited:表示注解型別能被類自動繼承,
  • @Documented:表示含有該注解型別的元素(帶有注釋的)會通過javadoc或類似工具進行檔案化,

具體說下這幾個元注解都是怎么用的

  • @Target

target,表示注解物件的作用范圍,比如Override注解所標示的就是ElementType.METHOD,即所作用的范圍是方法范圍,也就是只能在方法頭上加這個注解,另外還有以下幾個修飾范圍引數:

  • TYPE:類、介面、列舉、注解型別,
  • FIELD:類成員(構造方法、方法、成員變數),
  • METHOD:方法,
  • PARAMETER:引數,
  • CONSTRUCTOR:構造器,
  • LOCAL_VARIABLE:區域變數,
  • ANNOTATION_TYPE:注解,
  • PACKAGE:包宣告,
  • TYPE_PARAMETER:型別引數,
  • TYPE_USE:型別使用宣告,

比如ANNOTATION_TYPE就是表示該注解的作用范圍就是注解,哈哈,有點繞吧,看看Target注解的代碼:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

帶了一個ElementType型別的引數,也就是上面說到的作用范圍引數,另外還被Target注解修飾了,傳的引數就是ANNOTATION_TYPE,也就是我注解我自己,我設定我自己的作用范圍是注解,大家自己繞一下,,

  • @Retention

表示注解保留的生命周期,或者說表示該注解所保留的時長,主要有以下幾個可選引數:

  • SOURCE:僅存在Java源檔案,經過編譯器后便丟棄相應的注解,適用于一些檢查性的操作,比如@Override,

  • CLASS:編譯class檔案時生效,存在Java源檔案,以及經編譯器后生成的Class位元組碼檔案,但在運行時VM不再保留注釋,這個也是默認的引數,適用于在編譯時進行一些預處理操作,比如ButterKnife的@BindView,可以在編譯時生成一些輔助的代碼或者完成一些功能,

  • RUNTIME:存在源檔案、編譯生成的Class位元組碼檔案,以及保留在運行時VM中,可通過反射性地讀取注解,適用于一些需要運行時動態獲取注解資訊,類似反射獲取注解等,

  • @Inherited

表示注解型別能被類自動繼承,這里需要注意兩點:

  • 類,也就是說只有在類集成關系中,子類才會集成父類使用的注解中被@Inherited所修飾的那個注解,其他的介面集成關系,類實作介面關系中,都不會存在自動繼承注解,

  • 自動繼承,也就是說如果父類有@Inherited所修飾的那個注解,那么子類不需要去寫這個注解,就會自動有了這個注解,

還是看個例子:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface MyInheritedAnnotation {
 //注解1,有Inherited注解修飾
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
 //注解2,沒有Inherited注解修飾
}


@MyInheritedAnnotation
@MyAnnotation
public class BaseClass {
 //父類,有以上兩個注解
}

public class ExtendClass extends BaseClass  {

 //子類會繼承父類的MyInheritedAnnotation注解,
 //而不會繼承MyAnnotation注解
}
  • @Documented

表示擁有該注解的元素可通過javadoc此類的工具進行檔案化,也就是說生成JavaAPI檔案的時候會被寫進檔案中,

注解可以用來做什么

主要有以下幾個用處:

  • 降低專案的耦合度,
  • 自動完成一些規律性的代碼,
  • 自動生成java代碼,減輕開發者的作業量,

19、Activity從創建到我們看到界面,發生了哪些事

首先是通過setContentView加載布局,這其中創建了一個DecorView,然后根據然后根據activity設定的主題(theme)或者特征(Feature)加載不同的根布局檔案,最后再通過inflate方法加載layoutResID資源檔案,其實就是決議了xml檔案,根據節點生成了View物件,流程圖:

其次就是進行view繪制到界面上,這個程序發生在handleResumeActivity方法中,也就是觸發onResume的方法,在這里會創建一個ViewRootImpl物件,作為DecorView的parent然后對DecorView進行測量布局和繪制三大流程,流程圖:

20、Activity、PhoneWindow、DecorView、ViewRootImpl 的關系?

  • PhoneWindow是Window 的唯一子類,每個Activity都會創建一個PhoneWindow物件,你可以理解它為一個視窗,但不是真正的可視視窗,而是一個管理類,是Activity和整個View系統互動的介面,是Activity和View互動系統的中間層,

  • DecorView是PhoneWindow的一個內部類,是整個View層級的最頂層,一般包括標題欄和內容欄兩部分,會根據不同的主題特性調整不同的布局,它是在setContentView方法中被創建,具體點來說是在PhoneWindow的installDecor方法中被創建,

  • ViewRootImpl是DecorView的parent,用來控制View的各種事件,在handleResumeActivity方法中被創建,

21、requestLayout和invalidate

requestLayout方法是用來觸發繪制流程,他會會一層層呼叫 parent 的requestLayout,一直到最上層也就是ViewRootImpl的requestLayout,這里也就是判斷執行緒的地方了,最后會執行到performMeasure -> performLayout -> performDraw 三個繪制流程,也就是測量——布局——繪制,

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();//執行繪制流程
    }
}

其中performMeasure方法會執行到View的measure方法,用來測量大小,performLayout方法會執行到view的layout方法,用來計算位置,performDraw方法需要注意下,他會執行到view的draw方法,但是并不一定會進行繪制,只有只有 flag 被設定為 PFLAG_DIRTY_OPAQUE 才會進行繪制,

invalidate方法也是用來觸發繪制流程,主要表現就是會呼叫draw()方法,雖然他也會走到scheduleTraversals方法,也就是會走到三大流程,但是View會通過mPrivateFlags來判斷是否進行onMeasure和onLayout操作,而在用invalidate方法時,更新了mPrivateFlags,所以不會進行measure和layout,同時他也會設定Flag為PFLAG_DIRTY_OPAQUE,所以肯定會執行onDraw方法,

private void invalidateRectOnScreen(Rect dirty) {
    final Rect localDirty = mDirty;
    //...
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();//執行繪制流程
    }
}

最后看一下scheduleTraversals方法中三大繪制流程邏輯,是不是我們之前說的那樣,FORCE_LAYOUT標志才會onMeasure和onLayout,PFLAG_DIRTY_OPAQUE標志才會onDraw:

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    // 只有mPrivateFlags為PFLAG_FORCE_LAYOUT的時候才會進行onMeasure方法
    if (forceLayout || needsLayout) {
      onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    // 設定 LAYOUT_REQUIRED flag
    mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
  }


  public void layout(int l, int t, int r, int b) {
    ...
    //判斷標記位為PFLAG_LAYOUT_REQUIRED的時候才進行onLayout方法
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
     }
 }



public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    // flag 是 PFLAG_DIRTY_OPAQUE 則需要繪制
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    if (!dirtyOpaque) onDraw(canvas);
    // 繪制 Child
    dispatchDraw(canvas);
    // foreground 不管 dirtyOpaque 標志,每次都會繪制
    onDrawForeground(canvas);
} 

參考文章中有一段總結挺好的:

雖然兩者都是用來觸發繪制流程,但是在measure和layout程序中,只會對 flag 設定為 FORCE_LAYOUT 的情況進行重新測量和布局,而draw方法中只會重繪flag為 dirty 的區域,requestLayout 是用來設定FORCE_LAYOUT標志,invalidate 用來設定 dirty 標志,所以 requestLayout 只會觸發 measure 和 layout,invalidate 只會觸發 draw,

22、系統為什么提供Handler

這點大家應該都知道一些,就是為了切換執行緒,主要就是為了解決在子執行緒無法訪問UI的問題,

那么為什么系統不允許在子執行緒中訪問UI呢?

  • 因為Android的UI控制元件不是執行緒安全的,所以采用單執行緒模型來處理UI操作,通過Handler切換UI訪問的執行緒即可,

那么為什么不給UI控制元件加鎖呢?

  • 因為加鎖會讓UI訪問的邏輯變得復雜,而且會降低UI訪問的效率,阻塞執行緒執行,

Handler是怎么獲取到當前執行緒的Looper的

  • 大家應該都知道Looper是系結到執行緒上的,他的作用域就是執行緒,而且不同執行緒具有不同的Looper,也就是要從不同的執行緒取出執行緒中的Looper物件,這里用到的就是ThreadLocal,

假設我們不知道有這個類,如果要完成這樣一個需求,從不同的執行緒獲取執行緒中的Looper,是不是可以采用一個全域物件,比如hashmap,用來存盤執行緒和對應的Looper?

所以需要一個管理Looper的類,但是,執行緒中并不止這一個要存盤和獲取的資料,還有可能有其他的需求,也是跟執行緒所系結的,所以,我們的系統就設計出了ThreadLocal這種工具類,

ThreadLocal的作業流程是這樣的:我們從不同的執行緒可以訪問同一個ThreadLocal的get方法,然后ThreadLocal會從各自的執行緒中取出一個陣列,然后再陣列中通過ThreadLocal的索引找出對應的value值,具體邏輯呢,我們還是看看代碼,分別是ThreadLocal的get方法和set方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
} 

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}    

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

首先看看set方法,獲取到當前執行緒,然后取出執行緒中的threadLocals變數,是一個ThreadLocalMap類,然后將當前的ThreadLocal作為key,要設定的值作為value存到這個map中,

get方法就同理了,還是獲取到當前執行緒,然后取出執行緒中的ThreadLocalMap實體,然后從中取到當前ThreadLocal對應的值,

其實可以看到,操作的物件都是執行緒中的ThreadLocalMap實體,也就是讀寫操作都只限制在執行緒內部,這也就是ThreadLocal故意設計的精妙之處了,他可以在不同的執行緒進行讀寫資料而且執行緒之間互不干擾,

畫個圖方便理解記憶:

當MessageQueue 沒有訊息的時候,在干什么,會占用CPU資源嗎,

  • MessageQueue 沒有訊息時,便阻塞在 loop 的 queue.next() 方法這里,具體就是會呼叫到nativePollOnce方法里,最終呼叫到epoll_wait()進行阻塞等待,

這時,主執行緒會進行休眠狀態,也就不會消耗CPU資源,當下個訊息到達的時候,就會通過pipe管道寫入資料然后喚醒主執行緒進行作業,

這里涉及到阻塞和喚醒的機制叫做 epoll 機制,

先說說檔案描述符和I/O多路復用:

在Linux作業系統中,可以將一切都看作是檔案,而檔案描述符簡稱fd,當程式打開一個現有檔案或者創建一個新檔案時,內核向行程回傳一個檔案描述符,可以理解為一個索引值,

I/O多路復用是一種機制,讓單個行程可以監視多個檔案描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作

所以I/O多路復用其實就是一種監聽讀寫的通知機制,而Linux提供的三種 IO 復用方式分別是:select、poll 和 epoll ,而這其中epoll是性能最好的多路I/O就緒通知方法,

所以,這里用到的epoll其實就是一種I/O多路復用方式,用來監控多個檔案描述符的I/O事件,通過epoll_wait方法等待I/O事件,如果當前沒有可用的事件則阻塞呼叫執行緒,

23、Binder通信程序和原理

首先,還是看一張圖,原圖也是出自神書中:

首先要明確的是客戶端行程是無法直接操作服務端中的類和方法的,因為不同行程直接是不共享資源的,所以客戶端這邊操作的只是服務端行程的一個代理物件,也就是一個服務端的類參考,也就是Binder參考,

總體通信流程就是:

  • 客戶端通過代理物件向服務器發送請求,
  • 代理物件通過Binder驅動發送到服務器行程
  • 服務器行程處理請求,并通過Binder驅動回傳處理結果給代理物件
  • 代理物件將結果回傳給客戶端,

再看看在我們應用中常常用到的作業模型,上圖:

這就是在應用層面我們常用的作業模型,通過ServiceManager去獲取各種系統行程服務,這里的通信程序如下:

  • 服務端跨行程的類都要繼承Binder類,所以也就是服務端對應的Binder物體,這個類并不是實際真實的遠程Binder物件,而是一個Binder參考(即服務端的類參考),會在Binder驅動里還要做一次映射,
  • 客戶端要呼叫遠程物件函式時,只需把資料寫入到Parcel,在呼叫所持有的Binder參考的transact()函式
  • transact函式執行程序中會把引數、識別符號(標記遠程物件及其函式)等資料放入到Client的共享記憶體,Binder驅動從Client的共享記憶體中讀取資料,根據這些資料找到對應的遠程行程的共享記憶體,
  • 然后把資料拷貝到遠程行程的共享記憶體中,并通知遠程行程執行onTransact()函式,這個函式也是屬于Binder類,
  • 遠程行程Binder物件執行完成后,將得到的寫入自己的共享記憶體中,Binder驅動再將遠程行程的共享記憶體資料拷貝到客戶端的共享記憶體,并喚醒客戶端執行緒,

所以通信程序中比較重要的就是這個服務端的Binder參考,通過它來找到服務端并與之完成通信,

看到這里可能有的人疑惑了,圖中執行緒池怎么沒用到啊?

  • 可以從第一張圖中看出,Binder執行緒池位于服務端,它的主要作用就是將每個業務模塊的Binder請求統一轉發到遠程Servie中去執行,從而避免了重復創建Service的程序,也就是服務端只有一個,但是可以處理多個不同客戶端的Binder請求,

24、Binder在在Android中的應用

Binder在Android中的應用除了剛才的ServiceManager,你還想到了什么呢?

  • 系統服務是用過getSystemService獲取的服務,內部也就是通過ServiceManager,例如四大組件的啟動調度等作業,就是通過Binder機制傳遞給ActivityManagerService,再反饋給Zygote,而我們自己平時應用中獲取服務也是通過getSystemService(getApplication().WINDOW_SERVICE)代碼獲取,
  • AIDL(Android Interface definition language),例如我們定義一個IServer.aidl檔案,aidl工具會自動生成一個IServer.java的java介面類(包含Stub,Proxy等內部類),
  • 前臺行程通過bindService系結后臺服務行程時,onServiceConnected(ComponentName name, IBinder service)傳回IBinder物件,并且可以通過IServer.Stub.asInterface(service)獲取IServer的內部類Proxy的物件,其實作了IServer介面,

25、Binder優勢

在Linux中,行程通信的方式肯定不止Binder這一種,還有以下這些:

  • 管道(Pipe)
  • 信號(Signal)
  • 訊息佇列(Message)
  • 共享記憶體(Share Memory)
  • 套接字(Socket)
  • Binder

而Binder在這之后主要有以下優點:

  • 性能高,效率高:傳統的IPC(套接字、管道、訊息佇列)需要拷貝兩次記憶體、Binder只需要拷貝一次記憶體、共享記憶體不需要拷貝記憶體,
  • 安全性好:接收方可以從資料包中獲取發送發的行程Id和用戶Id,方便驗證發送方的身份,其他IPC想要實驗只能夠主動存入,但是這有可能在發送的程序中被修改,

熟悉Zygote的朋友可能知道,在fork()行程的時候,也就是向Zygote行程發出創建行程的訊息的時候,用到的行程間通信方式就不是Binder了,而換成了Socket,這主要是因為fork不允許存在多執行緒,Binder通訊偏偏就是多執行緒,

所以具體的情況還是要去具體選擇合適的IPC方式,

26、RecyclerView預取機制與快取機制

講一下RecyclerView的快取機制,滑動10個,再滑回去,會有幾個執行onBindView,快取的是什么?cachedView會執行onBindView嗎?

這兩個問題都是關于快取的,我就一起說了,

1)首先說下RecycleView的快取結構:

Recycleview有四級快取,分別是mAttachedScrap(螢屏內),mCacheViews(螢屏外),mViewCacheExtension(自定義快取),mRecyclerPool(快取池)

  • mAttachedScrap(螢屏內),用于螢屏內itemview快速重用,不需要重新createView和bindView
  • mCacheViews(螢屏外),保存最近移出螢屏的ViewHolder,包含資料和position資訊,復用時必須是相同位置的ViewHolder才能復用,應用場景在那些需要來回滑動的串列中,當往回滑動時,能直接復用ViewHolder資料,不需要重新bindView,
  • mViewCacheExtension(自定義快取),不直接使用,需要用戶自定義實作,默認不實作,
  • mRecyclerPool(快取池),當cacheView滿了后或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中,放之前會把ViewHolder資料清除掉,所以復用時需要重新bindView,

2)四級快取按照順序需要依次讀取,所以完整快取流程是:

保存快取流程:

  • 插入或是洗掉itemView時,先把螢屏內的ViewHolder保存至AttachedScrap中
  • 滑動螢屏的時候,先消失的itemview會保存到CacheView,CacheView大小默認是2,超過數量的話按照先入先出原則,移出頭部的itemview保存到RecyclerPool快取池(如果有自定義快取就會保存到自定義快取里),RecyclerPool快取池會按照itemview的itemtype進行保存,每個itemType快取個數為5個,超過就會被回收,

獲取快取流程:

  • AttachedScrap中獲取,通過pos匹配holder——>獲取失敗,從CacheView中獲取,也是通過pos獲取holder快取 ——>獲取失敗,從自定義快取中獲取快取——>獲取失敗,從mRecyclerPool中獲取 ——>獲取失敗,重新創建viewholder——createViewHolder并bindview,

3)了解了快取結構和快取流程,我們再來看看具體的問題 滑動10個,再滑回去,會有幾個執行onBindView?

  • 由之前的快取結構可知,需要重新執行onBindView的只有一種快取區,就是快取池mRecyclerPool,

所以我們假設從加載RecyclView開始盤的話(頁面假設可以容納7條資料):

  • 首先,7條資料會依次呼叫onCreateViewHolder和onBindViewHolder,
  • 往下滑一條(position=7),那么會把position=0的資料放到mCacheViews中,此時mCacheViews快取區數量為1,mRecyclerPool數量為0,然后新出現的position=7的資料通過postion在mCacheViews中找不到對應的ViewHolder,通過itemtype也在mRecyclerPool中找不到對應的資料,所以會呼叫onCreateViewHolder和onBindViewHolder方法,
  • 再往下滑一條資料(position=8),如上,
  • 再往下滑一條資料(position=9),position=2的資料會放到mCacheViews中,但是由于mCacheViews快取區默認容量為2,所以position=0的資料會被清空資料然后放到mRecyclerPool快取池中,而新出現的position=9資料由于在mRecyclerPool中還是找不到相應type的ViewHolder,所以還是會走onCreateViewHolder和onBindViewHolder方法,所以此時mCacheViews快取區數量為2,mRecyclerPool數量為1,
  • 再往下滑一條資料(position=10),這時候由于可以在mRecyclerPool中找到相同viewtype的ViewHolder了,所以就直接復用了,并呼叫onBindViewHolder方法系結資料,
  • 后面依次類推,剛消失的兩條資料會被放到mCacheViews中,再出現的時候是不會呼叫onBindViewHolder方法,而復用的第三條資料是從mRecyclerPool中取得,就會呼叫onBindViewHolder方法了,

4)所以這個問題就得出結論了(假設mCacheViews容量為默認值2):

  • 如果一開始滑動的是新資料,那么滑動10個,就會走10個bindview方法,然后滑回去,會走10-2個bindview方法,一共18次呼叫,

  • 如果一開始滑動的是老資料,那么滑動10-2個,就會走8個bindview方法,然后滑回去,會走10-2個bindview方法,一共16次呼叫,

但是但是,實際情況又有點不一樣,因為Recycleview在v25版本引入了一個新的機制,預取機制,

預取機制,就是在滑動程序中,會把將要展示的一個元素提前快取到mCachedViews中,所以滑動10個元素的時候,第11個元素也會被創建,也就多走了一次bindview方法,但是滑回去的時候不影響,因為就算提前取了一個快取資料,只是把bindview方法提前了,并不影響總的系結item數量,

所以滑動的是新資料的情況下就會多一次呼叫bindview方法,

5)總結,問題怎么答呢?

  • 四級快取和流程說一下,
  • 滑動10個,再滑回去,bindview可以是19次呼叫,可以是16次呼叫,
  • 快取的其實就是快取item的view,在Recycleview中就是viewholder,
  • cachedView就是mCacheViews快取區中的view,是不需要重新系結資料的,

27、如何實作RecyclerView的區域更新,用過payload嗎,notifyItemChange方法中的引數?

  • notifyDataSetChanged(),重繪全部可見的item,*notifyItemChanged(int),重繪指定item,
  • notifyItemRangeChanged(int,int),從指定位置開始重繪指定個item,
  • notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int),插入、移動一個并自動重繪,
  • notifyItemChanged(int, Object),區域重繪,

可以看到,關于view的區域重繪就是notifyItemChanged(int, Object)方法,下面具體說說:

notifyItemChange有兩個構造方法:

  • notifyItemChanged(int position, @Nullable Object payload)
  • notifyItemChanged(int position)

其中payload引數可以認為是你要重繪的一個標示,比如我有時候只想重繪itemView中的textview,有時候只想重繪imageview?又或者我只想某一個view的文字顏色進行高亮設定?那么我就可以通過payload引數來標示這個特殊的需求了,

具體怎么做呢?比如我呼叫了notifyItemChanged(14,"changeColor"),那么在onBindViewHolder回呼方法中做下判斷即可:

@Override
public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
    if (payloads.isEmpty()) {
        // payloads為空,說明是更新整個ViewHolder
        onBindViewHolder(holder, position);
    } else {
        // payloads不為空,這只更新需要更新的View即可,
        String payload = payloads.get(0).toString();
        if ("changeColor".equals(payload)) {
            holder.textView.setTextColor("");
        }
    }
}

28、RecyclerView嵌套RecyclerView滑動沖突,NestScrollView嵌套RecyclerView

1)RecyclerView嵌套RecyclerView的情況下,如果兩者都要上下滑動,那么就會引起滑動沖突,默認情況下外層的RecycleView可滑,內層不可滑,

之前說過解決滑動沖突的辦法有兩種:內部攔截法和外部攔截法,這里我提供一種內部攔截法,還有一些其他的辦法大家可以自己思考下,

holder.recyclerView.setOnTouchListener { v, event ->
    when(event.action){
        //當按下操作的時候,就通知父view不要攔截,拿起操作就設定可以攔截,正常走父view的滑動,
        MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)
        MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)
    }
false}

2)關于ScrclerView的滑動沖突還是同樣的解決辦法,就是進行事件攔截,還有一個辦法就是用Nestedscrollview代替ScrollView,Nestedscrollview是官方為了解決滑動沖突問題而設計的新的View,它的定義就是支持嵌套滑動的ScrollView,

所以直接替換成Nestedscrollview就能保證兩者都能正常滑動了,但是要注意設定RecyclerView.setNestedScrollingEnabled(false)這個方法,用來取消RecyclerView本身的滑動效果,

這是因為RecyclerView默認是setNestedScrollingEnabled(true),這個方法的含義是支持嵌套滾動的,也就是說當它嵌套在NestedScrollView中時,默認會隨著NestedScrollView滾動而滾動,放棄了自己的滾動,所以給我們的感覺就是滯留、卡頓,所以我們將它設定為false就解決了卡頓問題,讓他正常的滑動,不受外部影響,

系統的面試復習路線

多余的話就不講了,接下來將分享我之前面試的復習程序,如果你也在準備面試但是不知道怎么高效復習,可以參考一下我的復習路線,有任何問題也歡迎一起互相交流,加油吧!

這里給大家提供一個方向,進行體系化的學習:

1、看視頻進行系統學習

這幾年的Crud經歷,讓我明白自己真的算是菜雞中的戰斗機,也正因為Crud,導致自己技術比較零散,也不夠深入不夠系統,所以重新進行學習是很有必要的,我差的是系統知識,差的結構框架和思路,所以通過視頻來學習,效果更好,也更全面,關于視頻學習,個人可以推薦去B站進行學習,B站上有很多學習視頻,唯一的缺點就是免費的容易過時,

另外,我自己也珍藏了好幾套視頻,有需要的我也可以分享給你,

2、進行系統梳理知識,提升儲備

客戶端開發的知識點就那么多,面試問來問去還是那么點東西,所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度,so,出去面試時先看看自己復習到了哪個階段就好,

系統學習方向:

  • 架構師筑基必備技能:深入Java泛型+注解深入淺出+并發編程+資料傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO

  • Android高級UI與FrameWork原始碼:高級UI晉升+Framework內核決議+Android組件內核+資料持久化

  • 360°全方面性能調優:設計思想與代碼質量優化+程式性能優化+開發效率優化

  • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網路訪問框架設計+RXJava回應式編程框架設計+IOC架構設計+Android架構組件Jetpack

  • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發

  • 微信小程式:小程式介紹+UI開發+API操作+微信對接

  • Hybrid 開發與Flutter:Html5專案實戰+Flutter進階

知識梳理完之后,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結,

3、讀原始碼,看實戰筆記,學習大神思路

“編程語言是程式員的表達的方式,而架構是程式員對世界的認知”,所以,程式員要想快速認知并學習架構,讀原始碼是必不可少的,閱讀原始碼,是解決問題 + 理解事物,更重要的:看到原始碼背后的想法;程式員說:讀萬行原始碼,行萬種實踐,

主要內含微信 MMKV 原始碼、AsyncTask 原始碼、Volley 原始碼、Retrofit原始碼、OkHttp 原始碼等等,

4、面試前夕,刷題沖刺

面試的前一周時間內,就可以開始刷題沖刺了,請記住,刷題的時候,技術的優先,演算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎么會問,

關于面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:

總結

改變人生,沒有什么捷徑可言,這條路需要自己親自去走一走,只有深入思考,不斷反思總結,保持學習的熱情,一步一步構建自己完整的知識體系,才是最終的制勝之道,也是程式員應該承擔的使命,

以上內容均免費分享給大家,需要完整版的朋友,點這里可以看到全部內容,

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

標籤:其他

上一篇:第三屆廣西大學東信杯題解(感謝中國東信爸爸贊助支持)

下一篇:OSPF的基本配置實驗(四)

標籤雲
其他(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)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more