撒花,2022Android最新面試專題:完結篇

語言: CN / TW / HK

theme: mk-cute

前言

面試專題前面的百度篇,騰訊篇,阿里篇,京東篇,bilibili篇,網易篇,位元組篇,小紅書,小米,攜程十大板塊已經更新完了,還剩下最後個專題~持續更新中。

1.12W字;2022最新Android11位大廠面試專題(一)百度篇

2.12W字;2022最新Android11位大廠面試專題(二)阿里篇

3.12W字;2022最新Android11位大廠面試專題(三)騰訊篇

4.麵霸養成記;50萬字Android面試文件(四五)位元組,京東篇

5.麵霸養成記;50萬字Android面試文件(六七)網易,Bilibili篇

6.麵霸養成記;50萬字Android面試文件(八九)小紅書,小米篇

7.含淚刷128道面試題,50萬字2022最新Android11位大廠面試專題(七)

一共50W字的文件,面試專題12W字只是一小部分,字數限制,分幾篇更。

關注公眾號:初一十五a

提前解鎖 《整套50W字Android體系PDF》,讓學習更貼近未來實戰。

總共囊括1.騰訊Android開發筆記(33W字)

2.2022最新Android十一位大廠面試專題(12W字)

3.音視訊經典面試題(6W字)

4.Jetpack全家桶

5.Android 效能監控框架Matrix

6.JVM

7.車載應用開發

共十一模組,今天來更新第11專題愛奇藝篇,面試專題完結啦🤣

十一丶愛奇藝

1.Android佈局層級過深為什麼會對效能有影響?為什麼Compose沒有佈局巢狀問題?

做過佈局效能優化的同學都知道,為了優化介面載入速度,要儘可能的減少佈局的層級。這主要是因為佈局層級的增加,可能會導致測量時間呈指數級增長。

而Compose卻沒有這個問題,它從根本上解決了佈局層級對佈局效能的影響: Compose介面只允許一次測量。這意味著隨著佈局層級的加深,測量時間也只是線性增長的.

下面我們就一起來看看Compose到底是怎麼只測量一次就把活給幹了的,本文主要包括以下內容: - 佈局層級過深為什麼影響效能? - Compose為什麼沒有佈局巢狀問題?

①佈局層級過深為什麼影響效能?

我們總說佈局層級過深會影響效能,那麼到底是怎麼影響的呢?主要是因為在某些情況下ViewGroup會對子View進行多次測量

舉個例子 ```

<View
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="@android:color/holo_red_dark" />

<View
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@android:color/black" />

`` - LinearLayout寬度為wrap_content,因此它將選擇子View的最大寬度為其最後的寬度 - 但是有個子View的寬度為match_parent,意思它將以LinearLayout的寬度為寬度,這就陷入死迴圈了 - 因此這時候, LinearLayout 就會先以0為強制寬度測量一下子View,並正常地測量剩下的其他子View,然後再用其他子View裡最寬的那個的寬度,二次測量這個match_parent的子 View,最終得出它的尺寸,並把這個寬度作為自己最終的寬度。 - 這是對單個子View的二次測量,如果有多個子View寫了match_parent` ,那就需要對它們每一個都進行二次測量。 - 除此之外,如果在LinearLayout中使用了weight會導致測量3次甚至更多,重複測量在Android中是很常見的

上面介紹了為什麼會出現重複測量,那麼會有什麼影響呢?不過是多測量了幾次,會對效能有什麼大的影響嗎?

之所以需要避免佈局層級過深是因為它對效能的影響是指數級的

  • 如果我們的佈局有兩層,其中父View會對每個子View做二次測量,那它的每個子View一共需要被測量 2 次
  • 如果增加到三層,並且每個父View依然都做二次測量,這時候最下面的子View被測量的次數就直接翻倍了,變成 4 次
  • 同理,增加到 4 層的話會再次翻倍,子 View 需要被測量 8 次

image.png

也就是說,對於會做二次測量的系統,層級加深對測量時間的影響是指數級的,這就是Android官方文件建議我們減少佈局層級的原因

②Compose為什麼沒有佈局巢狀問題?

我們知道,Compose只允許測量一次,不允許重複測量。

如果每個父元件對每個子元件只測量一次,那就直接意味著介面中的每個元件只會被測量一次

image.png

這樣即使佈局層級加深,測量時間卻沒有增加,把元件載入的時間複雜度從O(2ⁿ) 降到了 O(n)。

那麼問題就來了,上面我們已經知道,多次測量有時是必要的,但是為什麼Compose不需要呢?

Compose中引入了固有特性測量(Intrinsic Measurement)

固有特性測量即Compose允許父元件在對子元件進行測量之前,先測量一下子元件的「固有尺寸」

我們上面說的,ViewGroup的二次測量,也是先進行這種「粗略測量」再進行最終的「正式測量」,使用固有特性測量可以產生同樣的效果

而使用固有特性測量之所以有效能優勢,主要是因為其不會隨著層級的加深而加倍,固有特性測量也只進行一次

Compose會先對整個元件樹進行一次Intrinsic測量,然後再對整體進行正式的測量。這樣開闢兩個平行的測量過程,就可以避免因為層級增加而對同一個子元件反覆測量所導致的測量時間的不斷加倍了。

image.png 總結成一句話就是,在Compose裡瘋狂巢狀地寫介面,和把所有元件全都寫進同一層裡面,效能是一樣的!所以Compose沒有佈局巢狀問題

2.kotlin協程

kotlin(完整版)→33W字開發筆記中有詳細版

3.HashMap原理(第三章第10題)

4.演算法:手寫快排

快速排序演算法思路

從小到大排序時

快速排序演算法原理: 快速排序利用了分治的思想,採用遞迴來實現 如果要排序一個數組,先取一個參照數(這裡取陣列最後一個數作為參照)把陣列從分成以引數為”中間“結點的前後2部分,陣列中參照數之前的都小於參照數,參照數之後的都大於參照數, 然後,對參照數的前後兩部分分別排序 這樣整個陣列就有序了。

遞推公式:

  • quickSort(p,r) = partition(p,r)+quickSortCore(p,q-1)+quickSortCore(q+1,r);
  • q = partition(p,r);
  • 終止條件:p>=r 不用繼續分解
  • p: 每段的起始下標
  • r: 每段的末尾下標
  • q: 每段的"中間"下標

最好情況下 :

image.png

最壞情況下

image.png

時間複雜度:O(N*logN) ~ O(n 2 n^2n 2 )

1.快速排序演算法的耗時是在拆分這裡(partition),通過上圖可以看出是個完全二叉樹,當n規模足夠大時就可以近似看成是滿二叉樹,由partition函式來看合併的時間複雜度是O(n)

  • 最好情況下 O(nlogn):即拆分時左右2部分均分,時間複雜度由上圖可知每層處理的資料規模都是n 假設每層耗時常量t 樹高為h,那麼總耗時=nth, T(n) = ntlog ⁡ 2 n \log_2 nlog 2 n (參考:完全二叉樹的樹高H和結點數N的關係)

  • 最壞情況下 O(n 2):即當陣列資料已經有序時例如{1,2,3,4,5},此時無法均分,取5為參照數,每次就只會執行quickSortCore(p,q-1)方法,partition處理的資料規模就是每次:T(n) = (n+n-1+ …1)t = tn*(n-1)/2 (t指單個數據處理耗時,是個常量值,n指資料規模)

空間複雜度:O(logn)~O(n nn),快速排序雖然沒有申請額外的空間,但是有遞迴函式呼叫棧的記憶體消耗。

  • 最好情況下 O(logn):即拆分時左右2部分均分時樹深度最深也就是樹的高度可表示log ⁡ 2 n \log_2 nlog 2 n。

  • 最壞情況下 O(n nn):即當陣列資料已經有序時例如{1,2,3,4,5},此時無法均分,取5為參照數,每次就只會執行quickSortCore(p,q-1)方法一直往下遞迴

快速排序是不穩定的排序演算法。 例如{6,8,7,6,3,5,9,4},取末尾4為參照數,拆分時,當下標j=4指向數3時 此時下標i=-1,r=7 ,arr[j] < arr[r] 進入交換導致第一個6在第二個6的後面順序發生改變。

總結:快速排序演算法時間複雜度:O(N*logN) ~ O(n 2 n^2n 2), 空間複雜度:O(logn) ~ O(n nn),該演算法可以進行進一步優化:出發點就是儘量使其每次左右均分資料規模

①核心程式碼

