主頁 > 移動端開發 > Android面試題收錄及解答10月刊

Android面試題收錄及解答10月刊

2020-10-26 11:05:05 移動端開發

前言

嗨,大家好,好久不見,這里跟大家侃侃這中間發生了什么,

一個月前呢,想準備面試,就網上隨便找找面試題什么的,發現要么就是賣課的,要么就是不給詳細回答的或者回答不夠深的(也許是我沒找到??),反正稍微有點苦惱,因為我畢竟是個懶人,就想看看面試題,然后自己思考下,順便看看一些參考回答,看看自己回答的全不全面等等,

于是,我就想干脆我自己做這個事吧,就算沒人看,也當我自己每天復習下了,于是,我就建了一個小小公眾號(小到確實沒人看,哈哈哈),每天去找一些大廠的面試真題,然后解答下,然后自己確實也在這個程序中能復習到不少以前沒有重視的問題,今天就總結下之前一個多月總結的面試題,難度不大,大佬可以直接路過,當然發發善心點個贊也是可以的??,

進入正題,下面為10月刊內容,每三個問題為一個小節,也就是一個專題文章,我就不具體區分了,由于字數問題,也只節選了一些問題,大家見諒,另外答的不好的地方大家也可以留言敲敲我,感謝,

10月刊內容

網頁中輸入url,到渲染整個界面的整個程序,以及中間用了什么協議?

1)程序分析:主要分為三步

  • DNS決議,用戶輸入url后,需要通過DNS決議找到域名對應的ip地址,有了ip地址才能找到服務器端,首先會查找瀏覽器快取,是否有對應的dns記錄,再繼續按照作業系統快取—路由快取—isp的dns服務器—根服務器的順序進行DNS決議,直到找到對應的ip地址,
  • 客戶端(瀏覽器)和服務器互動,瀏覽器根據決議到的ip地址和埠號發起HTTP請求,請求到達傳輸層,這里也就是TCP層,開始三次握手建立連接,服務器收到請求后,發送相應報文給客戶端(瀏覽器),客戶端收到相應報文并進行決議,得到html頁面資料,包括html,js,css等,
  • 客戶端(瀏覽器)決議html資料,構建DOM樹,再構造呈現樹(render樹),最侄訓制到瀏覽器頁面上,

2)其中涉及到TCP/IP協議簇,包括DNS,TCP,IP,HTTP協議等等,

具體介紹下TCP/IP

TCP/IP一般指的是TCP/IP協議簇,主要包括了多個不同網路間實作資訊傳輸涉及到的各種協議
主要包括以下幾層:

  • 應用層:主要提供資料和服務,比如HTTP,FTP,DNS等
  • 傳輸層:負責資料的組裝,分塊,比如TCP,UDP等
  • 網路層:負責告訴通信的目的地,比如IP等
  • 資料鏈路層:負責連接網路的硬體部分,比如以太網,WIFI等

TCP的三次握手和四次揮手,為什么不是兩次握手?為什么揮手多一次呢?

客戶端簡稱A,服務器端簡稱B
1)TCP建立連接需要三次握手

  • A向B表示想跟B進行連接(A發送syn包,A進入SYN_SENT狀態)
  • B收到訊息,表示我也準備好和你連接了(B收到syn包,需要確認syn包,并且自己也發送一個syn包,即發送了syn+ack包,B進入SYN_RECV狀態)
  • A收到訊息,并告訴B表示我收到你也準備連接的信號了(A收到syn+ack包,向服務器發送確認包ack,AB進入established狀態)開始連接,

2)TCP斷開連接需要四次揮手

  • A向B表示想跟B斷開連接(A發送fin,進入FIN_WAIT_1狀態)
  • B收到訊息,但是B訊息沒發送完,只能告訴A我收到你的斷開連接訊息(B收到fin,發送ack,進入CLOSE_WAIT狀態)
  • 過一會,B資料發送完畢,告訴A,我可以跟你斷開了(B發送fin,進入LAST_ACK狀態)
  • A收到訊息,告訴B,可以他斷開(A收到fin,發送ack,B進入closed狀態)

3)為什么揮手多一次
其實正常的斷開和連接都是需要四次

  • A發訊息給B
  • B反饋給A表示正確收到訊息
  • B發送訊息給A
  • A反饋給B表示正確收到訊息,

但是連接中,第二步和第三步是可以合并的,因為連接之前A和B是無聯系的,所以沒有其他情況需要處理,而斷開的話,因為之前兩端是正常連接狀態,所以第二步的時候不能保證B之前的訊息已經發送完畢,所以不能馬上告訴A要斷開的訊息,這就是連接為什么可以少一步的原因,

4)為什么連接需要三次,而不是兩次,
正常來說,我給你發訊息,你告訴我能收到,不就代表我們之前通信是正常的嗎?

  • 簡單回答就是,TCP是雙向通信協議,如果兩次握手,不能保證B發給A的訊息正確到達,

TCP 協議為了實作可靠傳輸, 通信雙方需要判斷自己已經發送的資料包是否都被接收方收到, 如果沒收到, 就需要重發,

TCP是怎么保證可靠傳輸的?

  • 序列號和確認號,比如連接的一方發送一段80byte資料,會帶上一個序列號,比如101,接收方收到資料,回復確認號181(180+1),這樣下一次發送訊息就會從181開始發送了,

所以握手程序中,比如A發送syn信號給B,初始序列號為120,那么B收到訊息,回復ack訊息,序列號為120+1,同時B發送syn信號給A,初始序列號為256,如果收不到A的回復訊息,就會重發,否則丟失這個序列號,就無法正常完成后面的通信了,

這就是三次握手的原因,

TCP和UDP的區別?

TCP提供的是面向連接,可靠的位元組流服務,即客戶和服務器交換資料前,必須現在雙方之間建立一個TCP連接(三次握手),之后才能傳輸資料,并且提供超時重發,丟棄重復資料,檢驗資料,流量控制等功能,保證資料能從一端傳到另一端,

UDP 是一個簡單的面向資料報的運輸層協議,它不提供可靠性,只是把應用程式傳給IP層的資料報發送出去,但是不能保證它們能到達目的地,由于UDP在傳輸資料報前不用再客戶和服務器之間建立一個連接,且沒有超時重發等機制,所以傳輸速度很快,

所以總結下來就是:

  • TCP 是面向連接的,UDP 是面向無連接的
  • TCP資料報頭包括序列號,確認號,等等,相比之下UDP程式結構較簡單,
  • TCP 是面向位元組流的,UDP 是基于資料報的
  • TCP 保證資料正確性,UDP 可能丟包
  • TCP 保證資料順序,UDP 不保證

可以看到TCP適用于穩定的應用場景,他會保證資料的正確性和順序,所以一般的瀏覽網頁,介面訪問都使用的是TCP傳輸,所以才會有三次握手保證連接的穩定性,
而UDP是一種結構簡單的協議,不會考慮丟包啊,建立連接等,優點在于資料傳輸很快,所以適用于直播,游戲等場景,

HTTP的幾種請求方法具體介紹

常見的有四種:

  • GET 獲取資源,沒有body,冪等性
  • POST 增加或者修改資源,有body
  • PUT 修改資源,有body,冪等性
  • DELETE 洗掉資源,冪等性

HTTP請求和回應報文的格式,以及常用狀態碼

1)請求報文:

   //請求行(包括method、path、HTTP版本)
   GET /s HTTP/1.1
   //Headers
   Host: www.baidu.com
   Content-Type: text/plain
   //Body
   搜索****

2)回應報文

   //狀態行 (包括HTTP版本、狀態碼,狀態資訊)
   HTTP/1.1 200 OK
   //Headers
   Content-Type: application/json; charset=utf-8
   //Body
   [{"info":"xixi"}]

3)常用狀態碼

主要分為五種型別:

  • 1開頭, 代表臨時性訊息,比如100(繼續發送)
  • 2開頭, 代表請求成功,比如200(OK)
  • 3開頭, 代表重定向,比如304(內容無改變)
  • 4開頭, 代表客戶端的一些錯誤,比如403(禁止訪問)
  • 5開頭, 代表服務器的一些錯誤,比如500

介紹對稱加密和非對稱加密

1)對稱加密,即加密和解密演算法不同,但是密鑰相同,比如DES,AES演算法,

資料A --> 演算法D(密鑰S)--> 加密資料B
加密資料B --> 演算法E(密鑰S)--> 資料A

優點:
缺點:密鑰有可能被破解,容易被偽造,傳輸程序中一旦密鑰被其他人獲知則可以進行資料解密,

2)非對稱加密,即加密和解密演算法相同,但是密鑰不同,私鑰自己保存,公鑰提供給對方,比如RSA,DSA演算法,

資料A --> 演算法D(公鑰)--> 加密資料B
加密資料B --> 演算法D(私鑰)--> 資料A

優點:安全,公鑰即使被其他人獲知,也無法解密資料,
缺點:需要通信雙方都有一套公鑰和私鑰

數字簽名的原理

1)首先,為什么需要數字簽名?
防止被攻擊,被偽造,由于公鑰是公開的,別人截獲到公鑰就能偽造資料進行傳輸,所以我們需要驗證資料的來源,

2)怎么簽名?
由于公鑰能解密 私鑰加密的資料,所以私鑰也能解密 公鑰加密的資料,(上圖非對稱加密A和B代號互換即可)
所以我們用公鑰進行加密后,再用私鑰進行一次加密,那么私鑰的這次加密就叫簽名,也就是只有我自己可以進行加密的操作,所以傳輸資料流程就變成了加密資料和簽名資料,如果解出來都是同樣的資料,那么則資料安全可靠

資料A --> 演算法D(公鑰)--> 加密資料B
資料A --> 演算法D(私鑰)--> 簽名資料C

加密資料B --> 演算法D(私鑰)--> 資料A
簽名資料C --> 演算法D(公鑰)--> 資料A