``` private static int[] quickSort(int[] arr) { if (null != arr && arr.length > 0) { quickSortCore(arr, 0, arr.length - 1); } return arr; }

private static void quickSortCore(int[] arr, int p, int r) {
    if (p >= r) {
        return;
    }
    int q = partition(arr, p, r);
    quickSortCore(arr, p, q - 1);
    quickSortCore(arr, q + 1, r);
}

/**
 * [p,i] 小於 arr[r]的區域
 * [i+1,j-1] 大於 arr[r]的區域
 * [j,r-1]  未處理的區域
 * r 下標預設是分割槽點參照的元素
 * p,j,i,r 均指下標,arr[r]指參照數
 *
 * @param arr
 * @param p
 * @param r
 * @return
 */
private static int partition(int[] arr, int p, int r) {
    // 初始時設定i= -1,表示小於參照數的區域是空,i標識小於參照數的區域末尾位置
    int i = p - 1;
    // 掃描未處理區域和挨個和參照數進行比較
    for (int j = p; j < r; j++) {
        // 比參照數小的放到[p,i]區域,[p,i]區域就開始擴大了
        if (arr[j] < arr[r]) {
            swap(arr, i + 1, j);
            i++;
        }
    }
    // 再把參照數放到比它小的區域的後一個下標位置,這樣 參照數左側就是全部小於參照數的數,右側就是大於參照數的數,可以繼續往下拆分左右2側遞迴了
    swap(arr, i + 1, r);
    return i + 1;
}

private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

```

②測試用例

``` package arithmetic.ecut.com.排序.a_快速排序;

/* * 快速排序 * * @author 起鳳 * @description: TODO * @date 2022/4/11 / public class QuickSort { public static void main(String[] args) { int[] arr = {1, 5, 6, 2, 3, 4}; print(quickSort(arr));

    int[] arr1 = {2, 3, 1, 4, -1, 8, -1};
    print(quickSort(arr1));

    int[] arr2 = {-1, 7, 1, 4, 5, 8, 7};
    print(quickSort(arr2));
}

private static int[] quickSort(int[] arr) {
    if (null != arr && arr.length > 0) {
        quickSortCore(arr, 0, arr.length - 1);
    }
    return arr;
}

private static void quickSortCore(int[] arr, int p, int r) {
    if (p >= r) {
        return;
    }
    int q = partition(arr, p, r);
    quickSortCore(arr, p, q - 1);
    quickSortCore(arr, q + 1, r);
}

/**
 * [p,i] 小於 arr[r]的區域
 * [i+1,j-1] 大於 arr[r]的區域
 * [j,r-1]  未處理的區域
 * r 下標預設是分割槽點參照的元素
 * p,j,i,r 均指下標,arr[r]指參照數
 *
 * @param arr
 * @param p
 * @param r
 * @return
 */
private static int partition(int[] arr, int p, int r) {
    // 初始時設定i= -1,表示小於參照數的區域是空,i標識小於參照數的區域末尾位置
    int i = p - 1;
    // 掃描未處理區域和挨個和參照數進行比較
    for (int j = p; j < r; j++) {
        // 比參照數小的放到[p,i]區域,[p,i]區域就開始擴大了
        if (arr[j] < arr[r]) {
            swap(arr, i + 1, j);
            i++;
        }
    }
    // 再把參照數放到比它小的區域的後一個下標位置,這樣 參照數左側就是全部小於參照數的數,右側就是大於參照數的數,可以繼續往下拆分左右2側遞迴了
    swap(arr, i + 1, r);
    return i + 1;
}

private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

private static void print(int[] sort) {
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < sort.length; i++) {
        builder.append(sort[i]).append(",");
    }
    System.out.println(builder);
}

} ```

image.png

5.Activity啟動模式

①標準模式——standard

這個啟動模式是最常見的,Activity 預設就是此啟動模式。每啟動一次 Activity,就會建立一個新 Activity 例項並置於棧頂。誰啟動了這個 Activity,那麼這個 Activity 就執行在啟動它的那個 Activity 所在的棧中。

其實後面這句話挺重要,之前學習的時候並不是太理解這句話,但也是在不久前遇到了一個問題讓我重新理解了:Android 中有多視窗模式,這個問題是在多視窗模式下發現的,我們應用中有個地方會呼叫設定中的頁面來選擇聲音,在正常模式下是沒有問題的,但是多視窗模式下就會重新啟動一個任務棧,但我們系統中限制多視窗模式下只能有一個應用在前臺,結果我們自己的應用被幹掉了。。。大家一定引以為戒,知識點的每一句話都有可能有用!

下面咱們來測試下標準模式,先一步一步來,先從第一個頁面跳轉到第二個頁面,看下 log:

E/MainActivity: onCreate: E/MainActivity: onStart: E/MainActivity: onResume: E/MainActivity: onPause: E/TwoActivity: onCreate: E/TwoActivity: onStart: E/TwoActivity: onResume: E/MainActivity: onStop:

沒什麼問題,和預想的一致,然後回到桌面再開啟應用,看下 log:

E/TwoActivity: onPause: E/TwoActivity: onStop: E/TwoActivity: onStart: E/TwoActivity: onResume:

嗯,沒問題,現在任務棧裡有兩個 Activity,點選返回鍵依次退出再來看下 log:

E/TwoActivity: onPause: E/MainActivity: onStart: E/MainActivity: onResume: E/TwoActivity: onStop: E/TwoActivity: onDestroy: E/MainActivity: onPause: E/MainActivity: onStop:

從第二個 Activity 回到第一個 Activity 可以理解,但是大家有沒有發現第一個 Activity 並沒有走 onDestroy ,這裡引用下一個厲害的大哥文章中的描述吧:

Android 12 以前,當我們處於 Root Activity 時,點選返回鍵時,應用返回桌面, Activity 執行 onDestroy,程式結束。Android 12 起同樣場景下 Activity 只會 onStop,不再執行 onDestroy。

到這裡標準模式就差不多了,因為這是預設的啟動模式,大家使用也最頻繁,也就不再囉嗦。

棧頂模式——singleTop

棧頂模式其實很好理解,如果棧頂存在該activity的例項,則複用,不存在新建放入棧頂,它的表現幾乎和 上面剛說的標準模式一模一樣,棧頂模式的 Activity 例項可以無限多,唯一的區別是如果在棧頂已經有一個相同型別的 Activity 例項,那麼 Intent 則不會再去建立一個 Activity,而是通過 onNewIntent() 傳送到現有的Activity。

比如應用現在在一個詳情頁面,而且這個頁面啟動模式為棧頂模式,這個時候來了一個通知,點選通知正好要跳轉到詳情頁面,那麼這個時候任務棧就不會為這個 Activity 再建立一個例項而用已經在棧頂的之前建立好的 Activity 例項。

③棧內複用——singleTask

這個模式之前真的沒有理解透徹,之前我理解的就是如果棧記憶體在該 Activity 的例項則進行復用,如果不存在則建立。

接下來將剛才的 Demo 中的主 Activity 的啟動模式改為棧內複用,先來看下啟動應用後點擊跳轉到第二個 Activity 的 log:

E/MainActivity: onCreate: E/MainActivity: onStart: E/MainActivity: onResume: E/MainActivity: onPause: E/TwoActivity: onCreate: E/TwoActivity: onStart: E/TwoActivity: onResume: E/MainActivity: onStop:

目前來看還是比較正常的,接下來直接回到桌面,再來看下 log:

E/TwoActivity: onPause: E/TwoActivity: onStop:

也還對著呢,然後再次開啟應用再看 log:

E/TwoActivity: onDestroy: E/MainActivity: onStart: E/MainActivity: onResume:

是不是不對了,我本來想讓應用回到第二個 Activity,但為什麼第二個 Activity 直接銷燬了?

其實棧內複用中還有一點要注意,也正是我忽略的重要一點:棧內複用模式會將該例項上邊的 Activity 全部出棧,將該例項置於棧頂,這也就是出現文章開頭我說的那個問題的根本原因。

④單例模式——singleInstance

單例模式,顧名思義,就是新開一個任務棧,該棧內只存放當前例項。比如說專案中語音通話功能,來電顯示頁面採用的就可以採用單例模式進行處理。

當然還有別的方法來新開任務棧,比如說啟動 Activity 的時候加上 FLAG_ACTIVITY_NEW_TASK ,也會開啟一個新的任務棧。

這裡需要注意,即使將 Activity 的啟動模式設定為單例模式或者添加了 flag,也不會出現像上面某信那種效果,因為 Activity 的 taskAffinity 是一樣的,但如果將 Activity 的 taskAffinity 修改下,就可以出現類似於上面某信的效果,如下圖所示:

image.png

6.Activity四大啟動方式生命週期

①Standard 標準模式啟動

android:launchMode=“standard”

歸納:直接在棧頂新建一個Activity進行啟動

生命週期:

===>開啟一個Activity

I/LifecycleActivity_A: onCreate: I/LifecycleActivity_A: onStart: I/LifecycleActivity_A: onResume:

===>退出

I/LifecycleActivity_A: onPause: I/LifecycleActivity_A: onStop: I/LifecycleActivity_A: onDestroy:

②SingleTop 棧頂啟動

manifest:android:launchMode=“singleTop” FLAG:FLAG_ACTIVITY_SINGLE_TOP

歸納:當啟動的Activity在棧頂時,則進行復用,如果棧頂沒有時使用預設的Standard模式

生命週期:

===> 啟動A:

A: onCreate: A: onStart: A: onResume:

===> 棧頂啟動:

A: onPause: A: onNewIntent: A: onResume:

FLAG_ACTIVITY_CLEAR_TOP啟動

棧頂複用不走newIntent(), 將棧內的需要啟動的activity移到最頂處啟動,並將上面的activity全部出棧銷燬。

===> 啟動A FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onCreate: I/LifecycleActivity_A: onStart: I/LifecycleActivity_A: onResume: I/LifecycleActivity_A: onPause:

===> 啟動B FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onPause: I/LifecycleActivity_B: onCreate: I/LifecycleActivity_B: onStart: I/LifecycleActivity_B: onResume: I/LifecycleActivity_A: onStop:

===> 啟動A FLAG_ACTIVITY_CLEAR_TOP

I/LifecycleActivity_B: onPause: I/LifecycleActivity_A: onDestroy: I/LifecycleActivity_A: onCreate: I/LifecycleActivity_A: onStart: I/LifecycleActivity_A: onResume: I/LifecycleActivity_B: onStop: I/LifecycleActivity_B: onDestroy:

===>A退出

I/LifecycleActivity_A: onPause: I/LifecycleActivity_A: onStop: I/LifecycleActivity_A: onDestroy:

③SingleTask 棧內複用模式

android:launchMode=“singleTask”

歸納:當棧內有需要啟動的Activity時,則將該Activity移動到棧頂,並在棧中這個activity頂部的activity全部出棧銷燬。

生命週期:

===> 啟動A:

A: onCreate: A: onStart: A: onResume:

===> 啟動B:

A: onPause: B: onCreate: B: onStart: B: onResume: A: onStop:

===> 棧內啟動A:

B: onPause: A: onNewIntent: A: onRestart: A: onStart: A: onResume: B: onStop: B: onDestroy:

④FLAG_ACTIVITY_REORDER_TO_FRONT啟動

===>啟動A FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onCreate: I/LifecycleActivity_A: onStart: I/LifecycleActivity_A: onResume:

===> 啟動B FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onPause: I/LifecycleActivity_B: onCreate: I/LifecycleActivity_B: onStart: I/LifecycleActivity_B: onResume: I/LifecycleActivity_A: onStop:

===> 啟動A FLAG_ACTIVITY_REORDER_TO_FRONT

I/LifecycleActivity_B: onPause: I/LifecycleActivity_A: onNewIntent: I/LifecycleActivity_A: onRestart: I/LifecycleActivity_A: onStart: I/LifecycleActivity_A: onResume: I/LifecycleActivity_B: onStop:

===>A退出

I/LifecycleActivity_A: onPause: I/LifecycleActivity_B: onRestart: I/LifecycleActivity_B: onStart: I/LifecycleActivity_B: onResume: I/LifecycleActivity_A: onStop: I/LifecycleActivity_A: onDestroy:

===>B退出

I/LifecycleActivity_B: onPause: I/LifecycleActivity_B: onStop: I/LifecycleActivity_B: onDestroy:

⑤SingleInstance 單例啟動模式

android:launchMode=“singleinstance”

歸納:單獨建立一個棧控制元件啟動這個Activity,常用於啟動桌布,電話,鬧鐘等介面實現。

7.有序廣播例項

有序廣播:按照接收者的優先順序接收,只有一個廣播接收者能接收資訊,在此廣播接收者的邏輯執行完畢後,才會繼續傳遞。

abortBroadcast();終止廣播

有序廣播功能概述:

image.png

廣播型別:

image.png

activity_main.xml

```

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/btn"
    android:text="傳送有序廣播"
  />

```

MainActivity.java

``` package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import android.widget.Button;

public class MainActivity extends AppCompatActivity { MyReceiverOne one ; MyReceiverTow two; MyReceiverThree three; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); registerReceiver();

    Button button =findViewById(R.id.btn);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent=new Intent();
            intent.setAction("Intercept_Stitch");
            sendOrderedBroadcast(intent,null);
        }
    });
}

private void registerReceiver() {
    one=new MyReceiverOne();
    IntentFilter filter1=new IntentFilter();
    filter1.setPriority(1000);
    filter1.addAction("Intercept_Stitch");
    registerReceiver(one,filter1);

    two=new MyReceiverTow();
    IntentFilter filter2=new IntentFilter();
    filter2.setPriority(900);
    filter2.addAction("Intercept_Stitch");
    registerReceiver(two,filter2);

    three=new MyReceiverThree();
    IntentFilter filter3=new IntentFilter();
    filter3.setPriority(600);
    filter3.addAction("Intercept_Stitch");
    registerReceiver(three,filter3);
}

} ```

myReceiverone

``` package com.example.myapplication;

import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log;

class MyReceiverOne extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
    Log.i("test","自定義的廣播接收者One,接受到了廣播事件");
}

} ```

MyRiverTwo

``` package com.example.myapplication;

import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log;

class MyReceiverTow extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.i("test","自定義的廣播接收者Two,接受到了廣播事件"); } } ```

MyReceiverThree

``` package com.example.myapplication;

import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log;

class MyReceiverThree extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.i("test","自定義的廣播接收者Three,接受到了廣播事件"); } } ```

image.png

image.png

image.png

8.SharedPreferences的詳解

①SharedPreferences 首選項 介紹

儲存軟體的配置資訊 儲存的資訊:很小,簡單的資料;比如:自動登入,記住密碼,小說app(返回後再次進入還是 原來看的頁數),按鈕的狀態。 特點:當程式執行首選項裡面的資料會全部載入進內容。

②SharedPreferences的簡單使用

1.佈局檔案中,寫入兩個按鈕,儲存到SP,和從SP中獲取資料 佈局程式碼的檔案就不再寫了。

2.看MainActivity的程式碼,裡面有註釋詳解

``` package com.example.spdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

private Button btn_save;
private Button btn_obtain;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    btn_save=findViewById(R.id.btn_save);
    btn_obtain=findViewById(R.id.btn_obtain);

    //儲存到SP
    btn_save.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            SharedPreferences sp = getSharedPreferences("onClick", MODE_PRIVATE);
            sp.edit().putString("SoundCode","測點程式碼").apply();//apply才會寫入到xml配置檔案裡面
        }
    });

    //獲取到SP中的資料
    btn_obtain.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            SharedPreferences sp1 = getSharedPreferences("onClick", MODE_PRIVATE);
            //如果SoundCode,獲取的值是空的,則會彈出後面的預設值
            String obtain = sp1.getString("SoundCode", "預設值");
            Toast.makeText(MainActivity.this, obtain, Toast.LENGTH_SHORT).show();
        }
    });
}
/**
 *
 *引數1:SP的名字
 *引數2:SP儲存時,用的模式,MODE_PRIVATE常規(每次儲存都會更新),MODE_APPEND(每次儲存都會追加到後面)
 * @Override
 *     public SharedPreferences getSharedPreferences(String name, int mode) {
 *         return mBase.getSharedPreferences(name, mode);
 *     }
 *
 */

} ```

③SharedPreferences的實戰,實現記錄密碼和自動登入功能

效果如下:

1.XML程式碼如下:

```

</LinearLayout>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    >
    <Button
        android:id="@+id/btn_register"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="註冊"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        />
    <Button
        android:id="@+id/btn_login"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="登入"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        />

</LinearLayout>

```

MainActivity程式碼如下:詳解和註釋都已經寫好

``` package com.example.shareddemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences; import android.graphics.Color; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

private EditText et_name;
private EditText et_password;
private CheckBox ck_password;
private CheckBox ck_login;
private Button btn_register;
private Button btn_login;
private SharedPreferences sp;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    et_name=findViewById(R.id.et_name);
    et_password=findViewById(R.id.et_password);
    ck_password=findViewById(R.id.ck_password);
    ck_login=findViewById(R.id.ck_login);
    btn_register=findViewById(R.id.btn_register);
    btn_login=findViewById(R.id.btn_login);
     sp = getSharedPreferences("Personal", MODE_PRIVATE);
    //登入方法
    LoginMethod();
    //程式再次進入獲取SharedPreferences中的資料
    AgainInto();
}

private void AgainInto() {
    //如果獲取為空就返回預設值
    boolean ck1 = sp.getBoolean("ck_password", false);
    boolean ck2 = sp.getBoolean("ck_login", false);

    //如果是記住密碼
    if (ck1){
        String name=sp.getString("name","");
        String password=sp.getString("password","");
        et_name.setText(name);
        et_password.setText(password);
        //記住密碼打上√
        ck_password.setChecked(true);
    }
    //如果是自動登入
    if (ck2){
        ck_login.setChecked(true);
        Toast.makeText(this, "我是自動登入!", Toast.LENGTH_SHORT).show();
    }
}

private void LoginMethod() {
    btn_login.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            String name=et_name.getText().toString().trim();
            String password=et_password.getText().toString().trim();
            //判斷使用者名稱和密碼是否為空
            if (TextUtils.isEmpty(name)||TextUtils.isEmpty(password)){
                Toast.makeText(MainActivity.this, "使用者名稱和密碼不能為空", Toast.LENGTH_SHORT).show();
            }else {
                //如果記錄密碼是勾選的
                if (ck_password.isChecked()){
                    //把使用者名稱和密碼儲存在SharedPreferences中
                    sp.edit().putString("name",name).apply();
                    sp.edit().putString("password",password).apply();
                    sp.edit().putBoolean("ck_password",true).apply();
                }else {//沒有勾選,儲存空值
                    sp.edit().putString("name","").apply();
                    sp.edit().putString("password","").apply();
                    sp.edit().putBoolean("ck_password",false).apply();
                }
                //如果自動登入是勾選的
                if (ck_login.isChecked()){
                    sp.edit().putBoolean("ck_login",true).apply();
                }else {
                    sp.edit().putBoolean("ck_login",false).apply();
                }
            }
        }
    });

}

} ```