Base64演算法是什么,是加密演算法嗎?

  • Base64是一種將二進制資料轉換成64種字符組成的字串的編碼演算法,主要用于非文本資料的傳輸,比如圖片,可以將圖片這種二進制資料轉換成具體的字串,進行保存和傳輸,

  • 嚴格來說,不算,雖然它確實把一段二進制資料轉換成另外一段資料,但是他的加密和解密是公開的,也就無秘密可言了,所以我更傾向于認為它是一種編碼,每個人都可以用base64對二進制資料進行編碼和解碼,

  • 面試加分項:為了減少混淆,方便復制,減少資料長度,就衍生出一種base58編碼,去掉了base64中一些容易混淆的數字和字母(數字0,字母O,字母I,數字1,符號+,符號/)
    大名鼎鼎的位元幣就是用的改進后的base58編碼,即Base58Check編碼方式,有了校驗機制,加入了hash值,

為什么多執行緒同時訪問(讀寫)同個變數,會有并發問題?

  • Java 記憶體模型規定了所有的變數都存盤在主記憶體中,每條執行緒有自己的作業記憶體,
  • 執行緒的作業記憶體中保存了該執行緒中用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在作業記憶體中進行,而不能直接讀寫主記憶體,
  • 執行緒訪問一個變數,首先將變數從主記憶體拷貝到作業記憶體,對變數的寫操作,不會馬上同步到主記憶體,
  • 不同的執行緒之間也無法直接訪問對方作業記憶體中的變數,執行緒間變數的傳遞均需要自己的作業記憶體和主存之間進行資料同步

說說原子性,可見性,有序性分別是什么意思?

  • 原子性:在一個操作中,CPU 不可以在中途暫停然后再調度,即不被中斷操作,要么執行完成,要么就不執行,

  • 可見性:多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值,

  • 有序性:程式執行的順序按照代碼的先后順序執行,

實際專案程序中,有用到多執行緒并發問題的例子嗎?

有,比如單例模式
由于單例模式的特殊性,可能被程式中不同地方多個執行緒同時呼叫,所以為了避免多執行緒并發問題,一般要采用volatile+Synchronized的方式進行變數,方法保護,

    private volatile static Singleton singleton;

    public static Singleton getSingleton4() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }

        }
        return singleton;
    }

介紹幾種啟動模式,

  • standard,默認模式,每次啟動都會新建一個Activity實體,并進入當前任務堆疊
  • singleTop,如果要啟動的Activity在堆疊頂存在實體,則不會重新創建Activity,而是直接使用堆疊頂的Activity實體,并回呼onNewIntent方法,
  • singleTask,如果要啟動的Activity在堆疊中存在實體,則不會重新創建Activity,而是直接使用堆疊里的Activity實體,并回呼onNewIntent方法,并且會把這個實體放到堆疊頂,之前在這個Activity之上的都會被出堆疊銷毀,
  • singleInstance,有點單例的感覺,就是所啟動的Activity會單獨放在一個任務堆疊里,并且后續所有啟動該Activity都會直接用這個實體,同樣被重復呼叫的時候會呼叫并回呼onNewIntent方法,

Activity依次A→B→C→B,其中B啟動模式為singleTask,AC都為standard,生命周期分別怎么呼叫?如果B啟動模式為singleInstance又會怎么呼叫?B啟動模式為singleInstance不變,A→B→C的時候點擊兩次回傳,生命周期如何呼叫,

1)A→B→C→B,B啟動模式為singleTask

  • 啟動A的程序,生命周期呼叫是 (A)onCreate→(A)onStart→(A)onResume
  • 再啟動B的程序,生命周期呼叫是 (A)onPause→(B)onCreate→(B)onStart→(B)onResume→(A)onStop
  • B→C的程序同上
  • C→B的程序,由于B啟動模式為singleTask,所以B會呼叫onNewIntent,并且將B之上的實體移除,也就是C會被移出堆疊,所以生命周期呼叫是 (C)onPause→(B)onNewIntent→(B)onRestart→(B)onStart→(B)onResume→(C)onStop→(C)onDestory

2)A→B→C→B,B啟動模式為singleInstance

  • 如果B為singleInstance,那么C→B的程序,C就不會被移除,因為B和C不在一個任務堆疊里面,所以生命周期呼叫是 (C)onPause→(B)onNewIntent→(B)onRestart→(B)onStart→(B)onResume→(C)onStop

3)A→B→C,B啟動模式為singleInstance,點擊兩次回傳鍵

  • 如果B為singleInstance,A→B→C的程序,生命周期還是同前面一樣正常呼叫,但是點擊回傳的時候,由于AC同任務堆疊,所以C點擊回傳,會回到A,再點擊回傳才回到B,所以生命周期是:(C)onPause→(A)onRestart→(A)onStart→(A)onResume→(C)onStop→(C)onDestory,

  • 再次點擊回傳,就會回到B,所以生命周期是:(A)onPause→(B)onRestart→(B)onStart→(B)onResume→(A)onStop→(A)onDestory

螢屏旋轉時Activity的生命周期,如何防止Activity重建,

  • 切換螢屏的生命周期是:onConfigurationChanged->onPause->onSaveInstanceState->onStop->onDestroy->onCreate->onStart->onRestoreInstanceState->onResume

  • 如果需要防止旋轉時候,Activity重新創建的話需要做如下配置:
    targetSdkVersion的值小于或等于12時,配置 android:configChanges="orientation",
    targetSdkVersion的值大于12時,配置 android:configChanges="orientation|screenSize",

執行緒的三種啟動方式

1)繼承thread類

    class MyThread :Thread(){
        override fun run() {
            super.run()
        }
    }

    fun test(){
        var t1=MyThread()
        t1.start()
    }

2)實作runnable介面

    class MyRunnable : Runnable {
        override fun run() {

        }
    }

    fun test() {
        var t1 = Thread(MyRunnable(),"test")
        t1.start()
    }

3)實作 Callable 介面

    class MyCallThread : Callable<String> {
        override fun call(): String {
            return "i got it"
        }

    }

    fun test() {
        var task = FutureTask(MyCallThread())
        var t1 = Thread(task, "test")
        t1.start()
        try {
            //獲取結果
            var result = task.get()
        } catch (e: Exception) {
        }
    }

也有人表示其實是兩個方法,因為第三個方法FutureTask也是實作了Runnable的方法,只不過表現方法不一樣,然后帶回傳值,這個大家面試的時候可以都說上,然后說說自己的見解,畢竟要讓面試官多多看到你的知識面,

執行緒run和start的區別

  • start方法,用start方法來啟動執行緒,真正實作了多執行緒運行,這時無需等待run方法體中的代碼執行完畢而直接繼續執行后續的代碼,通過呼叫Thread類的 start()方法來啟動一個執行緒,這時此執行緒處于就緒(可運行)狀態,并沒有運行,一旦得到cpu時間片,就開始執行run()方法,這里的run()方法 稱為執行緒體,它包含了要執行的這個執行緒的內容,Run方法運行結束,此執行緒隨即終止,

  • run方法,run方法只是類的一個普通方法而已,如果直接呼叫Run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是只有一條,還是要順序執行,還是要等待run方法體執行完畢后才可繼續執行下面的代碼,這樣就沒有達到寫執行緒的目的,

簡單的說就是:
呼叫start方法方可啟動執行緒,而run方法只是thread類中的一個普通方法呼叫,不用啟動新執行緒,還是在主執行緒里執行,

執行緒的幾種狀態,相互之間是如何轉化的

1) 初始狀態(New),新創建了一個執行緒物件就進入了初始狀態,也就是通過上述新建執行緒的幾個方法就能進入該狀態,

2) 可運行狀態,就緒狀態(RUNNABLE),執行緒物件創建后,其他執行緒(比如main執行緒)呼叫了該物件的start()方法,該狀態的執行緒位于可運行執行緒池中,等待被執行緒調度選中,獲取cpu 的使用權,以下幾種方式會進入可運行狀態:

  • 呼叫start方法,
  • 拿到物件鎖
  • 呼叫yield方法

3)運行狀態(RUNNING),可運行狀態(runnable)的執行緒獲得了cpu 時間片 ,執行程式代碼,執行緒調度程式從可運行池中選擇一個執行緒作為當前執行緒,就會進入運行狀態,

4)阻塞狀態(BLOCKED),執行緒正在運行的時候,被暫停,通常是為了等待某個時間的發生(比如說某項資源就緒)之后再繼續運行,wait,sleep,suspend等方法都可以導致執行緒阻塞,

5)死亡狀態(DEAD),執行緒run()、main() 方法執行結束,或者因例外退出了run()方法,則該執行緒結束生命周期,死亡的執行緒不可再次復生,

String是java中的基本資料型別嗎?是可變的嗎?是執行緒安全的嗎?

  • String不是基本資料型別,java中把大資料型別是:byte, short, int, long, char, float, double, boolean
  • String是不可變的
  • String是不可變類,一旦創建了String物件,我們就無法改變它的值,因此,它是執行緒安全的,可以安全地用于多執行緒環境中

為什么要設計成不可變的呢?如果String是不可變的,那我們平時賦值是改的什么呢?

1)為什么設計不可變

  • 安全,由于String廣泛用于java類中的引數,所以安全是非常重要的考慮點,包括執行緒安全,打開檔案,存盤資料密碼等等,
  • String的不變性保證哈希碼始終一,所以在用于HashMap等類的時候就不需要重新計算哈希碼,提高效率
  • 因為java字串是不可變的,可以在java運行時節省大量java堆空間,因為不同的字串變數可以參考池中的相同的字串,如果字串是可變得話,任何一個變數的值改變,就會反射到其他變數,那字串池也就沒有任何意義了,

2)平時使用雙引號方式賦值的時候其實是回傳的字串參考,并不是改變了這個字串物件

淺談一下String, StringBuffer,StringBuilder的區別?String的兩種創建方式,在JVM的存盤方式相同嗎?

String是不可變類,每當我們對String進行操作的時候,總是會創建新的字串,操作String很耗資源,所以Java提供了兩個工具類來操作String - StringBuffer和StringBuilder