9.xml解析方式

①xml解析方式

解析:操作xml文件,將文件中的資料讀取到記憶體中

操作xml文件

1.解析(讀取):將文件中的資料讀取到記憶體中

2.寫入:將記憶體中的資料儲存到xml文件中。持久化的儲存

解析xml的方式:

1.DOM:將標記語言文件一次性載入進記憶體,在記憶體中形成一棵樹dom樹

優點:操作方便,可以對文件進行CRUD的所有操作

缺點:佔記憶體

2.SAX:逐行讀取,基於事件驅動的。

優點:不佔記憶體

缺點:只能讀取,不能增刪改

②xml常見的解析器

  • JAXP:sun公司提供的解析器,支援dom和sax兩種思想
  • DOM4J:一款非常優秀的解析器

  • Jsoup:jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文字內容。

它提供了一套非常省力的API,可通過DOM,CSS以及類似於jQuery的操作方法來取出和操作資料

  • PULL:Android作業系統內建的解析器,sax方式的。

10.json與xml的區別,json為什麼比xml更好

① JSON相比XML的不同之處

  • 沒有結束標籤
  • 更短
  • 讀寫的速度更快
  • 能夠使用內建的 JavaScript eval() 方法進行解析
  • 使用陣列
  • 不使用保留字

總之: JSON 比 XML 更小、更快,更易解析。

②XML和JSON的區別:

XML的主要組成成分:

text XML是element、attribute和element content。

JSON的主要組成成分

text JSON是object、array、string、number、boolean(true/false)和null。

11.Android view繪製流程

(第一章第2題)

12.surfaceView的顯示與隱藏

在安卓中如果有用最靈活的播放,則就是surfaceView實現的。

那麼有這樣一一個應用場景,就是我想要通過一個按鈕比如叫做“選擇一個視訊”,改按鈕會調用出一個dialog去從一個視訊列表選擇一個視訊後,在當前介面載入。

像是如下這樣

image.png

但是呢,一般的做法就是在當前介面先隱藏一個surfaceView,用setVisibility(View.GONE)或者直接在佈局上面寫。

但是你會發現,當你視訊檔案選擇之後,讓其重新顯示的時候就不能正常顯示出來了。

這裡要採用如: sv_with_ds.getChildAt(0).setVisibility(View.VISIBLE); sv_with_ds.setVisibility(View.VISIBLE); 這裡我的surfaceview雖然沒有隱藏,但是其父級隱藏了,之前我直接讓父級顯示,但是surfaceView仍舊不顯示,對於這種情況下,要先讓surfaceView先顯示,雖然它本身沒有設定隱藏。

當然純粹的直接在surfaceView中進行隱藏和顯示,我沒有試過,那樣應該是可以的

那麼對於surfaceView來説比較原生一些,經常很容易由於自己理解的不夠透徹而導致各種問題的發生。

最常見的就是其播放視訊必須要在getHolder所獲得的物件中新增一個回撥,這個回撥必須是實現了SurfaceView

13.關於移動端適配機型

image.png

14.ANR 什麼時候出現,如何排查

①ANR產生的場景:

  • activity內對事件 5秒無法完成處理
  • BroadcastReceiver 內對事件10秒無法完成處理
  • Service 的各個生命週期函式在特定時間(20秒)內無法完成處理
  • 應用程式UI執行緒存在耗時操作,例如在UI執行緒中進行網路請求,資料庫操作或者檔案操作等,可能會導致UI執行緒無法及時處理使用者輸入等。
  • 應用程式UI執行緒等待子執行緒釋放某個鎖,從而無法處理使用者的請求的輸入
  • 耗時操作的動畫需要大量的計算工作,可能導致CPU負載過重

② ANR產生的原因

主執行緒(UI執行緒)如果在規定時間內沒有處理完相應的工作,就會出現ANR, 超時產生的原因一般有:

  • 主執行緒在做一些耗時任務
  • 主執行緒被其他執行緒鎖
  • cpu被其他程序佔用,該程序沒被分配到足夠的cpu資源
  • 自身服務阻塞
  • 系統阻塞
  • 記憶體緊張

③ANR產生原因定位分析

通過ANR 日誌定位問題

當ANR發生時,我們往往通過Logcat和traces檔案(目錄/data/anr/)的相關資訊輸出去定位問題。主要包含以下幾方面:

  • 基本資訊,包括程序名、程序號、包名、系統build號、ANR 型別等等;
  • CPU使用資訊,包括活躍程序的CPU 平均佔用率、IO情況等等;
  • 執行緒堆疊資訊,所屬程序包括髮生ANR的程序、其父程序、最近有活動的3個程序等等。

測試過程發現ANR的現狀

  • 在平常測試中,ANR有基本測試不到,因為ANR基本發生在垃圾裝置中,弱網路,頻繁操作。
  • 問題不必現,即使看到了問題,定位麻煩:要去data/anr.txt 檔案裡面查詢。必須root,沒有對應關係,分析複雜,匯出檔案就必須依賴手機零距離。

引入ANR檢測工具

由於anr問題不必現,因此引入以下ANR檢測工具,當anr問題出現時,自動dump手機中的日誌資訊如trace檔案、堆疊資訊等,基本原理如下:

image.png

檢測到UI主執行緒卡頓時間超過設定的時間,如4s,即dump trace檔案以及堆疊資訊,同時丟擲異常,收集資訊,根據這些檔案資訊即可定位到發生anr的原因 。

通過traces.txt的檔案分析

在APP觸發ANR時, 系統預設會生成一個traces.txt的檔案,並放在/data/anr/下(我們可以藉助第三方的MT管理器或者adb命令進行對traces.txt進行操作)。我們就可以結合traces.txt檔案進行分析排查定位出是有app中的哪個類觸發的ANR。

15.Android的幾種動畫定義與使用

Android動畫的分類與使用

學習Android必不可少的就是動畫的使用了,在Android版本迭代的過程中,出現了很多動畫框架,這裡做一個總結。

Android動畫型別分類

逐幀動畫【Frame Animation】,即順序播放事先準備的圖片

補間動畫【Tween Animation】,View的動畫效果可以實現簡單的平移、縮放、旋轉。

屬性動畫【Property Animation】,補間動畫增強版,支援對物件執行動畫。

過渡動畫【Transition Animation】,實現Activity或View過渡動畫效果。包括5.0之後的MD過渡動畫等。

動畫的分類與版本

Android動畫實現方式分類都可以分為xml定義和java定義。

Android 3.0之前版本,逐幀動畫,補間動畫 Android 3.0之後版本,屬性動畫 Android 4.4中,過渡動畫 Android 5.0以上 MD的動畫效果

下面一起看看簡單的實現吧。

①逐幀動畫

推薦使用一些小圖片,它的效能不是很好,如果使用大圖的幀動畫,會出現效能問題導致卡頓。

比較常用的方式,在res/drawable目錄下新建動畫XML檔案:

image.png

設定或清除動畫程式碼:

//開始動畫 mIvRefreshIcon.setImageResource(R.drawable.anim_loading); mAnimationDrawable = (AnimationDrawable) mIvRefreshIcon.getDrawable(); mAnimationDrawable.start(); ​ //停止動畫 mIvRefreshIcon.clearAnimation(); if (mAnimationDrawable != null){ mAnimationDrawable.stop(); }

設定Background和設定ImageResource是一樣的效果:

image.png

ImageView voiceIcon = new ImageView(CommUtils.getContext()); voiceIcon.setBackgroundResource(message.isSelf() ? R.drawable.right_voice : R.drawable.left_voice); final AnimationDrawable frameAnim = (AnimationDrawable) voiceIcon.getBackground(); ​ frameAnimatio.start(); ​ MediaUtil.getInstance().setEventListener(new MediaUtil.EventListener() { @Override public void onStop() { frameAnimatio.stop(); frameAnimatio.selectDrawable(0); } });