StringBuffer和StringBuilder是可變類,StringBuffer是執行緒安全的,StringBuilder則不是執行緒安全的,所以在多執行緒對同一個字串操作的時候,我們應該選擇用StringBuffer,由于不需要處理多執行緒的情況,StringBuilder的效率比StringBuffer高,

1) String常見的創建方式有兩種

  • String s1 = “Java”
  • String s2 = new String("Java")

2)存盤方式不同

  • 第一種,s1會先去字串常量池中找字串"Java”,如果有相同的字符則直接回傳常量句柄,如果沒有此字串則會先在常量池中創建此字串,然后再回傳常量句柄,或者說字串參考,

  • 第二種,s2是直接在堆上創建一個變數物件,但不存盤到字串池 ,呼叫intern方法才會把此字串保存到常量池中

執行緒池是干嘛的,優點有哪些?

執行緒池主要用作管理子執行緒,優點有:

  • 重用執行緒池中的執行緒,避免頻繁創建和銷毀執行緒所帶來的記憶體開銷
  • 有效控制執行緒的最大并發數,避免因執行緒之間搶占資源而導致的阻塞現象
  • 能夠對執行緒進行簡單的管理,提供定時執行以及指定時間間隔回圈執行等功能,

執行緒池的構造方法每個引數是什么意思,執行任務的流程

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}
  • corePoolSize:核心執行緒數,默認情況下執行緒池是空的,只是任務提交時才會創建執行緒,如果當前運行的執行緒數少于corePoolSize,則會創建新執行緒來處理任務;如果等于或者等于corePoolSize,則不再創建,如果呼叫執行緒池的prestartAllcoreThread方法,執行緒池會提前創建并啟動所有的核心執行緒來等待任務,
  • maximumPoolSize:執行緒池允許創建的最大執行緒數,如果任務佇列滿了并且執行緒數小于maximumPoolSize時,則執行緒池仍然會創建新的執行緒來處理任務,
  • keepAliveTime:非核心執行緒閑置的超時事件,超過這個事件則回收,如果任務很多,并且每個任務的執行時間很短,則可以調大keepAliveTime來提高執行緒的利用率,另外,如果設定allowCoreThreadTimeOut屬性來true時,keepAliveTime也會應用到核心執行緒上,
  • TimeUnit:keepAliveTime引數的時間單位,可選的單位有天Days、小時HOURS、分鐘MINUTES、秒SECONDS、毫秒MILLISECONDS等,
  • workQueue:任務佇列,如果當前執行緒數大于corePoolSzie,則將任務添加到此任務佇列中,該任務佇列是BlockingQueue型別的,即阻塞佇列,
  • ThreadFactory:執行緒工廠,可以使用執行緒工廠給每個創建出來的執行緒設定名字,一般情況下無須設定該引數,
  • RejectedExecutionHandler:拒絕策略,這是當前任務佇列和執行緒池都滿了時所采取的應對策略,默認是AbordPolicy,表示無法處理新任務,并拋出RejectedExecutionException例外,

其中,拒絕策略有四種:

  • AbordPolicy:無法處理新任務,并拋出RejectedExecutionException例外,
  • CallerRunsPolicy:用呼叫者所在的執行緒來處理任務,此策略提供簡單的反饋控制機制,能夠級訓新任務的提交速度,
  • DiscardPolicy:不能執行的任務,并將該任務洗掉,
  • DiscardOldestPolicy:丟棄佇列最近的任務,并執行當前的任務,

執行任務流程:

  • 如果執行緒池中的執行緒數量未達到核心執行緒的數量,會直接啟動一個核心執行緒來執行任務,
  • 如果執行緒池中的執行緒數量已經達到或者超過核心執行緒的數量,那么任務會被插入到任務佇列中排隊等待執行,
  • 如果任務佇列無法插入新任務,說明任務佇列已滿,如果未達到規定的最大執行緒數量,則啟動一個非核心執行緒來執行任務,
  • 如果執行緒數量超過規定的最大值,則執行拒絕策略-RejectedExecutionHandler,

Android執行緒池主要分為哪幾類,分別代表了什么?

主要有四類:FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledTheadPool

1) FixedThreadPool——可重用固定執行緒數的執行緒池

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • 執行緒數量固定且都是核心執行緒:核心執行緒數量和最大執行緒數量都是nThreads;
  • 都是核心執行緒且不會被回收,快速相應外界請求;
  • 沒有超時機制,任務佇列也沒有大小限制;
  • 新任務使用核心執行緒處理,如果沒有空閑的核心執行緒,則排隊等待執行,
  1. CachedThreadPool——按需創建的執行緒池
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 執行緒數量不定,只有非核心執行緒,最大執行緒數任意大:傳入核心執行緒數量的引數為0,最大執行緒數為Integer.MAX_VALUE;
  • 有新任務時使用空閑執行緒執行,沒有空閑執行緒則創建新的執行緒來處理,
  • 該執行緒池的每個空閑執行緒都有超時機制,時常為60s(引數:60L, TimeUnit.SECONDS),空閑超過60s則回收空閑執行緒,
  • 適合執行大量的耗時較少的任務,當所有執行緒閑置超過60s都會被停止,所以這時幾乎不占用系統資源,
  1. SingleThreadExecutor——單執行緒的執行緒池
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • 只有一個核心執行緒,所有任務在同一個執行緒按順序執行,
  • 所有的外界任務統一到一個執行緒中,所以不需要處理執行緒同步的問題,
  1. ScheduledThreadPool——定時和周期性的執行緒池
    private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
  • 核心執行緒數量固定,非核心執行緒數量無限制
  • 非核心執行緒閑置超過10s會被回收;
  • 主要用于執行定時任務和具有固定周期的重復任務;

索引是什么,優缺點

資料庫索引,是資料庫管理系統中一個排序的資料結構,以協助快速查詢,更新資料庫中表的資料.索引的實作通常使用B樹和變種的B+樹(mysql常用的索引就是B+樹)

優點

  • 通過創建索引,可以在查詢的程序中,提高系統的性能
  • 通過創建唯一性索引,可以保證資料庫表中每一行資料的唯一性
  • 在使用分組和排序子句進行資料檢索時,可以減少查詢中分組和排序的時間

缺點

  • 創建索引和維護索引要耗費時間,而且時間隨著資料量的增加而增大
  • 索引需要占用物理空間,如果要建立聚簇索引,所需要的空間會更大
  • 在對表中的資料進行增加洗掉和修改時需要耗費較多的時間,因為索引也要動態地維護

事務四大特性

資料庫事務必須具備ACID特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔離性)和Durability(持久性)的英文縮寫,

  • 原子性

一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節,事務在執行程序中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣,

  • 一致性

事務的一致性指的是在一個事務執行之前和執行之后資料庫都必須處于一致性狀態,如果事務成功地完成,那么系統中所有變化將正確地應用,系統處于有效狀態,如果在事務中出現錯誤,那么系統中的所有變化將自動地回滾,系統回傳到原始狀態,

  • 隔離性

指的是在并發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間,由并發事務所做的修改必須與任何其他并發事務所做的修改隔離,事務查看資料更新時,資料所處的狀態要么是另一事務修改它之前的狀態,要么是另一事務修改它之后的狀態,事務不會查看到中間狀態的資料,

  • 持久性

指的是只要事務成功結束,它對資料庫所做的更新就必須永久保存下來,即使發生系統崩潰,重新啟動資料庫系統后,資料庫還能恢復到事務成功結束時的狀態,

講講幾個范式

范式的英文名稱是Normal Form,它是英國人E.F.Codd(關系資料庫的老祖宗)在上個世紀70年代提出關系資料庫模型后總結出來的,范式是關系資料庫理論的基礎,也是我們在設計資料庫結構程序中所要遵循的規則和指導方法,通常所用到的只是前三個范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)

  • 第一范式就是屬性不可分割,每個欄位都應該是不可再拆分的,比如一個欄位是姓名(NAME),在國內的話通常理解都是姓名是一個不可再拆分的單位,這時候就符合第一范式;但是在國外的話還要分為FIRST NAME和LAST NAME,這時候姓名這個欄位就是還可以拆分為更小的單位的欄位,就不符合第一范式了,

  • 第二范式就是要求表中要有主鍵,表中其他其他欄位都依賴于主鍵,因此第二范式只要記住主鍵約束就好了,比如說有一個表是學生表,學生表中有一個值唯一的欄位學號,那么學生表中的其他所有欄位都可以根據這個學號欄位去獲取,依賴主鍵的意思也就是相關的意思,因為學號的值是唯一的,因此就不會造成存盤的資訊對不上的問題,即學生001的姓名不會存到學生002那里去,

  • 第三范式就是要求表中不能有其他表中存在的、存盤相同資訊的欄位,通常實作是在通過外鍵去建立關聯,因此第三范式只要記住外鍵約束就好了,比如說有一個表是學生表,學生表中有學號,姓名等欄位,那如果要把他的系編號,系主任,系主任也存到這個學生表中,那就會造成資料大量的冗余,一是這些資訊在系資訊表中已存在,二是系中有1000個學生的話這些資訊就要存1000遍,因此第三范式的做法是在學生表中增加一個系編號的欄位(外鍵),與系資訊表做關聯,

Recycleview和listview區別

  • Recycleview布局效果更多,增加了縱向,表格,瀑布流等效果
  • Recycleview去掉了一些api,比如setEmptyview,onItemClickListener等等,給到用戶更多的自定義可能
  • Recycleview去掉了設定頭部底部item的功能,專向通過viewholder的不同type實作
  • Recycleview實作了一些區域重繪,比如notifyitemchanged
  • Recycleview自帶了一些布局變化的影片效果,也可以通過自定義ItemAnimator類實作自定義影片效果
  • Recycleview快取機制更全面,增加兩級快取,還支持自定義快取邏輯

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,

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

  1. 保存快取流程:
  • 插入或是洗掉itemView時,先把螢屏內的ViewHolder保存至AttachedScrap
  • 滑動螢屏的時候,先消失的itemview會保存到CacheView,CacheView大小默認是2,超過數量的話按照先入先出原則,移出頭部的itemview保存到RecyclerPool快取池(如果有自定義快取就會保存到自定義快取里),RecyclerPool快取池會按照itemview的itemtype進行保存,每個itemTyep快取個數為5個,超過就會被回收,
  1. 獲取快取流程:
  • AttachedScrap中獲取,通過pos匹配holder——>獲取失敗,從CacheView中獲取,也是通過pos獲取holder快取
    ——>獲取失敗,從自定義快取中獲取快取——>獲取失敗,從mRecyclerPool中獲取
    ——>獲取失敗,重新創建viewholder——createViewHolder并bindview,

需要注意的是,如果從快取池找到快取,還需要重新bindview,

說說RecyclerView性能優化,

  • bindViewHolder方法是在UI執行緒進行的,此方法不能耗時操作,不然將會影響滑動流暢性,比如進行日期的格式化,
  • 對于新增或洗掉的時候,可以使用diffutil進行區域重繪,少用全域重繪
  • 對于itemVIew進行布局優化,比如少嵌套等,
  • 25.1.0 (>=21)及以上使用 Prefetch 功能,也就是預取功能,嵌套時且使用的是LinearLayoutManager,子RecyclerView可通過setInitialPrefatchItemCount設定預取個數
  • 加大RecyclerView快取,比如cacheview大小默認為2,可以設定大點,用空間來換取時間,提高流暢度
  • 如果高度固定,可以設定setHasFixedSize(true)來避免requestLayout浪費資源,否則每次更新資料都會重新測量高度,
void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}
  • 如果多個 RecycledView 的 Adapter 是一樣的,比如嵌套的 RecyclerView 中存在一樣的 Adapter,可以通過設定 RecyclerView.setRecycledViewPool(pool); 來共用一個 RecycledViewPool,這樣就減少了創建VIewholder的開銷,
  • 在RecyclerView的元素比較高,一屏只能顯示一個元素的時候,第一次滑動到第二個元素會卡頓,這種情況就可以通過設定額外的快取空間,重寫getExtraLayoutSpace方法即可,
new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        return size;
    }
};
  • 設定RecyclerView.addOnScrollListener();來在滑動程序中停止加載的操作,
  • 減少物件的創建,比如設定監聽事件,可以全域創建一個,所有view公用一個listener,并且放到CreateView里面去創建監聽,因為CreateView呼叫要少于bindview,這樣就減少了物件創建所造成的消耗
  • notifyDataSetChange時,配接器不知道整個資料集中的那些內容以及存在,再重新匹配ViewHolder時會花生閃爍,設定adapter.setHasStableIds(true),并重寫getItemId()來給每個Item一個唯一的ID,也就是唯一標識,就使itemview的焦點固定,解決了閃爍問題,

說說雙重校驗鎖,以及volatile的作用

先回顧下雙重校驗鎖的原型,也就是單例模式的實作:

public class Singleton {
    private volatile static Singleton mSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == mSingleton) {
            synchronized (Singleton.class) {
                if (null == mSingleton) {
                    mSingleton = new Singleton();
                }
            }
        }
        return mSingleton;
    }
}

有幾個疑問需要解決:

  • 為什么要加鎖?
  • 為什么不直接給getInstance方法加鎖?
  • 為什么需要雙重判斷是否為空?
  • 為什么還要加volatile修飾變數?

接下來一一解答:

  • 如果不加鎖的話,是執行緒不安全的,也就是有可能多個執行緒同時訪問getInstance方法會得到兩個實體化的物件,
  • 如果給getInstance方法加鎖,就每次訪問mSingleton都需要加鎖,增加了性能開銷
  • 第一次判空是為了判斷是否已經實體化,如果已經實體化就直接回傳變數,不需要加鎖了,第二次判空是因為走到加鎖這一步,如果執行緒A已經實體化,等B獲得鎖,進入的時候其實物件已經實體化完成了,如果不二次判空就會再次實體化
  • 加volatile是為了禁止指令重排,指令重排指的是在程式運行程序中,并不是完全按照代碼順序執行的,會考慮到性能等原因,將不影響結果的指令順序有可能進行調換,所以初始化的順序本來是這三步:
    1)分配記憶體空間
    2)初始化物件
    3)將物件指向分配的空間

如果進行了指令重排,由于不影響結果,所以2和3有可能被調換,所以就變成了:

1)分配記憶體空間
2)將物件指向分配的空間
3)初始化物件

就有可能會導致,假如執行緒A中已經進行到第二步,執行緒B進入第二次判空的時候,判斷mSingleton不為空,就直接回傳了,但是實際此時mSingleton還沒有初始化,

synchronized和volatile的區別

  • volatile本質是在告訴jvm當前變數在暫存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住.
  • volatile僅能使用在變數級別,synchronized則可以使用在變數,方法.
  • volatile僅能實作變數的修改可見性,而synchronized則可以保證變數的修改可見性和原子性.
  • volatile不會造成執行緒的阻塞,而synchronized可能會造成執行緒的阻塞.
  • 當一個域的值依賴于它之前的值時,volatile就無法作業了,如n=n+1,n++等,也就是不保證原子性,
  • 使用volatile而不是synchronized的唯一安全的情況是類中只有一個可變的域,

synchronized修飾static方法和修飾普通方法有什么區別

  • Synchronized修飾非靜態方法,實際上是對呼叫該方法的物件加鎖,俗稱“物件鎖”,也就是鎖住的是這個物件,即this,如果同一個物件在兩個執行緒分別訪問物件的兩個同步方法,就會產生互斥,這就是物件鎖,一個物件一次只能進入一個操作,

  • Synchronized修飾靜態方法,實際上是對該類物件加鎖,俗稱“類鎖”,也就是鎖住的是這個類,即xx.class,如果一個物件在兩個執行緒中分別呼叫一個靜態同步方法和一個非靜態同步方法,由于靜態方法會收到類鎖限制,但是非靜態方法會收到物件限制,所以兩個方法并不是同一個物件鎖,因此不會排斥,

記憶體泄漏是什么,為什么會發生?

記憶體泄漏(Memory Leak)是指程式中己動態分配的堆記憶體由于某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式運行速度減慢甚至系統崩潰等嚴重后果,
簡單點說,手機給我們的應用提供了一定大小的堆記憶體,在不斷創建物件的程序中,也在不斷的GC(java的垃圾回識訓制),所以記憶體正常情況下會保持一個平穩的值,
但是出現記憶體泄漏就會導致某個實體,比如Activity的實體,應用被某個地方參考到了,不能正常釋放,從而導致記憶體占用越來越大,這就是記憶體泄漏

記憶體泄漏發生的情況有哪些?

主要有四類情況

  • 集合類泄漏
  • 單例/靜態變數造成的記憶體泄漏
  • 匿名內部類/非靜態內部類
  • 資源未關閉造成的記憶體泄漏

1)集合類泄漏

集合類添加元素后,仍參考著集合元素物件,導致該集合中的元素物件無法被回收,從而導致記憶體泄露,

static List<Object> mList = new ArrayList<>();
   for (int i = 0; i < 100; i++) {
       Object obj = new Object();
      mList.add(obj);
       obj = null;
    }

解決辦法就是把集合也釋放掉,

  mList.clear();
  mList = null;

2)單例/靜態變數造成的記憶體泄漏

單例模式具有其靜態特性,它的生命周期等于應用程式的生命周期,正是因為這一點,往往很容易造成記憶體泄漏,


public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context;
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}


比如這個單例模式,如果我們呼叫newInstance方法時候把Activity的context傳進去,那么就是生命周期長的持有了生命周期短的參考,造成了記憶體泄漏,要修改的話把context改成context.getApplicationContext()即可,

3)匿名內部類/非靜態內部類

非靜態內部類他會持有他外部類的強參考,所以就有可能導致非靜態內部類的生命周期可能比外部類更長,容易造成記憶體泄漏,最常見的就是Handler

public class TestActivity extends Activity {
private TextView mText;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

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


        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    
        mHandler. sendEmptyMessageDelayed(0, 100000);
    }

怎么修改呢?改成靜態內部類,然后弱參考方式修飾外部類

public class TestActivity extends Activity {
    private TextView mText;
    private MyHandler myHandler = new MyHandler(TestActivity.this);
    private MyThread myThread = new MyThread();

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().mText.setText("do someThing");
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }

4)資源未關閉造成的記憶體泄漏

比如:

  • 網路、檔案等流忘記關閉
  • 手動注冊廣播時,退出時忘記unregisterReceiver()
  • Service 執行完后忘記 stopSelf()
  • EventBus 等觀察者模式的框架忘記手動解除注冊

該怎么發現和解決記憶體泄漏?

1、使用工具,比如Memory Profiler,可以查看app的記憶體實時情況,捕獲堆轉儲,就生成了一個記憶體快照,hprof檔案,通過查看檔案,可以看到哪些類發生了記憶體泄漏,

2、使用庫,比較出名的就是LeakCanary,匯入庫,然后運行后,就可以發現app內的記憶體泄漏情況,

這里說下LeakCanary的原理:

  • 監聽
    首先通過ActivityLifecycleCallbacksFragmentLifeCycleCallbacks監聽Activity和Fragment的生命周期,

  • 判斷
    然后在銷毀的生命周期中判斷物件是否被回收,弱參考在定義的時候可以指定參考物件和一個 ReferenceQueue,通過該弱參考是否被加入ReferenceQueue就可以判斷該物件是否被回收,

  • 分析
    最后通過haha庫來分析hprof檔案,從而找出類之前的參考關系,

什么是類加載機制?

我們撰寫的java檔案會在編譯后變成.class檔案,類加載器就是負責加載class位元組碼檔案,class檔案在檔案開頭有特定的檔案標識,將class檔案位元組碼內容加載到記憶體中,并將這些內容轉換成方法區中的運行時資料結構并且ClassLoader只負責class檔案的加載,至于它是否可以運行,則由執行引擎Execution Engine決定,