②補間動畫

一句話說明補間動畫:只能給View加,不能給物件加,並且不會改變物件的真實屬性。

無需關注每一幀,只需要定義動畫開始與結束兩個關鍵幀,並指定動畫變化的時間與方式等 。主要有四種基本的效果

  • 透明度變化
  • 大小縮放變化
  • 位移變化
  • 旋轉變化

可以在xml中定義,也可以在程式碼中定義!

透明度的定義:

```

```

縮放的定義:

```

```

平移的定義:

```

```

旋轉的定義:

```

```

Java程式碼中使用補間動畫(推薦):

透明度定義:

AlphaAnimation alpha = new AlphaAnimation(0, 1); alpha.setDuration(500); //設定持續時間 alpha.setFillAfter(true); //動畫結束後保留結束狀態 alpha.setInterpolator(new AccelerateInterpolator()); //新增差值器 ivImage.setAnimation(alpha);

縮放定義:

ScaleAnimation scale = new ScaleAnimation(1.0f, scaleXY, 1.0f, scaleXY, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); scale.setDuration(durationMillis); scale.setFillAfter(true); ivImage.setAnimation(scale);

平移定義

TranslateAnimation translate = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta); translate.setDuration(durationMillis); translate.setFillAfter(true); ivImage.setAnimation(translate);

旋轉定義:

RotateAnimation rotate = new RotateAnimation(fromDegrees, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); rotate.setDuration(durationMillis); rotate.setFillAfter(true); ivImage.setAnimation(rotate);

組合Set的定義:

``` RelativeLayout rlRoot = (RelativeLayout) findViewById(R.id.rl_root);

// 旋轉動畫
RotateAnimation animRotate = new RotateAnimation(0, 360,
            Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
            0.5f);
animRotate.setDuration(1000);// 動畫時間
animRotate.setFillAfter(true);// 保持動畫結束狀態


// 縮放動畫
ScaleAnimation animScale = new ScaleAnimation(0, 1, 0, 1,
            Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f);
animScale.setDuration(1000);
animScale.setFillAfter(true);// 保持動畫結束狀態


// 漸變動畫
AlphaAnimation animAlpha = new AlphaAnimation(0, 1);
animAlpha.setDuration(2000);// 動畫時間
animAlpha.setFillAfter(true);// 保持動畫結束狀態


// 動畫集合
AnimationSet set = new AnimationSet(true);
set.addAnimation(animRotate);
set.addAnimation(animScale);
set.addAnimation(animAlpha);

// 啟動動畫
rlRoot.startAnimation(set);

set.setAnimationListener(new AnimationListener() {

    @Override
    public void onAnimationStart(Animation animation) {
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        // 動畫結束,跳轉頁面
        // 如果是第一次進入, 跳新手引導
        // 否則跳主頁面
        boolean isFirstEnter = PrefUtils.getBoolean(
                    SplashActivity.this, "is_first_enter", true);

        Intent intent;
        if (isFirstEnter) {
            // 新手引導
            intent = new Intent(getApplicationContext(),
                    GuideActivity.class);
        } else {
            // 主頁面
            intent = new Intent(getApplicationContext(),MainActivity.class);
        }

        startActivity(intent);

        finish();
        }
});

```

③屬性動畫

補間動畫增強版本。補充補間動畫的一些缺點

作用物件:任意 Java 物件,不再侷限於 檢視View物件

實現的動畫效果:可自定義各種動畫效果,不再侷限於4種基本變換:平移、旋轉、縮放 & 透明度

分為ObjectAnimator和ValueAnimator。

3.1 一個簡單的屬性動畫

先用xml的方式實現

```

```

使用:

Button b3 = (Button) findViewById(R.id.b3); Animator mAnim = AnimatorInflater.loadAnimator(this, R.animator.animator_1_0); mAnim.setTarget(b3); mAnim.start();

當然我們可以直接使用Java程式碼實現:

``` public static ObjectAnimator setObjectAnimator(View view , String type , int start , int end , long time){ ObjectAnimator mAnimator = ObjectAnimator.ofFloat(view, type, start, end);

// 設定動畫重複播放次數 = 重放次數+1 
// 動畫播放次數 = infinite時,動畫無限重複 
mAnimator.setRepeatCount(ValueAnimator.INFINITE); 
// 設定動畫執行的時長 
mAnimator.setDuration(time); 
// 設定動畫延遲播放時間 
mAnimator.setStartDelay(0); 
// 設定重複播放動畫模式 
mAnimator.setRepeatMode(ValueAnimator.RESTART); 
// ValueAnimator.RESTART(預設):正序重放 
// ValueAnimator.REVERSE:倒序回放 
//設定差值器 
mAnimator.setInterpolator(new LinearInterpolator()); 
return mAnimator;

} ```

3.2 ValueAnimator與ObjectAnimator區別:

  • ValueAnimator 類是先改變值,然後手動賦值 給物件的屬性從而實現動畫;是間接對物件屬性進行操作;
  • ObjectAnimator 類是先改變值,然後自動賦值 給物件的屬性從而實現動畫;是直接對物件屬性進行操作;

``` //不同的定義方式 ValueAnimator animator = null;

    if (isOpen) {
        //要關閉
        if (longHeight > shortHeight) {
            isOpen = false;
            animator = ValueAnimator.ofInt(longHeight, shortHeight);
        }
    } else {
        //要開啟
        if (longHeight > shortHeight) {
            isOpen = true;
            animator = ValueAnimator.ofInt(shortHeight, longHeight);
        }
    }

    animator.start();


   //不同的定義方式
   ObjectAnimator animatorX = ObjectAnimator.ofFloat(mSplashImage, "scaleX", 1f, 2f);  
   animatorX.start();

```

3.3 監聽動畫的方式:

mAnim2.addListener(new AnimatorListenerAdapter() { // 向addListener()方法中傳入介面卡物件AnimatorListenerAdapter() // 由於AnimatorListenerAdapter中已經實現好每個介面 // 所以這裡不實現全部方法也不會報錯 @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); ToastUtils.showShort("動畫結束了"); } });

3.4 組合動畫AnimatorSet:

xml的組合

```

<set android:ordering="together" > 
    <!--下面的動畫同時進行--> 
    <objectAnimator 
        android:duration="2000" 
        android:propertyName="translationX" 
        android:valueFrom="0" 
        android:valueTo="300" 
        android:valueType="floatType" > 
    </objectAnimator>

    <objectAnimator 
        android:duration="3000" 
        android:propertyName="rotation" 
        android:valueFrom="0" 
        android:valueTo="360" 
        android:valueType="floatType" > 
    </objectAnimator> 
</set>

<set android:ordering="sequentially" > 
    <!--下面的動畫按序進行--> 
    <objectAnimator 
        android:duration="1500" 
        android:propertyName="alpha" 
        android:valueFrom="1" 
        android:valueTo="0" 
        android:valueType="floatType" > 
    </objectAnimator> 
    <objectAnimator 
        android:duration="1500" 
        android:propertyName="alpha" 
        android:valueFrom="0" 
        android:valueTo="1" 
        android:valueType="floatType" > 
    </objectAnimator> 
</set>

```

Java方式的組合

ObjectAnimator translation = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX); // 平移動畫 ObjectAnimator rotate = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f); // 旋轉動畫 ObjectAnimator alpha = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f); // 透明度動畫 // 建立組合動畫的物件 AnimatorSet animSet = new AnimatorSet(); // 根據需求組合動畫 animSet.play(translation).with(rotate).before(alpha); animSet.setDuration(5000); //啟動動畫 animSet.start();

常用的組合方法

  • AnimatorSet.play(Animator anim) :播放當前動畫
  • AnimatorSet.after(long delay) :將現有動畫延遲x毫秒後執行
  • AnimatorSet.with(Animator anim) :將現有動畫和傳入的動畫同時執行
  • AnimatorSet.after(Animator anim) :將現有動畫插入到傳入的動畫之後執行
  • AnimatorSet.before(Animator anim) : 將現有動畫插入到傳入的動畫之前執行

3.5 Evaluator估值器

表示計算某個時間點,動畫需要更新 view 的值。

Evaluator.evaluate(float fraction, T startValue, T endValue) 是核心方法。其中,fraction 表示一個百分比。startValue 和 endValue 表示動畫的起始值和結束值。通過 fraction、startValue、endValue 計算 view 對應的屬性位置。

常用的就那麼幾個:

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(animationView, "X", 0, 500); objectAnimator.setInterpolator(new LinearInterpolator()); objectAnimator.setEvaluator(new FloatEvaluator()); objectAnimator.setDuration(5 * 1000); objectAnimator.start();

3.6 簡單Demo,

實現開始隱藏在螢幕頂部,已動畫的形式慢慢返回:

``` text.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onGlobalLayout() { text.getViewTreeObserver().removeOnGlobalLayoutListener(this); textHeight = text.getHeight(); Log.e("tag", "textHeight: "+textHeight);

        //一開始需要先讓text往上移動它自身的高度
        ViewHelper.setTranslationY(text, -textHeight);
        Log.e("tag", "top:"+text.getTop());
            //再以動畫的形式慢慢滾動下拉
        text.animate(text).translationYBy(textHeight)
            .setDuration(500)
            .setStartDelay(1000)
            .start();

```

屬性動畫設定控制元件的高度,實現動畫關閉和開啟的效果:

``` private boolean isOpen = false;

/**
 * 狀態的開關。上下關閉的屬性動畫
 */
private void toggle() {
    ValueAnimator animator = null;
    if (isOpen) {
        isOpen = false;
        //開啟屬性動畫
        animator = ValueAnimator.ofInt(mDesHeight, 0);
    } else {
        isOpen = true;
        animator = ValueAnimator.ofInt(0, mDesHeight);
    }


    //動畫的過程監聽
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            Integer height = (Integer) valueAnimator.getAnimatedValue();
            mParams.height = height;
            llDesRoot.setLayoutParams(mParams);
        }
    });
    //設定動畫的狀態監聽。給小箭頭設定狀態
    animator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animator) {
        }


        @Override
        public void onAnimationEnd(Animator animator) {
             //結束的時候,更換小箭頭的圖片
            if (isOpen){
                ivArrow.setImageResource(R.drawable.arrow_up);
            }else {
                ivArrow.setImageResource(R.drawable.arrow_down);
            }
        }


        @Override
        public void onAnimationCancel(Animator animator) {
        }


        @Override
        public void onAnimationRepeat(Animator animator) {


        }
    });

    animator.setDuration(200);  //動畫時間
    animator.start();           //啟動
}

```

屬性動畫講的好亂,太多了,比較複雜。後面會有更詳細的程式碼!

④過渡動畫

4.1 Android5.0以前的過渡動畫

同樣可以在xml中定義 ,也可以使用java程式碼控制

我們在style資料夾中定義

```

<style name="My_AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
    <item name="android:activityOpenEnterAnimation">@anim/open_enter</item>
    <item name="android:activityCloseExitAnimation">@anim/close_exit</item>
</style>

<!--上下進出場的activity動畫-->
<style name="up_down_activity_anim" mce_bogus="1" parent="@android:style/Animation.Activity">
    <item name="android:activityOpenEnterAnimation">@anim/open_up</item>
    <item name="android:activityCloseExitAnimation">@anim/close_down</item>
</style>

```

定義的檔案如下,補間動畫的方式:

```

<translate
    android:duration="270"
    android:fromXDelta="100%p"
    android:toXDelta="0%p" />

<translate
    android:duration="270"
    android:fromXDelta="0%p"
    android:toXDelta="-100%p" />

```

對應的Activity實現指定的樣式即可實現。

在Java檔案中同樣可以通過 overridePendingTransition 來實現。

大致實現如下:

``` startActivity(intent); overridePendingTransition(R.anim.bottom_top_anim, R.anim.alpha_hide);

finish(); overridePendingTransition(R.anim.alpha_show, R.anim.top_bottom_anim); ```

4.2 Android5.0以後的過渡動畫

5.0之後,Android就自帶幾種動畫特效。 3種轉場動畫 ,1種共享元素。

三種轉場動畫如下:

``` @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void explode(View view) { intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 0);

    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());


}


@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void slide(View view) {
    intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 1);

    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());


}


@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void fade(View view) {
    intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 2);

    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

}

```

通過對面的頁面來指定實現的方式:

``` @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);

     getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);


    int flag = getIntent().getExtras().getInt("flag");


    switch (flag) {
        case 0:
            //分解效果 上面的上面消失  下面的下面消失  分解掉了
            getWindow().setEnterTransition(new Explode());

            break;
        case 1:
            //滑動效果 預設上下滑動
            getWindow().setEnterTransition(new Slide());

            break;
        case 2:
            //淡出效果  透明度
            getWindow().setEnterTransition(new Fade());
            getWindow().setExitTransition(new Fade());

            break;
        case 3:
            break;
    }

    setContentView(R.layout.activity_transition);

}

```

5.0的Share共享動畫:

跳轉的方法:

``` @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void share(View view) { View fab = findViewById(R.id.fab_button); intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 3);

    //建立單個共享

// startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, view, "share") // .toBundle());

    //建立多個共享
    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, Pair.create
            (view, "share"),
            Pair.create(fab,"fab"))
            .toBundle());

}

```

share的方式,不需要對方頁面接收設定過渡動畫,而是需要在xml中配置transitionName屬性:

<View android:background="?android:colorPrimary" android:id="@+id/holder_view" android:transitionName="share" android:layout_width="match_parent" android:layout_height="300dp"/>

那邊是一個button 共享名字叫“share” 那邊是拿到的view 不是button 轉過來定義的是view

那邊共享的是button 共享名字叫tab 共享過來也定義的button。

如果Share動畫 想Share一個ViewGroup怎麼辦?比如一個Item跳轉到Detail頁面 可以直接使用這種過渡效果。

``` private void toActivity(View sharedElement) { Intent intent = new Intent(getContext(), TimeTableAcivity.class); ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(getActivity(), sharedElement, "shared_element_end_root"); startActivity(intent, options.toBundle()); } @Override protected void onCreate(Bundle savedInstanceState) { getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); findViewById(android.R.id.content).setTransitionName("shared_element_end_root"); setEnterSharedElementCallback(new MaterialContainerTransformSharedElementCallback()); getWindow().setSharedElementEnterTransition(buildContainerTransform(true)); getWindow().setSharedElementReturnTransition(buildContainerTransform(false)); super.onCreate(savedInstanceState); }

private MaterialContainerTransform buildContainerTransform(boolean entering) {
    MaterialContainerTransform transform = new MaterialContainerTransform(this, entering);

    transform.setAllContainerColors(
            MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface));
    transform.addTarget(android.R.id.content);
    //設定動畫持續時間(毫秒)
    transform.setDuration(666);
    return transform;
}

```

5.0之後在MD中還有其他的動畫,比如揭露動畫,不知道算不算轉場動畫的一種。因為一般也是用於轉場的時候使用,但是這個動畫我們使用的很少很少。

簡單的使用如下:

``` View myView = findView(R.id.awesome_card);

int cx = (myView.getLeft() + myView.getRight()) / 2;
int cy = (myView.getTop() + myView.getBottom()) / 2;


int dx = Math.max(cx, myView.getWidth() - cx);
int dy = Math.max(cy, myView.getHeight() - cy);
float finalRadius = (float) Math.hypot(dx, dy);

Animator animator =
        ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(1500);
animator.start();

```

這些動畫雖然牛皮,但是記得5.0以上才生效的哦,同時我們也不能看著什麼動畫炫酷都想上,轉場動畫也是在主執行緒執行的,如果定義不當也會造成卡頓的。

⑤非同步動畫

在子執行緒中執行動畫?我懂了,看我操作!

``` Thread { val animatorscaleX = ObjectAnimator.ofFloat(mBinding.ivAnim, "scaleX", 2f) val animatorscaleY = ObjectAnimator.ofFloat(mBinding.ivAnim, "scaleY", 2f) val animatortranslationX = ObjectAnimator.ofFloat(mBinding.ivAnim, "translationX", 200f) val animatortranslationY = ObjectAnimator.ofFloat(mBinding.ivAnim, "translationY", 200f)

    val set = AnimatorSet()
    set.setDuration(1000).play(animatorscaleX).with(animatorscaleY).with(animatortranslationX).with(animatortranslationY)
    set.start()
    }.start()

```

開個執行緒,執行屬性動畫。 so easy! 等等,怎麼寫個屬性動畫這麼多程式碼,修改一下,優雅一點,同樣的效果一行程式碼解決。

Thread { mBinding.ivAnim.animate().scaleX(2f).scaleY(2f).translationX(200f).translationY(200f).setDuration(1000).start() }.start()

執行!

image.png

居然報錯?不能執行在沒有looper的子執行緒?哦...我懂了,子執行緒不能更新UI來著。

到此就引出一個經典面試題,子執行緒真的不能更新UI嗎?當然可以更新UI了。看我操作!

``` public class MyLooperThread extends Thread {

// 子執行緒的looper
private Looper myLooper;
// 子執行緒的handler
private Handler mHandler;

// 用於測試的textview
private TextView testView;

private Activity activity;

public Looper getLooper() {
    return myLooper;
}

public Handler getHandler() {
    return mHandler;
}

public MyLooperThread(Context context, TextView view) {
    this.activity = (Activity) context;
    testView = view;
}

@Override
public void run() {
    super.run();
    // 呼叫了此方法後,當前執行緒擁有了一個looper物件
    Looper.prepare();
    YYLogUtils.w("訊息迴圈開始");

    if (myLooper == null) {
        while (myLooper == null) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 呼叫此方法獲取當前執行緒的looper物件
            myLooper = Looper.myLooper();
        }
    }

    // 當前handler與當前執行緒的looper關聯
    mHandler = new Handler(myLooper) {
        @Override
        public void handleMessage(Message msg) {
            YYLogUtils.w("處理訊息:" + msg.obj);

            //此執行緒,此Looper建立的ui可以隨便修改
            addTextViewInChildThread().setText(String.valueOf(msg.obj));

            //發現跟ui建立的位置有關。如果ui是在main執行緒建立的,則在子執行緒中不可以更改此ui;
            // 如果在含有looper的子執行緒中建立的ui,則可以任意修改
            // 這裡傳進來的是主執行緒的ui,不能修改!低版本可能可以修改
            //CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

// try { // if (testView != null) { // testView.setText(String.valueOf(msg.obj)); // } // } catch (Exception e) { // e.printStackTrace(); // // } } }; Looper.loop(); YYLogUtils.w("looper訊息迴圈結束,執行緒終止"); }

/**
 * 建立TextView
 */
private TextView addTextViewInChildThread() {
    TextView textView = new TextView(activity);

    textView.setBackgroundColor(Color.GRAY);  //背景灰色
    textView.setGravity(Gravity.CENTER);  //居中展示
    textView.setTextSize(20);

    WindowManager windowManager = activity.getWindowManager();
    WindowManager.LayoutParams params = new WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            0, 0,
            WindowManager.LayoutParams.FIRST_SUB_WINDOW,
            WindowManager.LayoutParams.TYPE_TOAST,
            PixelFormat.TRANSPARENT);
    windowManager.addView(textView, params);

    return textView;
}

} ```

我們需要定義執行緒,然後準備Looper,並建立內部的Handler處理資料。我們內部執行緒建立TextView,我們傳送handle訊息建立textview並賦值。

``` val looperThread = MyLooperThread(this, mBinding.tvRMsg) looperThread.start()

    mBinding.ivAnim.click {

        looperThread.handler.obtainMessage(200, "test set tv'msg").sendToTarget()

    }

```

效果:

image.png

正常顯示子執行緒建立的textview,但是我們傳入執行緒物件的tvRMsg是不能在子執行緒賦值的,會報錯:

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

結論:如果ui是在main執行緒建立的,則在子執行緒中不可以更改此ui; 如果在含有looper的子執行緒中建立的ui,則可以任意修改。

既然子執行緒都可以更新UI了,那麼子執行緒執行動畫行不行? 當然行!

我們直接修改程式碼:

``` val looperThread = MyLooperThread(this, mBinding.tvRMsg) looperThread.start()

    mBinding.ivAnim.click {

        //試試子執行緒執行動畫看看
        looperThread.handler.post {
            mBinding.ivAnim.animate().scaleX(2f).scaleY(2f).translationX(200f).translationY(200f).setDuration(1000).start()
        }

    }

```

完美執行!

其實官方早有說明,RenderThread 中執行動畫。其實我們上面的Thread類就是仿 HandlerThread 來寫的。我們可以使用 HandlerThread 很方便的實現子執行緒動畫。具體的使用方式和我們自定義的 Thread 類似。

我們可以基於系統類 HandlerThread 封裝一個非同步動畫工具類:

class AsynAnimUtil private constructor() : LifecycleObserver { ​ private var mHandlerThread: HandlerThread? = HandlerThread("anim_run_in_thread") ​ private var mHandler: Handler? = mHandlerThread?.run { start() Handler(this.looper) } ​ private var mOwner: LifecycleOwner? = null private var mAnim: ViewPropertyAnimator? = null ​ companion object { val instance: AsynAnimUtil by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { AsynAnimUtil() } } ​ //啟動動畫 fun startAnim(owner: LifecycleOwner?, animator: ViewPropertyAnimator) { try { if (mOwner != owner) { mOwner = owner addLoopLifecycleObserver() } ​ if (mHandlerThread?.isAlive != true) { YYLogUtils.w("handlerThread restart") mHandlerThread = HandlerThread("anim_run_in_thread") mHandler = mHandlerThread?.run { start() Handler(this.looper) } } ​ mHandler?.post { mAnim = animator.setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) destory() } ​ override fun onAnimationCancel(animation: Animator?) { super.onAnimationCancel(animation) destory() } ​ override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) { super.onAnimationEnd(animation, isReverse) destory() } }) mAnim?.start() } ​ } catch (e: Exception) { e.printStackTrace() } ​ } ​ // 綁定當前頁面生命週期 private fun addLoopLifecycleObserver() { mOwner?.lifecycle?.addObserver(this) } ​ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { YYLogUtils.i("AsynAnimUtil Lifecycle -> onDestroy") mAnim?.cancel() destory() } ​ private fun destory() { YYLogUtils.w("handlerThread quit") ​ try { mHandlerThread?.quitSafely() ​ mAnim = null mOwner = null mHandler = null mHandlerThread = null } catch (e: Exception) { e.printStackTrace() } } ​ }

使用的時候就可以直接拿工具類來進行非同步動畫

mBinding.ivAnim.click { ​ //試試HandlerThread執行動畫 val anim = mBinding.ivAnim.animate() .scaleX(2f) .scaleY(2f) .translationXBy(200f) .translationYBy(200f) .setDuration(2000) ​ AsynAnimUtil.instance.startAnim(this, anim) ​ }

Ok,完美執行。這裡注意需要傳入LifecycleOwner 為了在當前頁面關閉的時候及時的停止動畫釋放資源。

總結與其他動畫效果

網上也有很多開源的第三方的動畫框架,如gif動畫 lottie動畫 mp4動畫 Leonids粒子動畫 SVGA動畫 SurfaceView執行緒動畫 Motion動畫 VAP動畫 等等太多了。這裡就不做過多的展開。如果大家有興趣可以自行搜尋哦!

16.startService與bindService的區別

區別:

  1. bindService繫結Activity可以和Service進行互動,而startService不可以
  2. startService啟動的服務只可以通過stopService進行停止,bindService啟動的無法用stopService停止
  3. bindService啟動的服務只能通過unBindService方法進行停止

17.Service保活方式

最近開發了個內部即時通訊的app,可以說是真的蛋疼了,我幾乎把整個保活的文章全部看了一遍,可以說android界真的是特別的魚龍混雜。很多文章都寫得很片面,容易形成很大的誤導。我先說一個最近研究得出來的結論,在7.0或之後的版本,包括三星和國內的這些原生rom,如果不通過使用者或廠家設定,至少service是絕對沒有任何辦法保活的,絕對,除非你還能找到未知的漏洞。雖然我也很頭疼,但我真的很贊同谷歌這樣的做法,不然天天收推送通知真的是噁心得不行,IOS也得手動去關。現在android可以說是將一切統統殺掉,然後把僅有的一絲許可權,給予使用者去設定,甚至app中連彈出授權的api都不會給你。最後也提供下目前訊息實時push的解決方案。

  • jni保活,在5.0以前,android系統本身是不管理jni層的,所以用linux那套fork機制,可以讓程序和app分開,就算關閉app也不會影響到。所以那時很多人說android非常的卡,幸運的是我那段時間用的ios,這些程序連使用者都沒法關掉,真的特別噁心

  • jobservice和alarmmanager,在5.0之後,連native層也會受到系統限制,比如之前可用的jni包活方式,也就是說無論是殺掉還是凍結,都只和你啟動的第一個程序有關,後面不再以父子的關係去看待,而是以歷史同組的關係去看待。這個歷史同組真的是一個很關鍵的改變,其實不止killgroup的作用性,無論怎樣寫程式碼,都脫離不了系統對你app的控制,比如讓你何時休眠之類的。說了這麼多,那麼jobservice和alarmmanager到底有什麼用呢,其實在整個體系下,就可以看做是一個智慧省電的定時器,其他沒有任何特殊功能,和執行緒一樣會受到執行管控

  • doze,doze是在android6.0的時候出現的,作用在鎖屏之後,對app就行一系列的管理,可以說doze是一種底層機制。感覺doze還是很友好的,比如說提供白名單api、延遲執行的操作等等。就是說你每個app都會給你機會去發通知之類的,並且如果在app中授權ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,doze就不會再去管你,也就是如果即時通訊的話,加doze白名單後,如果沒有monitor,就可以一直在後臺刷資料

  • monitor,monitor可以看成是對doze的一種增強實現,也叫做廠商白名單,並且是對手機系統管理的一種實現,在絕大部分7.0的rom中都會有這東西,國產那幾乎就是100%有了,可以看做是一個root許可權的app。很多app之前一進去就彈窗進doze白名單,這樣後臺執行的app多起來又卡得不行,android又得背上這大鍋。monitor就預設只允許白名單中的app後臺執行和自啟動,就算在doze白名單中也不行,並且不提供任何彈窗api授權,必須由使用者在設定中手動加入白名單,或者找廠商給你加進去,比如QQ、微信、支付寶、搜狗這些

最後總結一下,那麼從android7.0之後,到現在的android12,如何實現後臺訊息實時push呢?

  • 引導使用者操作,同時加入doze白名單和monitor白名單,必須兩個都加入,app將一直在後臺執行,那麼就可以保持tcp的連線,就可以實時獲取到伺服器資料。切記發心跳,需要使用智慧省電的定時器~~

  • 和廠商對接,用錢解決一切,讓廠商給你加doze和monitor白名單

  • 通過廠商的api,對接推送,這種和ios思路一樣,比如目前的華為、小米都有開發實時推送api,比ios噁心的就是需要對接多套。

這是目前實踐過能用的方案,其他什麼亂七八糟,利用漏洞和系統搶實間的這些就不說了,基本都被堵死了

18.泛型

①為什麼需要泛型?

  • 擁有不同引數型別卻有相同的執行流程的方法,需要使用泛型;

  • 指定資料型別,可以在編譯期間發現型別錯誤,也不需要進行強制型別轉換;

②泛型類和泛型方法、泛型介面的定義

泛型類

public class A<T>{private T data; public A(T d){} public T getData(){return data;}……}

泛型介面:

``` public interface impl{public T method();}//定義1

class impls implements impl{}//呼叫1,也是泛型

class impls implements impl{public String method();}//呼叫2,指定了具體型別 ```

泛型方法: (完全獨立,不一定要宣告在泛型類和泛型介面中)

``` public T method(T,……) {} 泛型方法的標誌

class.method();//呼叫1

class.method();//呼叫2 ```

③泛型方法辨析

正確判斷泛型方法: 開頭

④限定型別

extends :指定泛型類派生於哪個類/介面

public class

public T method(T a,V b);

類和介面混用,類要放在開頭,有且只能有1個類

⑤泛型中的約束和侷限性

  • 不能例項化型別變數:new T()// 不行

  • 靜態域和靜態方法裡不能引用型別變數 private statc T instance;// 不行,因為在物件建立的時候,才知道T的具體型別

  • 靜態方法本身可以是泛型方法

  • 泛型只能是類,不能用基礎型別,可以用包裝類

  • 泛型不支援instanceof

  • 泛型的原生型別,型別不會因為T的改變而改變:Test<T> 的物件 t(String)t(Float) 的型別是一樣的

  • 泛型可以宣告陣列,卻不能new:

    Test

    Test[] arrays;//可以

    Test[] arrays = new Test[10];//不可以

  • 泛型類不能夠extends ExceptionThrowable,try……catch不能夠捕獲泛型類物件,但可以捕獲Throwable

    public void doWork(T x)

    {

        try{}catch(T x){}//不行
    
        try{} catch(Throwable e) throw T{throw t;}//可以
    

    }

⑥泛型型別的繼承規則

⑦萬用字元型別

解決繼承規則中C和C沒有任何關係的侷限。

``` class A;

  method(A<? extends/super B>)

  ? extends B:主要用於安全的訪問資料,訪問型別B

```

限定了泛型型別的上限;必須派生B的派生類;呼叫時增加

get一定是B;set不能用。

限定了泛型型別的下限;必須是B的超類;

設定只能設定本身和子類,返回只能返回Object,主要用於安全的寫入資料

⑧虛擬機器如何實現泛型

型別擦除 T 擦除成Object,T extends A,擦除成A(第一個)。實現介面時,在適當的位置加強制型別轉化

過載時,泛型引數的型別不通過。

19.重寫equals方法需要重寫hashCode方法嗎

需要。

①我們為什麼需要重寫hashCode()方法和equals()方法?(Why)

有時在我們的業務系統中判斷物件時有時候需要的不是一種嚴格意義上的相等,而是一種業務上的物件相等。在這種情況下,原生的equals方法就不能滿足我們的需求了.

我們所知道的JavaBean的超類(父類)是Object類,JavaBean中的equals方法是繼承自Object中的方法.Object類中定義的equals()方法是用來比較兩個引用所指向的物件的記憶體地址是否一致.並不是比較兩個物件的屬性值是否一致,所以這時我們需要重寫equals()方法.

Object類中equals()方法的原始碼

``` public boolean equals(Object obj) {

   return (this == obj);

}

public class Demo { public static void main(String[] args) { Student stu1 = new Student("awu",22); Student stu2 = new Student("awu",22); System.out.println(stu1.equals(stu2)); /因為Student這個JavaBean沒有重寫關於屬性值相等的equals()方法 ,所以預設比較的是地址值,從而輸出結果為false/
} } ```

那麼為什麼在重寫equals方法的時候需要重寫hashCode方法呢?

主要是Object.hashCode的通用約定:

  • 在java應用程式執行時,無論何時多次呼叫同一個物件時的hsahCode()方法,這個物件的hashCode()方法的返回值必須是相同的一個int值.
  • 如果兩個物件equals()返回值為true,則他們的hashCode()也必須返回相同的int值.
  • 如果兩個物件equals()返回值為false,則他們的hashCode()返回值也必須不同.

以HashSet來說明為什麼要這麼約定:HashSet存放元素時,根據元素的hashCode值快速找到要儲存的位置,如果這個位置有元素,兩個物件通過equals()比較,如果返回值為true,則不放入;如果返回值為false,則這個時候會以連結串列的形式在同一個位置上存放兩個元素,這會使得HashSet的效能降低,因為不能快速定位了。

還有一種情況就是兩個物件的hashCode()返回值不同,但是equals()返回true,這個時候HashSet會把這兩個物件都存進去,這就和Set集合不重複的規則相悖了;所以,我們重寫了equals()方法時,要按照b,c規則重寫hashCode()方法!(其實就是如果只重寫了 equals 方法,兩個物件 equals 返回了true,但是如果沒有重寫 hashCode 方法,集合還是會插入元素。這樣集合中就出現了重複元素了。)

②在什麼情況下需要重寫hashCode()方法和equals()方法? (When)

當我們自定義的一個類,想要把它的例項儲存在以Hash雜湊查詢的集合中時,我們就需要重寫這兩個方法;

```

public class Student { private String name;

private Integer age;

public Student(){

}

public Student(String name,Integer age){ this.name = name; this.age = age; }

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public Integer getAge() { return age; }

public void setAge(Integer age) { this.age = age; }

@Override
public int hashCode(){
final int prime = 31;
int result = 17;
result = prime * result + name.hashCode();
result = prime * result + age;
return result;
}

@Override  
public boolean equals(Object obj){  
    if(this == obj)  
        return true;  
    if(obj == null)  
        return false;  
    if(getClass() != obj.getClass())  
        return false;  
    final Student other = (Student)obj;  
    if(name.equals(other.name)){  
        return false;  
    }  
    if(age.equals(other.age)){  
        return false;  
    }  
    return true;  
}

}

public class Demo { public static void main(String[] args) { Student stu1 = new Student("awu",22); Student stu3 = new Student("awu",33); Student stu2 = new Student("awu",22);

Set set = new HashSet(); set.add(stu1); set.add(stu2); set.add(stu3);

System.out.println(set.size()); /輸出結果為2/

} } ```

如果不是以Hash雜湊查詢的集合,即使重寫HashCode也沒多大實際用處.比如如下栗子:

``` public class Demo { public static void main(String[] args) { Student stu1 = new Student("awu",22); Student stu3 = new Student("awu",33); Student stu2 = new Student("awu",22);

ArrayList list = new ArrayList(); list.add(stu1); list.add(stu2); list.add(stu3);

System.out.println(list .size()); /輸出結果為3/

} } ```

③如何重寫這兩個方法?

``` public class Student { private String name;

private Integer age;

public Student(){

}

public Student(String name,Integer age){ this.name = name; this.age = age; }

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public Integer getAge() { return age; }

public void setAge(Integer age) { this.age = age; }

 @Override  
    public int hashCode(){  
        final int prime = 31;  
        int result = 17;  
        result = prime * result + name.hashCode();  
        result = prime * result + age;  
        return result;  
    }

    @Override  
    public boolean equals(Object obj){  
        if(this == obj)  
            return true;  
        if(obj == null)  
            return false;  
        if(getClass() != obj.getClass())  
            return false;  
            final Student other = (Student)obj;  
        if(name.equals(other.name)){  
            return false;  
        }  
        if(age.equals(other.age)){  
            return false;  
        }  
        return true;  
}

} ```

一共50W字的文件,面試專題12W字只是一小部分,字數限制,分幾篇更。

關注公眾號:初一十五a

提前解鎖 《整套50W字Android體系PDF》,讓學習更貼近未來實戰。

總共囊括1.騰訊Android開發筆記(33W字)

2.2022最新Android十一位大廠面試專題(12W字)

3.音視訊經典面試題(6W字)

4.Jetpack全家桶

5.Android 效能監控框架Matrix

6.JVM

7.車載應用開發

《2022Android十一位大廠面試真題》 結合之前的 《騰訊Android開發筆記》 也算是雙管齊下了!😃