簡單來說類加載機制就是從檔案系統將一系列的 class 檔案讀入 JVM 記憶體中為后續程式運行提供資源的動作,

類加載器種類,

類加載器種類主要有四種:

  • BootstrapClassLoader:啟動類加載器,使用C++實作
  • ExtClassLoader:擴展類加載器,使用Java實作
  • AppClassLoader:應用程式類加載器,加載當前應用的classpath的所有類
  • UserDefinedClassLoader:用戶自定義類加載器

屬于依次繼承關系,也就是上一級是下一級的父加載器,

什么是雙親委派機制,為什么這么設計?

當一個類加載器收到了類加載的請求,它不會直接去加載這類,而是先把這個請求委派給父加載器去完成,依次會傳遞到最上級也就是啟動類加載器,然后父加載器會檢查是否已經加載過該類,如果沒加載過,就會去加載,加載失敗才會交給子加載器去加載,一直到最底層,如果都沒辦法能正確加載,則會跑出ClassNotFoundException例外,

舉例:

  • 當Application ClassLoader 收到一個類加載請求時,他首先不會自己去嘗試加載這個類,而是將這個請求委派給父類加載器Extension ClassLoader去完成,
  • 當Extension ClassLoader收到一個類加載請求時,他首先也不會自己去嘗試加載這個類,而是將請求委派給父類加載器Bootstrap ClassLoader去完成,
  • 如果Bootstrap ClassLoader加載失敗(在<JAVA_HOME>\lib中未找到所需類),就會讓Extension ClassLoader嘗試加載,
  • 如果Extension ClassLoader也加載失敗,就會使用Application ClassLoader加載,
  • 如果Application ClassLoader也加載失敗,就會使用自定義加載器去嘗試加載,
  • 如果均加載失敗,就會拋出ClassNotFoundException例外,

這么設計的原因是為了防止危險代碼的植入,比如String類,如果在AppClassLoader就直接被加載,就相當于會被篡改了,所以都要經過老大,也就是BootstrapClassLoader進行檢查,已經加載過的類就不需要再去加載了,

webView與js通信

1) Android呼叫JS代碼

主要有兩種方法:

  • 通過WebView的loadUrl()
// 呼叫javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");

但是這種不常用,因為它會自動重繪頁面而且沒有回傳值,有點影響互動,

  • 通過WebView的evaluateJavascript()
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此處為 js 回傳的結果
        }
    });

這種就比較全面了,呼叫方法并且獲取回傳值,

2) JS呼叫Android端代碼

主要有兩種方法:

  • 通過WebView的addJavascriptInterface()進行物件映射


public class AndroidtoJs extends Object {

    // 定義JS需要呼叫的方法
    // 被JS呼叫的方法必須加入@JavascriptInterface注解
    @JavascriptInterface
    public void hello(String msg) {
        System.out.println("JS呼叫了Android的hello方法");
    }
}

mWebView.addJavascriptInterface(new AndroidtoJs(), "test");


//js中:
function callAndroid(){
     // 由于物件映射,所以呼叫test物件等于呼叫Android映射的物件
     test.hello("js呼叫了android中的hello方法");
}

這種方法雖然很好用,但是要注意的是4.2以后,對于被呼叫的函式以@JavascriptInterface進行注解,否則容易出發漏洞,因為js方可以通過反射呼叫一些本地命令,很危險,

  • 通過 WebViewClient 的shouldOverrideUrlLoading ()方法回呼攔截 url

這種方法是通過shouldOverrideUrlLoading回呼去攔截url,然后進行決議,如果是之前約定好的協議,就呼叫相應的方法,

// 復寫WebViewClient類的shouldOverrideUrlLoading方法
mWebView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
			Uri uri = Uri.parse(url);                                 
            // 如果url的協議 = 預先約定的 js 協議
            if ( uri.getScheme().equals("js")) {
			// 如果 authority  = 預先約定協議里的 webview,即代表都符合約定的協議
            	if (uri.getAuthority().equals("webview")) {
            		System.out.println("js呼叫了Android的方法");
            		// 可以在協議上帶有引數并傳遞到Android上
            		HashMap<String, String> params = new HashMap<>();
            		Set<String> collection = uri.getQueryParameterNames();
				}
            	return true;
            }
            return super.shouldOverrideUrlLoading(view, url);
            }
        }
    );

如何避免WebView記憶體泄露

WebView的記憶體泄露主要是因為在頁面銷毀后,WebView的資源無法馬上釋放所導致的,現在主流的是兩種方法:

1)不在xml布局中添加webview標簽,采用在代碼中new出來的方式,并在頁面銷毀的時候去釋放webview資源

//addview
private WeakReference<BaseWebActivity> webActivityReference = new WeakReference<BaseWebActivity>(this);
mWebView = new BridgeWebView(webActivityReference .get());
webview_container.addView(mWebView);


//銷毀
ViewParent parent = mWebView.getParent();
if (parent != null) {
    ((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
mWebView=null;

2)另起一個行程加載webview,頁面銷毀后干掉這個行程,但是這個方法的麻煩之處就在于行程間通信

使用方法很簡單,xml檔案中寫出行程名即可,銷毀的時候呼叫System.exit(0)

<activity android:name=".WebActivity"
   android:process=":remoteweb"/>

System.exit(0)   

webView還有哪些可以優化的地方

  • 提前初始化或者使用全域WebView,首次初始化WebView會比第二次初始化慢很多,初始化后,即使WebView已釋放,但一些多WebView共用的全域服務/資源對想仍未釋放,而第二次初始化不需要生成,因此初始化變快,

  • DNS采用和客戶端API相同的域名,DNS決議也是耗時比較多的部分,所以用客戶端API相同的域名因為其DNS會被快取,所以打開webView的時候就不會再耗時在DNS上了

  • 對于JS的優化,盡量不要用偏重的框架,比如React,其次是高性能要求頁面還是需要后端渲染,最后就是app中的網頁框架要統一,這樣就可以對js進行快取和復用,

這里有美團團隊的總結方案,如下:

  • WebView初始化慢,可以在初始化同時先請求資料,讓后端和網路不要閑著,
  • 后端處理慢,可以讓服務器分trunk輸出,在后端計算的同時前端也加載網路靜態資源,
  • 腳本執行慢,就讓腳本在最后運行,不阻塞頁面決議,
  • 同時,合理的預加載、預快取可以讓加載速度的瓶頸更小,
  • WebView初始化慢,就隨時初始化好一個WebView待用,
  • DNS和鏈接慢,想辦法復用客戶端使用的域名和鏈接
  • 腳本執行慢,可以把框架代碼拆分出來,在請求頁面之前就執行好,

Activity、View、Window 之間的關系,

每個 Activity 包含了一個 Window 物件,這個物件是由 PhoneWindow 做的實作,而 PhoneWindowDecorView 作為了一個應用視窗的根 View,這個 DecorView 又把螢屏劃分為了兩個區域:一個是 TitleView,一個是ContentView,而我們平時在 Xml 檔案中寫的布局正好是展示在 ContentView 中的,

說說Android的事件分發機制完整流程,也就是從點擊螢屏開始,事件會怎么傳遞,

我覺得事件分發機制流程可以分為三部分,分別是從外傳里,從里傳外,消費之后

1)首先,從最外面一層傳到最里面一層:

如果當前是viewgroup層級,就會判斷 onInterceptTouchEvent 是否為true,如果為true,則代表事件要消費在這一層級,不再往下傳遞,接著便執行當前 viewgroup 的onTouchEvent方法,如果onInterceptTouchEvent為false,則代表事件繼續傳遞到下一層級的 dispatchTouchEvent方法,接著一樣的代碼邏輯,一直到最里面一層的view,

偽代碼解釋:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean isConsume = false;
    if (isViewGroup) {
        if (onInterceptTouchEvent(event)) {
            isConsume = onTouchEvent(event);
        } else {
            isConsume = child.dispatchTouchEvent(event);
        }

    } else {
        //isView
        isConsume = onTouchEvent(event);
    }
    return isConsume;
}

2)到最里層的view之后,view本身還是可以選擇消費或者傳到外面,

到最里面一層就會直接執行onTouchEvent方法,這時候,view有沒有權利拒絕消費事件呢? 按道理view作為最底層的,應該是沒有發言權才對,但是呢,秉著公平公正原則,view也是可以拒絕的,可以在onTouchEvent方法回傳false,表示他不想消費這個事件,那么它的父容器的onTouchEvent又會被呼叫,如果父容器的onTouchEvent又回傳false,則又交給上一級,一直到最上層,也就是Activity的onTouchEvent被呼叫,

偽代碼解釋:

public void handleTouchEvent(MotionEvent event) {
    if (!onTouchEvent(event)) {
        getParent.onTouchEvent(event);
    }
}

3)消費之后

當某一層viewGroup的onInterceptTouchEvent為true,則代表當前層級要消費事件,如果它的onTouchListener被設定了的話,則onTouch會被呼叫,如果onTouch的回傳值回傳true,則onTouchEvent不會被呼叫,如果回傳false或者沒有設定onTouchListener,則會繼續呼叫onTouchEvent,而onClick方法則是設定了onClickListener則會被正常呼叫,

偽代碼解釋:

public void consumeEvent(MotionEvent event) {
    if (setOnTouchListener) {
        int tag = onTouch();
        if (!tag) {
            onTouchEvent(event);
        }
    } else {
        onTouchEvent(event);
    }

    if (setOnClickListener) {
        onClick();
    }
}

解決滑動沖突的辦法,

解決滑動沖突的根本就是要在適當的位置進行攔截,那么就有兩種解決辦法:

  • 外部攔截:從父view端處理,根據情況決定事件是否分發到子view
  • 內部攔截:從子view端處理,根據情況決定是否阻止父view進行攔截,其中的關鍵就是requestDisallowInterceptTouchEvent方法,

1)外部攔截法,其實就是在onInterceptTouchEvnet方法里面進行判斷,是否攔截,見代碼:

    //外部攔截法:父view.java		
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        //父view攔截條件
        boolean parentCanIntercept;

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;

    }

還是比較簡單的,直接判斷攔截條件,然后回傳true就代表攔截,false就不攔截,傳到子view,注意的是ACTION_DOWN狀態不要攔截,如果攔截,那么后續事件就直接交給父view處理了,也就沒有攔截不攔截的問題了,

  1. 內部攔截法,就是通過requestDisallowInterceptTouchEvent方法讓父view不要攔截,
    //父view.java			
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

    //子view.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //父view攔截條件
        boolean parentCanIntercept;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

requestDisallowInterceptTouchEvent(true)的意思是阻止父view攔截事件,也就是傳入true之后,父view就不會再呼叫onInterceptTouchEvent,反之,傳入false就代表父view可以攔截,也就是會走到父view的onInterceptTouchEvent方法,所以需要父view攔截的時候,就傳入flase,需要父view不攔截的時候就傳入true,

Fragment生命周期,當hide,show,replace時候生命周期變化

1)生命周期:

  • onAttach():Fragment和Activity相關聯時呼叫,可以通過該方法獲取Activity參考,還可以通過getArguments()獲取引數,
  • onCreate():Fragment被創建時呼叫,
  • onCreateView():創建Fragment的布局,
  • onActivityCreated():當Activity完成onCreate()時呼叫,
  • onStart():當Fragment可見時呼叫,
  • onResume():當Fragment可見且可互動時呼叫,
  • onPause():當Fragment不可互動但可見時呼叫,
  • onStop():當Fragment不可見時呼叫,
  • onDestroyView():當Fragment的UI從視圖結構中移除時呼叫,
  • onDestroy():銷毀Fragment時呼叫,
  • onDetach():當Fragment和Activity解除關聯時呼叫,

每個呼叫方法對應的生命周期變化:

  • add(): onAttach()->…->onResume(),
  • remove(): onPause()->…->onDetach(),
  • replace(): 相當于舊Fragment呼叫remove(),新Fragment呼叫add(),remove()+add()的生命周期加起來
  • show(): 不呼叫任何生命周期方法,呼叫該方法的前提是要顯示的 Fragment已經被添加到容器,只是純粹把Fragment UI的setVisibility為true,
  • hide(): 不呼叫任何生命周期方法,呼叫該方法的前提是要顯示的Fragment已經被添加到容器,只是純粹把Fragment UI的setVisibility為false,

Activity 與 Fragment,Fragment 與 Fragment之間怎么互動通信,

  • Activity 與 Fragment通信

Activity有Fragment的實體,所以可以執行Fragment的方法,或者傳入一個介面,
同樣,Fragment可以通過getActivity()獲取Activity的實體,也是可以執行方法,

  • Fragment 與 Fragment之間通信

1)直接獲取另一個Fragmetn的實體

getActivity().getSupportFragmentManager().findFragmentByTag("mainFragment");

2)介面回呼
一個Fragment里面去實作介面,另一個Fragment把介面實體傳進去,

3)Eventbus等框架,

Fragment遇到viewpager遇到過什么問題嗎,

  • 滑動的時候,呼叫setCurrentItem方法,要注意第二個引數smoothScroll,傳false,就是直接跳到fragment,傳true,就是平滑過去,一般主頁切換頁面都是用false,

  • 禁止預加載的話,呼叫setOffscreenPageLimit(0)是無效的,因為方法里面會判斷是否小于1,需要重寫setUserVisibleHint方法,判斷fragment是否可見,

  • 不要使用getActivity()獲取activity實體,容易造成空指標,因為如果fragment已經onDetach()了,那么就會報空指標,所以要在onAttach方法里面,就去獲取activity的背景關系,

  • FragmentStatePagerAdapter對limit外的Fragment銷毀,生命周期為onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume,也就是說切換fragment的時候有可能會多次onCreateView,所以需要注意處理資料,

  • 由于可能多次onCreateView,所以我們可以把view保存起來,如果為空再去初始化資料,見代碼:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (null == mFragmentView) {
                mFragmentView = inflater.inflate(getContentViewLayoutID(), null);
                ButterKnife.bind(this, mFragmentView);
                isDestory = false;
                initViewsAndEvents();
            }
        return mFragmentView;
    }

ARouter的原理

首先,我們了解下ARouter是干嘛的?ARouter是阿里巴巴研發的一個用于解決組件間,模塊間界面跳轉問題的框架,
所以簡單的說,就是用來跳轉界面的,不同于平時用到的顯式或隱式跳轉,只需要在對應的界面上添加注解,就可以實作跳轉,看個案例:

@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

//跳轉
ARouter.getInstance().build("/test/activity").navigation();

使用很方便,通過一個path就可以進行跳轉了,那么原理是什么呢?

其實仔細思考下,就可以聯想到,既然關鍵跳轉程序是通過path跳轉到具體的activity,那么原理無非就是把pathActivity一一對應起來就行了,沒錯,其實就是通過注釋,通過apt技術,也就是注解處理工具,把path和activity關聯起來了,主要有以下幾個步驟:

  • 代碼里加入的@Route注解,會在編譯時期通過apt生成一些存盤path和activity.class映射關系的類檔案
  • app行程啟動的時候會加載這些類檔案,把保存這些映射關系的資料讀到記憶體里(保存在map里)
  • 進行路由跳轉的時候,通過build()方法傳入要到達頁面的路由地址,ARouter會通過它自己存盤的路由表找到路由地址對應的Activity.class
  • 然后new Intent方法,如果有呼叫ARouterwithString()方法,就會呼叫intent.putExtra(String name, String value)方法添加引數
  • 最后呼叫navigation()方法,它的內部會呼叫startActivity(intent)進行跳轉

ARouter怎么實作頁面攔截

先說一個攔截器的案例,用作頁面跳轉時候檢驗是否登錄,然后判斷跳轉到登錄頁面還是目標頁面:

   @Interceptor(name = "login", priority = 6)
    public class LoginInterceptorImpl implements IInterceptor {
        @Override
        public void process(Postcard postcard, InterceptorCallback callback) {
            String path = postcard.getPath();
            boolean isLogin = SPUtils.getInstance().getBoolean(ConfigConstants.SP_IS_LOGIN, false);
    
            if (isLogin) { 
                // 如果已經登錄不攔截
                callback.onContinue(postcard);
            } else {  
                // 如果沒有登錄,進行攔截
                callback.onInterrupt(postcard);
            }
    
        }
    
        @Override
        public void init(Context context) {
            LogUtils.v("初始化成功"); 
        }
    
    }

    //使用
    ARouter.getInstance().build(ConfigConstants.SECOND_PATH)
                             .withString("msg", "123")
                              .navigation(this,new LoginNavigationCallbackImpl()); 
                              // 第二個引數是路由跳轉的回呼
         
     
    // 攔截的回呼
    public class LoginNavigationCallbackImpl  implements NavigationCallback{
        @Override 
        public void onFound(Postcard postcard) {
    
        }
    
        @Override 
        public void onLost(Postcard postcard) {
    
        }
    
        @Override   
        public void onArrival(Postcard postcard) {
    
        }
    
        @Override
        public void onInterrupt(Postcard postcard) {
        	//攔截并跳轉到登錄頁
            String path = postcard.getPath();
            Bundle bundle = postcard.getExtras();
            ARouter.getInstance().build(ConfigConstants.LOGIN_PATH)
                    .with(bundle)
                    .withString(ConfigConstants.PATH, path)
                    .navigation();
        }
    }

攔截器實作IInterceptor介面,使用注解@Interceptor,這個攔截器就會自動被注冊了,同樣是使用APT技術自動生成映射關系類,這里還有一個優先級引數priority,數值越小,就會越先執行,

怎么應用到組件化中

首先,在公用組件的build.gradle中添加依賴:

dependencies {
    api 'com.alibaba:arouter-api:1.4.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
}

其次,必須在每個業務組件,也就是用到了arouter的組件中都宣告annotationProcessorOptions,否則會無法通過apt生成索引檔案,也就無法正常跳轉了:

//業務組件的build.gradle
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}
dependencies {
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation '公用組件'
}

這個arguments是用來設定給編譯處理器的一些引數,這里就把[AROUTER_MODULE_NAME: project.getName()]鍵值對傳了過去,方便Arouter使用apt的時候進行資料處理,也是Arouter庫所規定的配置,

然后就可以正常使用了,

說說你對協程的理解

在我看來,協程和執行緒一樣都是用來解決并發任務(異步任務)的方案,
所以協程和執行緒是屬于一個層級的概念,但是對于kotlin中的協程,又與廣義的協程有所不同,
kotlin中的協程其實是對執行緒的一種封裝,或者說是一種執行緒框架,為了讓異步任務更好更方便使用,

說下協程具體的使用

比如在一個異步任務需要回呼到主執行緒的情況,普通執行緒需要通過handler切換執行緒然后進行UI更新等,一旦多個任務需要順序呼叫,那更是很不方便,比如以下情況:

//客戶端順序進行三次網路異步請求,并用最終結果更新UI
thread{
	iotask1(parameter) { value1 ->
		iotask1(value1) { value2 ->
			iotask1(value2) { value3 ->
				runOnUiThread{
					updateUI(value3) 
				}      
		} 
	}              
}
}

簡直是魔鬼呼叫,如果不止3次,而是5次,6次,那還得了,,

而用協程就能很好解決這個問題:

//并發請求
GlobalScope.launch(Dispatchers.Main) {
    //三次請求并發進行
	val value1 = async { request1(parameter1) }
	val value2 = async { request2(parameter2) }
	val value3 = async { request3(parameter3) }
    //所有結果全部回傳后更新UI
	updateUI(value1.await(), value2.await(), value3.await())
}

//切換到io執行緒
suspend fun request1(parameter : Parameter){withContext(Dispatcher.IO){}}
suspend fun request2(parameter : Parameter){withContext(Dispatcher.IO){}}
suspend fun request3(parameter : Parameter){withContext(Dispatcher.IO){}}

就像是同一個執行緒中順序執行的效果一樣,再比如我要按順序執行一次異步任務,然后完成后更新UI,一共三個異步任務,
如果正常寫應該怎么寫?

thread{
	iotask1() { value1 ->
		runOnUiThread{
			updateUI1(value1) 
			iotask2() { value2 ->
			runOnUiThread{
				updateUI2(value2) 
				iotask3() { value3 ->
				runOnUiThread{
					updateUI3(value3) 
				} 
			    }   
			}      
			}	

		}
	}
}

暈了暈了,不就是一次異步任務,一次UI更新嗎,怎么這么麻煩,來,用協程看看怎么寫:

    GlobalScope.launch (Dispatchers.Main) {
        ioTask1()
        ioTask1()
        ioTask1()
        updateUI1()
        updateUI2()
        updateUI3()
    }

	suspend fun ioTask1(){
        withContext(Dispatchers.IO){}
    }
    suspend fun ioTask2(){
        withContext(Dispatchers.IO){}
    }
    suspend fun ioTask3(){
        withContext(Dispatchers.IO){}
    }

    fun updateUI1(){
    }
    fun updateUI2(){
    }
    fun updateUI3(){
    }

協程怎么取消

  • 取消協程作用域將取消它的所有子協程,
// 協程作用域 scope
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()
  • 取消子協程
// 協程作用域 scope
val job1 = scope.launch { … }
val job2 = scope.launch { … }
job1.cancel()

但是呼叫了cancel并不代表協程內的作業會馬上停止,他并不會組織代碼運行,
比如上述的job1,正常情況處于active 狀態,呼叫了cancel方法后,協程會變成Cancelling 狀態,作業完成之后會變成Cancelled 狀態,所以可以通過判斷協程的狀態來停止作業,

Jetpack 中定義的協程作用域(viewModelScope 和 lifecycleScope)可以幫助你自動取消任務,下次再詳細說明,其他情況就需要自行進行系結和取消了,

之前大家應該看過我寫的啟動流程分析了吧,那篇文章里我說過分析原始碼的目的一直都不是為了學知識而學,而是理解了這些基礎,我們才能更好的解決問題,所以今天就來看看通過分析app啟動流程,我們該怎么具體進行啟動優化,

  • App啟動流程中我們能進行優化的地方有哪些?
  • 具體有哪些優化方法?
  • 分析啟動耗時的方法

具體有哪些啟動優化方法?

  • 障眼法之閃屏頁

為了消除啟動時的白屏/黑屏,可以通過設定android:windowBackground,讓人感覺一點擊icon就啟動完畢了的感覺,

        <activity android:name=".ui.activity.啟動activity"
            android:theme="@style/MyAppTheme"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
    		<item name="android:windowBackground">@drawable/logo</item>
		</style>

  • 預創建Activity

物件第一次創建的時候,java虛擬機首先檢查類對應的Class 物件是否已經加載,如果沒有加載,jvm會根據類名查找.class檔案,將其Class物件載入,同一個類第二次new的時候就不需要加載類物件,而是直接實體化,創建時間就縮短了,

  • 第三方庫懶加載

很多第三方開源庫都說在Application中進行初始化,所以可以把一些不是需要啟動就初始化的三方庫的初始化放到后面,按需初始化,這樣就能讓Application變得更輕,

  • WebView啟動優化

webview第一次啟動會非常耗時,具體優化方法可以看我之前的文章,關于webview的優化,

  • 執行緒優化

執行緒是程式運行的基本單位,執行緒的頻繁創建是耗性能的,所以大家應該都會用執行緒池,單個cpu情況下,即使是開多個執行緒,同時也只有一個執行緒可以作業,所以執行緒池的大小要根據cpu個數來確定,

  • MultiDex 優化

由于65536方法限制,所以一般class檔案要生成多個dex檔案,Android5.0以下,ClassLoader加載類的時候只會從class.dex(主dex)里加載,所以要執行MultiDex.install(context)方法才能正常讀取dex類,

而這個install方法就是耗時大戶,會解壓apk,遍歷dex檔案,壓縮dex、將dex檔案通過反射轉換成DexFile物件、反射替換陣列,

這里需要的方案就是今日頭條方案:

1、在Application的attachBaseContext方法里,啟動另一個行程的LoadDexActivity去異步執行MultiDex邏輯,顯示Loading,
2、然后主行程Application進入while回圈,不斷檢測MultiDex操作是否完成
3、MultiDex執行完之后主行程Application繼續走,ContentProvider初始化和Application onCreate方法,也就是執行主行程正常的邏輯,

所以重點就是單開行程去執行MultiDex邏輯,這樣就不影響APP的啟動了,

分析啟動耗時的方法

  • Systrace + 函式插樁

也就是通過在方法的入口和出口加入統計代碼,從而統計方法耗時

class Trace{
    public static void i(String tag){
        android.os.Trace.beginSection(tag);
    }

    public static void o(){
        android.os.Trace.endSection();
    }
}


void test(){
    Trace.i("test");
    System.out.println("doSomething");
    Trace.o();
}
  • BlockCanary
    BlockCanary 可以監聽主執行緒耗時的方法,就是在主執行緒訊息回圈打出日志的地入手, 當一個訊息操作時間超過閥值后, 記錄系統各種資源的狀態, 并展示出來,所以我們將閾值設定低一點,這樣的話如果一個方法執行時間超過200毫秒,獲取堆疊資訊,

而記錄時間的方法我們之前也說過,就是通過looper()方法中回圈去從MessageQueue中去取msg的時候,在dispatchMessage方法前后會有logging日志列印,所以只需要自定義一個Printer,重寫println(String x)方法即可實作耗時統計了,

Activity、View、Window三者如何關聯?

Activity包含了一個PhoneWindow,而PhoneWindow就是繼承于Window的,Activity通過setContentView將View設定到了PhoneWindow上,而View通過WindowManager的addView()、removeView()、updateViewLayout()對View進行管理,Window的添加程序以及Activity的啟動流程都是一次IPC的程序,Activity的啟動需要通過AMS完成;Window的添加程序需要通過WindowSession完成,

onCreate,onResume,onStart里面,什么地方可以獲得寬高

如果在onCreate、onStart、onResume中直接呼叫View的getWidth/getHeight方法,是無法得到View寬高的正確資訊,因為view的measure程序與Activity的生命周期是不同步的,所以無法保證在這些生命周期里view
的measure已經完成,所以很有可能獲取的寬高為0,

所以主要有以下三個方法來獲取view的寬高:

  • view.post()方法

在該方法里的runnable物件,能保證view已經繪制完成,也就是執行完measure、layout和draw方法了,

        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getWidth();
                int hight = view.getHeight();
            }
        });
  • onWindowFocusChanged方法

Activity中可以重寫onWindowFocusChanged方法,該方法表示Activity的視窗得到焦點或者失去焦點的時候,所以Activitiy獲取焦點時,view肯定繪制完成了,這時候獲取寬高也是沒問題的:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width = view.getWidth();
            int hight = view.getHeight();
        }
    }
  • ViewTreeObserver注冊OnGlobalLayoutListener介面

ViewTreeObserver是一個觀察者,主要是用來觀察視圖樹的各種變化,OnGlobalLayoutListener的作用是當View樹的狀態發生改變或者View樹中某view的可見性發生改變時,OnGlobalLayoutListener的onGlobalLayout方法將會被回呼,因此,此時獲取view的寬高也是可以的,

        ViewTreeObserver observer = title_name.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
               int width = view.getWidth();
               int hight = view.getHeight();
            }
        });

為什么view.post可以獲得寬高,有看過view.post的原始碼嗎?

能獲取寬高的原因肯定就是因為在此之前view 繪制已經完成,所以View.post() 添加的任務能夠保證在所有 View 繪制流程結束之后才被執行,

看看post的原始碼:

 public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action); 
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().post(action);  
        return true;
    }

//RunQueue .class
     void post(Runnable action) {
            postDelayed(action, 0);
        }
 
        void postDelayed(Runnable action, long delayMillis) {
            HandlerAction handlerAction = new HandlerAction();
            handlerAction.action = action;
            handlerAction.delay = delayMillis;
 
            synchronized (mActions) {
                mActions.add(handlerAction);
            }
        }

        void executeActions(Handler handler) {
            synchronized (mActions) {
                final ArrayList<HandlerAction> actions = mActions;
                final int count = actions.size();
 
                for (int i = 0; i < count; i++) {
                    final HandlerAction handlerAction = actions.get(i);
                    handler.postDelayed(handlerAction.action, handlerAction.delay);
                }
 
                actions.clear();
            }
        }

所以在執行View.post()的方法時,那些Runnable并沒有馬上被執行,而是保存到RunQueue里面,然后通過executeActions方法執行,也就是通過handler,post了一個延時任務Runnable,而executeActions方法什么時候會執行呢?


private void performTraversals() {
     getRunQueue().executeActions(attachInfo.mHandler);
    ...
    performMeasure();
    ...
    performLayout();
    ...
    performDraw();
 }

可以看到在performTraversals方法中執行了,但是在view繪制之前,這是因為在繪制之前就把需要執行的runnable封裝成Message發送到MessageQueue里排隊了,但是Looper不會馬上去取這個訊息,因為Looper會按順序取訊息,主執行緒還有什么訊息沒執行完呢?其實就是當前的這個performTraversals所在的任務,所以要等下面的·performMeasure,performLayout,performDraw·都執行完,也就是view繪制完畢了,才會去執行之前我們post的那個runnable,也就是我們能在view.post方法里的runnable能獲取寬高的主要原因了,

SharedPreferences是如何保證執行緒安全的,其內部的實作用到了哪些鎖

SharedPreferences的本質是用鍵值對的方式保存資料到xml檔案,然后對檔案進行讀寫操作,

  • 對于讀操作,加一把鎖就夠了:
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
  • 對于寫操作,由于是兩步操作,一個是editor.put,一個是commit或者apply所以其實是需要兩把鎖的:
//第一把鎖,操作Editor類的map物件
public final class EditorImpl implements Editor {
  @Override
  public Editor putString(String key, String value) {
      synchronized (mEditorLock) {
          mEditorMap.put(key, value);
          return this;
      }
  }
}


//第二把鎖,操作檔案的寫入
synchronized (mWritingToDiskLock) {
    writeToFile(mcr, isFromSyncCommit);
}

是行程安全的嗎?如果是不安全的話我們作為開發人員該怎么辦?

1) SharedPreferences是行程不安全的,因為沒有使用跨行程的鎖,既然是行程不安全,那么久有可能在多行程操作的時候發生資料例外,

2) 我們有兩個辦法能保證行程安全:

  • 使用跨行程組件,也就是ContentProvider,這也是官方推薦的做法,通過ContentProvider對多行程進行了處理,使得不同行程都是通過ContentProvider訪問SharedPreferences,
  • 加檔案鎖,由于SharedPreferences的本質是讀寫檔案,所以我們對檔案加鎖,就能保證行程安全了,

SharedPreferences 操作有檔案備份嗎?是怎么完成備份的?

  • SharedPreferences 的寫入操作,首先是將源檔案備份:
  if (!backupFileExists) {
      !mFile.renameTo(mBackupFile);
  }
  • 再寫入所有資料,只有寫入成功,并且通過 sync 完成落盤后,才會將 Backup(.bak) 檔案洗掉,
  • 如果寫入程序中行程被殺,或者關機等非正常情況發生,行程再次啟動后如果發現該 SharedPreferences 存在 Backup 檔案,就將 Backup 檔案重名為源檔案,原本未完成寫入的檔案就直接丟棄,這樣就能保證之前資料的正確,

為什么需要插件化

我覺得最主要的原因是可以動態擴展功能,
把一些不常用的功能或者模塊做成插件,就能減少原本的安裝包大小,讓一些功能以插件的形式在被需要的時候被加載,也就是實作了動態加載

比如動態換膚、節日促銷、見不得人的一些功能,就可以在需要的時候去下載相應模式的apk,然后再動態加載功能,所以一般這個功能適用于一些平臺類的專案,比如大眾點評美團這種,功能很多,用戶很大概率只會用其中的一些功能,而且這些模塊單獨拿出來都可以作為一個app運行,

但是現在用的卻很少了,具體情況見第三點,

插件化的原理

要實作插件化,也就是實作從apk讀取所有資料,要考慮三個問題:

  • 讀取插件代碼,完成插件中代碼的加載和與主工程的互相呼叫
  • 讀取插件資源,完成插件中資源的加載和與主工程的互相訪問
  • 四大組件管理

1)讀取插件代碼,其實也就是進行插件中的類加載,所以用到類加載器就可以了,
Android中常用的有兩種類加載器,DexClassLoaderPathClassLoader,它們都繼承于BaseDexClassLoader,區別在于DexClassLoader多傳了一個optimizedDirectory引數,表示快取我們需要加載的dex檔案的,并創建一個DexFile物件,而且這個路徑必須為內部存盤路徑,而PathClassLoader這個引數為null,意思就是不會快取到內部存盤空間了,而是直接用原來的檔案路徑加載,所以DexClassLoader功能更為強大,可以加載外部的dex檔案,

同時由于雙親委派機制,在構造插件的ClassLoader時會傳入主工程的ClassLoader作為父加載器,所以插件是可以直接可以通過類名參考主工程的類,
而主工程呼叫插件則需要通過DexClassLoader去加載類,然后反射呼叫方法,

2)讀取插件資源,主要是通過AssetManager進行訪問,具體代碼如下:

/**
 * 加載插件的資源:通過AssetManager添加插件的APK資源路徑
 */
protected void loadPluginResources() {
    //反射加載資源
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
        mAssetManager = assetManager;
    } catch (Exception e) {
        e.printStackTrace();
    }
    Resources superRes = super.getResources();
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}   

通過addAssetPath方法把插件的路徑穿進去,就可以訪問到插件的資源了,

3)四大組件管理
為什么單獨說下四大組件呢?因為四大組件不僅要把他們的類加載出來,還要去管理他們的生命周期,在AndroidManifest.xml中注冊,這也是插件化中比較重要的一部分,這里重點說下Activity,

主要實作方法是通過Hook技術,主要的方案就是先用一個在AndroidManifest.xml中注冊的Activity來進行占坑,用來通過AMS的校驗,接著在合適的時機用插件Activity替換占坑的Activity

Hook 技術又叫做鉤子函式,在系統沒有呼叫該函式之前,鉤子程式就先捕獲該訊息,鉤子函式先得到控制權,這時鉤子函式既可以加工處理(改變)該函式的執行行為,還可以強制結束訊息的傳遞,簡單來說,就是把系統的程式拉出來變成我們自己執行代碼片段,

這里的hook其實就是我們常說的下鉤子,可以改變函式的內部行為,

這里加載插件Activity用到hook技術,有兩個可以hook的點,分別是:

  • Hook IActivityManager
    上面說了,首先會在AndroidManifest.xml中注冊的Activity來進行占坑,然后合適的時機來替換我們要加載的Activity,所以我們主要需要兩步操作:
    第一步:使用占坑的這個Activity完成AMS驗證,
    也就是讓AMS知道我們要啟動的Activity是在xml里面注冊過的哦,具體代碼如下:
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("startActivity".contains(method.getName())) {
                //換掉
                Intent intent = null;
                int index = 0;
                for (int i = 0; i < args.length; i++) {
                    Object arg = args[i];
                    if (arg instanceof Intent) {
                        //說明找到了startActivity的Intent引數
                        intent = (Intent) args[i];
                        //這個意圖是不能被啟動的,因為Acitivity沒有在清單檔案中注冊
                        index = i;
                    }
                }
               //偽造一個代理的Intent,代理Intent啟動的是proxyActivity
                Intent proxyIntent = new Intent();
                ComponentName componentName = new ComponentName(context, proxyActivity);
                proxyIntent.setComponent(componentName);
                proxyIntent.putExtra("oldIntent", intent);
                args[index] = proxyIntent;
            }

            return method.invoke(iActivityManagerObject, args);
        }

第二步:替換回我們的Activity,
上面一步是把我們實際要啟動的Activity換成了我們xml里面注冊的activity來躲過驗證,那么后續我們就需要把Activity換回來,
Activity啟動的最后一步其實是通過H(一個handler)中重寫的handleMessage方法會對LAUNCH_ACTIVITY型別的訊息進行處理,最侄訓呼叫Activity的onCreate方法,最后會呼叫到Handler的dispatchMessage方法用于處理訊息,如果Handler的Callback型別的mCallback不為null,就會執行mCallback的handleMessage方法, 所以我們能hook的點就是這個mCallback


 public static void hookHandler() throws Exception {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");//1
        Field mHField = FieldUtil.getField(activityThread,"mH");//2
        Handler mH = (Handler) mHField.get(currentActivityThread);//3
        FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
    }

public class HCallback implements Handler.Callback{
    //...
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //得到訊息中的Intent(啟動SubActivity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                //得到此前保存起來的Intent(啟動TargetActivity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //將啟動SubActivity的Intent替換為啟動TargetActivity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

用自定義的HCallback來替換mH中的mCallback即可完成Activity的替換了,

  • Hook Instrumentation

這個方法是由于startActivityForResult方法中呼叫了Instrumentation的execStartActivity方法來激活Activity的生命周期,所以可以通過替換Instrumentation來完成,然后在InstrumentationexecStartActivity方法中用占坑SubActivity來通過AMS的驗證,在InstrumentationnewActivity方法中還原TargetActivity,

public class InstrumentationProxy extends Instrumentation {
    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;
    public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
        mInstrumentation = instrumentation;
        mPackageManager = packageManager;
    }
    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        if (infos == null || infos.size() == 0) {
            intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
            intent.setClassName(who, "com.example.liuwangshu.pluginactivity.StubActivity");//2
        }
        try {
            Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
                    target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
        IllegalAccessException, ClassNotFoundException {
    	String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
    	if (!TextUtils.isEmpty(intentName)) {
        	return super.newActivity(cl, intentName, intent);
    	}
    	return super.newActivity(cl, className, intent);
	}

}

  public static void hookInstrumentation(Context context) throws Exception {
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        Field mMainThreadField  =FieldUtil.getField(contextImplClass,"mMainThread");//1
        Object activityThread = mMainThreadField.get(context);//2
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
        FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
                context.getPackageManager()));
    }

市面上的一些插件化方案以及你的想法

前幾年插件化還是很火的,比如Dynamic-Load-Apk(任玉剛),DroidPlugin,RePlugin(360),VirtualApk(滴滴),但是現在機會都沒怎么在運營了,好多框架都最多只支持到Android9,

這是為什么呢?我覺得一個是維護成本太高難以兼容,每更新一次原始碼,就要重新維護一次,二就是確實插件化技術現在用的不多了,以前用插件化框架干嘛?主要是比如增加新的功能,讓功能模塊之間解耦,現在有RN可以進行插件化功能,有組件化可以進行專案解耦,所以用的人就不多咯,

雖然插件化用的不多了,但是我覺得技識訓是可以了解的,而且熱更新主要用的也是這些技術,方案可以被淘汰,但是技術不會,

參考

多執行緒
記憶體泄露
啟動優化
view.post
view.post
SharedPreferences

總結

希望給大家一點幫助吧,當然文章我也會繼續寫的,感覺大家之前給我點的贊,嘿嘿,

大家一起加油吧!共勉!愛你們!

有一起學習的小伙伴可以關注下??,

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

標籤:其他

上一篇:數字信號處理大作業——基于matlab R2019a的男聲變女聲資料匯總與代碼

下一篇:LoRaWAN地區引數

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

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

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

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

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

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

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

    ......

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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