Android 記憶體管理

来源:https://www.cnblogs.com/zhiqinlin/archive/2023/08/23/17652334.html
-Advertisement-
Play Games

我司存在記憶體為1G RAM的設備,屬於低記憶體設備,經常會出現記憶體很緊張的場景,也容易因此導致一系列七七八八的邊際問題,故有必要瞭解Android系統的記憶體相關知識: 1. 瞭解記憶體的分配、回收方式 2. 瞭解OOM、LMK的相關機制 3. 瞭解Android系統記憶體相關調試方式 4. 瞭解Andro... ...


一、需求

        我司存在記憶體為1G RAM的設備,屬於低記憶體設備,經常會出現記憶體很緊張的場景,也容易因此導致一系列七七八八的邊際問題,故有必要瞭解Android系統的記憶體相關知識:

  1. 瞭解記憶體的分配、回收方式
  2. 瞭解OOM、LMK的相關機制
  3. 瞭解Android系統記憶體相關調試方式
  4. 瞭解Android系統的性能優化方案

二、環境

  1. JDK 1.8
  2. Android 10

三、JVM

        JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一個虛構出來的電腦,有著自己完善的硬體架構,如處理器、堆棧等。

3.1 編譯&執行過程

        Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程式只需生成在Java虛擬機上運行的目標代碼(位元組碼),就可以在多種平臺上不加修改地運行。
        Java文件必須先通過一個叫javac的編譯器,將代碼編譯成class文件,然後通過JVM把class文件解釋成各個平臺可以識別的機器碼,最終實現跨平臺運行代碼。

3.2 JVM記憶體模型

3.2.1 方法區

        方法區是《Java虛擬機規範》中規定的一個記憶體區域,它用於存儲已被虛擬機載入的類型信息、常量、靜態變數、即時編譯器編譯後的代碼緩存等。方法區是一個規範,它的實現取決於不同的虛擬機:

  1. 在Java8之前,HotSpot虛擬機使用 永久代 來實現方法區。
  1. 而Java8之後,HotSpot虛擬機使用 元空間 來實現方法區。
        元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地記憶體;永久代在虛擬機中。

        方法區存儲的信息如下:

名稱 內容
類型信息 (1)是類class、介面interface、枚舉enum、註解annotation中哪一種
(2)完整有效名稱(包名.類名)
(3)直接父類的完整名稱(介面和java.lang.Object沒有父類)
(4)類型的修飾符(public、abstract、final等)
(5)類型直接介面的有序列表(實現的介面構成列表)
域(Field、屬性)信息 (1)保存類型所有域(屬性)的相關信息和聲明順序
(2)相關信息包含:功能變數名稱稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient等)
方法(method)信息:按順序保存 (1)方法名稱
(2)返回類型(含Void)
(3)方法參數和類型(按順序)
(4)方法的修飾符(public、private、protected、static、final、synchronized、native、abstract)
(5)方法的位元組碼、操作數棧、局部變數表及其大小(abstract和native方法除外)
(6)異常表abstract和native方法除外),每個異常處理開始、結束位置,代碼處理在程式計數器中的偏移地址,被捕獲的異常類的常量池索引等
Non-final類變數:(static修飾的變數,靜態變數) (1)邏輯上是類數據一部分
(2)在類的載入過程中鏈接的準備階段設置預設初始值,初始化階段賦予真實值
(3)類變數(non-final)被所有實例共用,沒有實例化類對象也可訪問,(全局常量,static和final一起修飾)
(4)與final修飾的類變數不同,每個全局常量在編譯時就分配了
Class文件常量池 (1)一個有效的位元組碼文件除了包含類的版本信息、欄位、方法以及介面描述信息外,還包含一個常量池表。常量池表中包含字面量、域和方法的符號引用。
(2)字面量就是int i=5;String=”Hello World!”中的5和”Hello World!”
(3)一個JAVA源原文件中的類、介面,編譯後生成位元組碼文件,Java中的位元組碼需要數據,但是這些數據很多很大,不能直接存到記憶體中,可以將其存到常量池中,位元組碼中包含了指向常量池的引用。
(4)常量池中包含:數量值、字元串引用、類引用、欄位引用、方法引用
運行時常量池 (1)運行時常量池是方法區的一部分
(2)常量池表示Class中的一部分,用於存放編譯器生成的各種字面量和符號引用,在載入類和介面到虛擬機後,就創建相應的運行時常量池
(3)JVM為每個載入的類或介面維護一個運行時常量池,池中數據類似數組項,通過索引訪問
(4)運行時常量池中含多種不同常量,包含編譯器就明確的數值字面量,也包含運行期的方法或者欄位引用,此時不再是常量池中的符號地址,而是真實地址。
(5)運行時常量池,相對於Class文件中的常量池,還有一個特征就是具備動態性,可以動態添加數據到運行時常量池
(6)當創建運行時常量池時,如果所需記憶體空間大於方法區能提供的最大值,那麼JVM拋出OutOfMemoryError異常

3.2.2 堆

        堆是java記憶體管理中最大的一塊記憶體,也是所有線程共用的一塊記憶體,在虛擬機啟動時創建。堆中主要存放的是對象實例、數組。幾乎所有的對象實例、數組都在這一塊記憶體中分配。
        堆也是GC垃圾回收的主要區域。垃圾回收現在主要採取的是分代垃圾回收演算法。為了方便垃圾回收,java堆還進行了細分:新生代(YoungGen)、和老年代(oldGen),預設占比為:1:2;其中新生代還可以劃分為Eden空間、survivor0空間、survivor1空間,預設占比為:8:1:1;

對象記憶體分配過程如下:
        1.new一個對象value,value先放於新生代->Eden區;
        2.當Eden區空間填滿後,我們需要再創建value2對象,JVM會對Eden區繼續垃圾回收(Minor GC);
        3.Eden區觸發GC後,Eden區會被清空,同時Eden區幸存對象會移動到S0幸存區。此時,Eden區和S1區未存放對象;
        4.如果Eden區再次被填滿,再次觸發GC,此時會對Eden區和S0區進行垃圾回收,存活對象移動至S1幸存區。此時Eden區和S0區未存放對象;
        5.在eden區發生gc後剩餘對象記憶體大於s區時,直接進入老年代。
        6.如果再次經歷垃圾回收,此時幸存對象會重新放回S0區,如此反覆,幸存區會永遠存在一個區為空對象;
        7. 當我們的對象時長超過一定年齡時(預設15,可以通過參數設置),將會把對象放入老生代,當然大的對象會直接進入老生代。老生代採用的回收演算法是標記整理演算法。
        8. 當老年代記憶體滿了或者發生young GC後要轉移至老年代的對象記憶體大於老年代剩餘記憶體時,觸發Full GC(Full GC會觸發STW(stop the world))。

3.2.3 程式計數器

        程式計數器是一塊較小的記憶體空間,可看作是當前線程所執行位元組碼的行號指示器。位元組碼解釋器根據這個計數器來獲取當前線程需要執行的下一條指令,分支、迴圈、跳轉、異常、線程恢復等功能都需要依賴程式計數器來完成。
        此外,線上程爭奪CPU時間片的時候,需要線程切換,這時候,就需要這個計數器來幫助線程恢復到正確執行的位置,每一條線程有自己的程式計數器,所以才能夠保證當前程式能夠正確恢復到上次執行的步驟。
ps:程式計數器是唯一一個不會出現OOM錯誤的記憶體區域,它的生命周期伴隨線程的創建而創建,隨程式的消亡而消亡。

3.2.4 虛擬機棧

        虛擬機棧是線程私有的。虛擬機棧跟線程的生命周期相同,它描述的是java方法執行的記憶體模型,每次java方法調用的數據,都是通過棧傳遞的。
        java記憶體可以粗糙的分為 堆記憶體(heap)和 棧記憶體(stack) ,其中棧記憶體就是指的虛擬機棧,或者說是虛擬機棧中局部變數表中的部分。實際上,虛擬機棧就是由一個個棧幀組成,而每個棧幀中都擁有:局部變數表、操作數棧、動態鏈接、方法出口信息:

名稱 內容
局部變數表 主要存放的是編譯期間可知的各種數據類型(八大基本數據類型)、對象引用(Reference類型,不同於對象,可能是指向對象地址的指針或者與此對象位置相關的信息)
操作數棧 主要用於保存計算過程中的中間結果,同時作為計算過程中變數臨時的存儲空間
動態鏈接 對運行常量池的引用,類載入機制過程中解析那一步的作用是將常量池中的符號引用替換成直接引用,這叫靜態鏈接,而這裡的動態連接的意思是在運行過程中轉換成直接引用
方法出口 無論是程式正常返回或者是異常調用完成返回,都必須回到最初方法被調用時的位置。

        虛擬機棧可能拋出兩種錯誤:StackOverflowError 、OutOfMemoryError。

3.2.5 本地方法棧

        本地方法棧的工作原理跟虛擬機棧並無區別,唯一的區別就是本地方法棧面向的不是.class位元組碼,而是Native修飾的本地方法。
        本地方法的執行過程,也是本地方法棧中棧幀的出棧過程。
        同虛擬機棧一樣,本地方法棧也是會拋出 StackOverflowError 、OutOfMemoryError 兩種異常。

3.2.6 直接記憶體

        直接記憶體有一種叫法,堆外記憶體。
        直接記憶體(堆外記憶體)指的是Java應用程式通過直接方式從操作系統中申請的記憶體。這個的差別與之前的堆,棧,方法區不同。那些記憶體都是經過了虛擬化的記憶體

3.2.7 方法區、堆、棧之間的關係

        棧中的類位元組碼存儲在方法區(也就是存類),實例化對象存儲在Java堆,對象引用存儲在棧中。

四、OOM

        OOM(Out of Memory)即記憶體溢出,是因為應用所需要分配的記憶體超過系統對應用記憶體的閾值,而拋出的 java.lang.OutOfMemoryError錯誤。 OOM的根本原因是開發者對記憶體使用不當造成的。
        Android的每個應用程式都會使用一個專有的Dalvik虛擬機實例來運行,也就是說每個應用程式都是在屬於自己的進程中運行的。如果程式記憶體溢出,Android系統只會kill掉該進程,而不會影響其他進程的使用(如果是system_process等系統進程出問題的話,則會引起系統重啟)。

4.1 OOM閾值

Android系統JVM對應用所分配的記憶體閾值:

sl8541e_1h10_32b:/ # getprop | grep dalvik.vm.heap
[dalvik.vm.heapgrowthlimit]: [80m] //單個應用程式最大記憶體限制,超過將被Kill
[dalvik.vm.heapsize]: [256m] //所有情況下(包括設置android:largeHeap="true"的情形)的最大堆記憶體值,超過直接oom
[dalvik.vm.heapstartsize]: [6m] //單個應用程式分配的初始記憶體

4.2 OOM演示

4.2.1 測試代碼

void testForOutMemory(){
    ActivityManager mActivityManager = (ActivityManager) getApplication().getSystemService(Context.ACTIVITY_SERVICE);
    int largeMemoryClass = mActivityManager.getLargeMemoryClass();
    int memoryClass = mActivityManager.getMemoryClass();
    int currentMemory = (int) Runtime.getRuntime().maxMemory() /1024 /1024;
    Log.d("LZQ","[show memory] largeMemoryClass = " + largeMemoryClass + " | memoryClass = " + memoryClass + " | currentMemory = " + currentMemory);

    List list = new ArrayList();
    int count = 0;
    while (true) {
        Log.d("LZQ","[allocate memory] count = " + count);
        byte[] test = new byte[20 * 1024 * 1024];//20M數據
        list.add(test);
        count++;
    }
}

4.2.2 測試結果

  1. 當前應用未設置largeHeap,故當前設備的應用最大記憶體為80MB;
  2. 往系統申請20MB的記憶體,僅申請了3*20=60MB的記憶體,當申請第4塊記憶體時,系統發生OOM,當前的應用內僅剩18MB,不足以繼續分配;

4.3 OOM異常定位

        OOM異常在log上還是相對明顯,有OOM標識:java.lang.OutOfMemoryError。
        堆記憶體分配失敗,對應的代碼如下(以下流程涉及JVM的記憶體分配流程,沒有進一步展開分析,詳細代碼可自行閱讀):

@art\runtime\gc\heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
  // If we're in a stack overflow, do not create a new exception. It would require running the
  // constructor, which will of course still be in a stack overflow.
  ...
  std::ostringstream oss;
  size_t total_bytes_free = GetFreeMemory();
  oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
      << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM,"
      << " target footprint " << target_footprint_.load(std::memory_order_relaxed)
      << ", growth limit "
      << growth_limit_;
  ...
  self->ThrowOutOfMemoryError(oss.str().c_str());
}

AndroidStudio工具Profiler,可以查看設備實時記憶體:
相關用法:https://developer.android.google.cn/studio/profile/memory-profiler?hl=zh-cn

4.4 OOM常見場景

如下Android開發者比較常見的OOM場景:

類型 應用場景
資源對象沒關閉造成的記憶體泄露 Cursor
調用registerReceiver後未調用unregisterReceiver()
未關閉InputStream/OutputStream
Bitmap使用後未調用recycle()
作用域不一樣,導致對象不能被垃圾回收器回收 非靜態內部類會隱式地持有外部類的引用,handler
Context泄露:
1、 不要保留對Context-Activity長時間的引用(對Activity的引用的時候,必須確保擁有和Activity一樣的生命周期)
2、嘗試使用Context-Application來替代Context-Activity
3、如果你不想控制內部類的生命周期,應避免在Activity中使用非靜態的內部類,而應該使用靜態的內部類,併在其中創建一個對Activity的弱引用。
記憶體壓力過大 圖片資源載入過多,超過記憶體使用空間,例如Bitmap 的使用
重覆創建view

五、LMKD

        進程的啟動分冷啟動和熱啟動,當用戶退出某一個進程的時候,並不會真正的將進程退出,而是將這個進程放到後臺,以便下次啟動的時候可以馬上啟動起來,這個過程名為熱啟動,這也是Android的設計理念之一。這個機制會帶來一個問題,每個進程都有自己獨立的記憶體地址空間,隨著應用打開數量的增多,系統已使用的記憶體越來越大,就很有可能導致系統記憶體不足。
        Android 低記憶體終止守護程式 (Low Memory Killer Daemon) ,可監控運行中的 Android 系統的記憶體狀態,並通過終止最不必要的進程來應對記憶體壓力大的問題,使系統以可接受的性能水平運行。

5.1 LMKD 框架圖

5.2 LMKD相關概念

5.2.1 LMKD錯誤信息

出現低記憶體異常殺死進程,一般會有lowmemorykiller的tag信息。

08-09 10:33:27.695   376   376 I lowmemorykiller: Kill 'com.android.deskclock' (14849), uid 1000, oom_adj 700 to free 6728kB; reason: low watermark is breached and swap is low (0kB < 108644kB), adjust critical adj.
08-09 10:33:27.695   376   376 D lowmemorykiller: handle notify_lmfs_process_killed done for 14849
08-09 10:33:27.720   383   383 D SurfaceFlinger: Setting power mode 0 on display 0
08-09 10:33:27.807   776  1101 D LmKillerTracker: doLmkForceStop pid=14849
08-09 10:33:27.827   328   328 I Zygote  : Process 14849 exited due to signal 9 (Killed)

5.2.2 LMKD水位

我們可以觀察到低記憶體出現後,會有如下錯誤原因: low watermark is breached,即到達低水位,那麼Android設備的水位是怎麼樣的呢?
方式一:
adb shell cat /sys/module/lowmemorykiller/parameters/minfree
adb shell cat /sys/module/lowmemorykiller/parameters/adj
(通過kernel的lmk機制殺死進程,在Android P及以前版本中,採用該方式)
方式二:
adb shell getprop | grep sys.lmk.minfree_levels(在Android Q及之後,採用該方式)

進程優先順序 記憶體水位(page) 記憶體水位(mb) 描述
0 18432 18432*4/1024=72mb 記憶體低於72mb,殺死進程等級大於0的進程
100 23040 23040*4/1024=90mb 記憶體低於90mb,殺死進程等級大於100的進程
200 27648 27648*4/1024=108mb 記憶體低於108mb,殺死進程等級大於200的進程
250 32256 32256*4/1024=126mb 記憶體低於126mb,殺死進程等級大於250的進程
900 36864 36864*4/1024=144mb 記憶體低於144mb,殺死進程等級大於900的進程
950 46080 46080*4/1024=180mb 記憶體低於180mb,殺死進程等級大於950的進程

ps: 絕大多數處理器上的記憶體頁的預設大小都是 4KB,雖然部分處理器會使用 8KB、16KB 或者 64KB 作為預設的頁面大小,但是 4KB 的頁面仍然是操作系統預設記憶體頁配置的主流,ProcessList有定義該值PAGE_SIZE=4KB;

5.2.3 進程優先順序

        對於每一個運行中的進程,Linux 內核都通過 proc 文件系統暴露 /proc/[pid]/oom_score_adj 這樣一個文件來允許其他程式修改指定進程的優先順序,這個文件允許的值的範圍是:-1000 ~ +1001之間。值越小,表示進程越重要。當記憶體非常緊張時,系統便會遍歷所有進程,以確定哪個進程需要被殺死以回收記憶體,此時便會讀取 oom_score_adj 這個文件的值。
        為了便於管理,ProcessList.java中預定義了oom_score_adj的可能取值,這裡的預定義值也是對應用進程的一種分類。

@frameworks\base\services\core\java\com\android\server\am\ProcessList.java
    // 任何主要或次要adj欄位的未初始化值
    static final int INVALID_ADJ = -10000;

    // 在某些我們還不知道的地方進行調整(通常這是將要被緩存的東西,但我們還不知道要分配的緩存範圍內的確切值)。
    static final int UNKNOWN_ADJ = 1001;

    // 這是一個只托管不可見活動的進程,因此可以在不中斷任何情況下終止它。
    static final int CACHED_APP_MAX_ADJ = 999;
    static final int CACHED_APP_MIN_ADJ = 900;

    // 這是我們允許先死的oom_adj級別。這不能等於CACHED_APP_MAX_ADJ,除非進程正在積極地被分配CACHED_APP_MAX_ADJ的oom_score_adj。
    static final int CACHED_APP_LMK_FIRST_ADJ = 950;

    // SERVICE_ADJ的B列表——這些是舊的和破舊的服務,不像A列表中的服務那麼閃亮和有趣。
    static final int SERVICE_B_ADJ = 800;

    // 這是用戶所在的前一個應用程式的進程。這個過程保持在其他事情之上,因為切換回上一個應用程式是非常常見的。這對於最近的任務切換(在兩個最熱門的最近應用程式之間切換)以及正常的UI流(例如單擊電子郵件應用程式中的URI以在瀏覽器中查看,然後按回返回電子郵件)都很重要。
    static final int PREVIOUS_APP_ADJ = 700;

    // 這是一個包含主應用程式的進程——我們希望儘量避免殺死它,即使它通常在後臺,因為用戶與它交互太多了。
    static final int HOME_APP_ADJ = 600;

    // 這是一個包含應用程式服務的進程——就用戶而言,殺死它不會有太大的影響。
    static final int SERVICE_ADJ = 500;

    // 這是一個重量級應用程式的過程。它在背景中,但我們要儘量避免殺死它。在system/rootdir/init中設置的值。Rc啟動。
    static final int HEAVY_WEIGHT_APP_ADJ = 400;

    // 這是當前托管備份操作的進程。殺死它並不完全致命,但通常是個壞主意。
    static final int BACKUP_APP_ADJ = 300;

    // 這是一個受系統(或其他應用)約束的過程,它比服務更重要,但如果被殺死,它不會立即影響用戶。
    static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

    // 這是一個只承載用戶可感知組件的進程,我們確實希望避免殺死它們,但它們不是立即可見的。背景音樂播放就是一個例子。
    static final int PERCEPTIBLE_APP_ADJ = 200;

    // 這是一個只承載用戶可見的活動的進程,所以我們希望它們不要消失。
    static final int VISIBLE_APP_ADJ = 100;
    static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

    // 這是一個最近被列為TOP的過程,並轉移到了FGS。在一段時間內,繼續把它當作前臺應用來對待。
    static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

    // 這是運行當前前臺應用程式的進程。我們真的不想殺死它!
    static final int FOREGROUND_APP_ADJ = 0;

    // 這是系統或持久進程綁定的進程,並表示它很重要。
    static final int PERSISTENT_SERVICE_ADJ = -700;

    // 這是一個系統持久進程,例如電話。我當然不想殺死它,但這樣做也不是完全致命的。
    static final int PERSISTENT_PROC_ADJ = -800;

    // 系統進程以預設調整運行。
    static final int SYSTEM_ADJ = -900;

    // 不受系統管理的本機進程的特殊代碼(因此沒有系統分配的空間)。
    static final int NATIVE_ADJ = -1000;

如下為測試應用在前臺和後臺切換,oom_score_adj的變化:

  1. 查看myproject應用的進程id:4262
  2. 打開應用,查看應用的adj值:0
  3. 按home鍵,應用回到後臺,查看應用的adj值:700
  4. 打開其他應用,並按home鍵回到後臺,此時查看myproject應用的adj值:800

ps:一般情況下,我們也會將進程分為:前臺進程>可見進程>服務進程>後臺進程>空進程。

5.2.4 系統記憶體實時查看

指令:adb shell cat proc/meminfo

5.3 LMKD源碼分析

        源碼分析是一個非常枯燥&無聊的事情,我們需要帶著些問題去查閱代碼,不然很容易被淹沒在代碼的海洋里!如下,是我們本次查看源碼需要瞭解的邏輯:

  1. lowmemorykiller的異常信息是在哪列印的?如何殺死進程?
  2. oom_score_adj是怎麼樣發生變化的?

5.3.1 LMKD流程圖

5.3.2 Framework層-AMS服務

5.3.2.1 進程adj變化

1. 進程殺掉後

2. 進程創建後

3. 進程回到後臺

5.3.2.2 更新進程adj值

從5.3.1.1節,我們可以看到adj的更新,最終都會引用到setOomAdj().
step 1. 構建buf,寫入指令id,進程id,adj值

@frameworks\base\services\core\java\com\android\server\am\ProcessList.java
    public static void setOomAdj(int pid, int uid, int amt) {
        ...
        long start = SystemClock.elapsedRealtime();
        ByteBuffer buf = ByteBuffer.allocate(4 * 4);
        buf.putInt(LMK_PROCPRIO);
        buf.putInt(pid);
        buf.putInt(uid);
        buf.putInt(amt);
        writeLmkd(buf, null);
        ...
    }

step 2. 打開lmkd的socket埠

    private static boolean openLmkdSocketLS() {
        try {
            sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
            sLmkdSocket.connect(
                new LocalSocketAddress("lmkd",
                        LocalSocketAddress.Namespace.RESERVED));
            sLmkdOutputStream = sLmkdSocket.getOutputStream();
            sLmkdInputStream = sLmkdSocket.getInputStream();
        } 
        ...
        return true;
    }

step 3. 往lmkd寫入buf數據

   private static boolean writeLmkdCommandLS(ByteBuffer buf) {
        try {
            sLmkdOutputStream.write(buf.array(), 0, buf.position());
        } catch (IOException ex) {
            Slog.w(TAG, "Error writing to lowmemorykiller socket");
            IoUtils.closeQuietly(sLmkdSocket);
            sLmkdSocket = null;
            return false;
        }
        return true;
    }

step 4. 從lmkd讀取buf數據

    private static boolean readLmkdReplyLS(ByteBuffer buf) {
        int len;
        try {
            len = sLmkdInputStream.read(buf.array(), 0, buf.array().length);
            if (len == buf.array().length) {
                return true;
            }
        } 
        ...
    }

5.3.3 Native層-LMKD進程

5.3.3.1 lmkd進程啟動

@system/core/lmkd/lmkd.rc
service lmkd /system/bin/lmkd
    class core
    user lmkd
    group lmkd system readproc
    capabilities DAC_OVERRIDE KILL IPC_LOCK SYS_NICE SYS_RESOURCE NET_ADMIN
    critical
    socket lmkd seqpacket 0660 system system
    socket lmfs stream 0660 root system
    socket vmpressure stream 0666 root system
    writepid /dev/cpuset/system-background/tasks

5.3.3.2 lmkd->main():入口函數

LMKD進程的入口main函數,主要初始化該模塊相關參數、消息事件

@\system\core\lmkd\lmkd.c
int main(int argc __unused, char **argv __unused) {
    ...
    level_oomadj[VMPRESS_LEVEL_LOW] =
        property_get_int32("ro.lmk.low", OOM_SCORE_ADJ_MAX + 1);//初始化屬性配置
    ...
    if (!init()) {//初始化消息事件處理
        if (!use_inkernel_interface) {//不使用驅動LMK殺進程方案
            ...
            if (mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) && (errno != EINVAL)) {//虛擬空間上鎖,防止記憶體交換
                ALOGW("mlockall failed %s", strerror(errno));
            }
            ...
        }
        ...
        mainloop();//輪詢監聽消息事件
    }
    ...
    return 0;
}

5.3.3.3 lmkd->init():初始化

step 1. 創建epoll
創建了一個epoll實例,整個lmkd的消息處理都是依賴epoll 機制來管理。其相當於是,創建一個池子,一個監控和管理句柄 fd 的池子,有點像java的線程池;

    epollfd = epoll_create(MAX_EPOLL_EVENTS);
    if (epollfd == -1) {
        ALOGE("epoll_create failed (errno=%d)", errno);
        return -1;
    }

step 2. 初始化socket lmkd
該部分對lmkd端的socket通信進行初始化,其對端AMS.mProcessList會通過/dev/socket/lmkd節點與lmkd進行通信,socket連接成功後,響應事件處理由ctrl_connect_handler函數處理。

    ctrl_sock.sock = android_get_control_socket("lmkd");//設置監聽的socket名稱
    if (ctrl_sock.sock < 0) {
        ALOGE("get lmkd control socket failed");
        return -1;
    }

    ret = listen(ctrl_sock.sock, MAX_DATA_CONN);//監聽socket消息
    if (ret < 0) {
        ALOGE("lmkd control socket listen failed (errno=%d)", errno);
        return -1;
    }

    epev.events = EPOLLIN;//只有當對端有數據寫入時才會觸發,所以觸發一次後需要不斷讀取所有數據直到讀完EAGAIN為止
    ctrl_sock.handler_info.handler = ctrl_connect_handler;//socket連接成功的響應處理事件
    epev.data.ptr = (void *)&(ctrl_sock.handler_info);
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_sock.sock, &epev) == -1) {//將socket句柄添加到epoll的池子,並設置epev的監聽事件類型
        ALOGE("epoll_ctl for lmkd control socket failed (errno=%d)", errno);
        return -1;
    }

step 3. 確定是否用LMK 驅動程式
        過去,Android 使用記憶體LMK 驅動程式來監控系統記憶體的壓力,這是一種依賴於硬編碼值的硬體機制。從Kernel 4.12開始,LMK驅動程式從上游內核中移除,由應用空間的 lmkd 執行記憶體監控和進程終止任務。
        通過函數access 確認舊的節點是否還存在,用以確認kernel 是否還在用LMK 驅動程式。之所以有這樣的處理,應該是Android 為了相容舊版本kernel。目前Android10上,該節點已不存在。

#define INKERNEL_MINFREE_PATH "/sys/module/lowmemorykiller/parameters/minfree"

has_inkernel_module = !access(INKERNEL_MINFREE_PATH, W_OK);
use_inkernel_interface = has_inkernel_module;

step 4. 選擇系統記憶體監控策略
        LMKD進程通過使用內核生成的 vmpressure 事件或PSI監視器,獲取記憶體壓力等級的通知。但是由於vmpressure信號會存在大量誤報的情況,造成不必要的系統開銷。因此,Android 10 以及更高版本,使用 PSI 監視器來檢測記憶體壓力,且當前Android為了對舊版本的支持,依然保留了vmpressure策略。
        PSI (Pressure Stall Information) 壓力失速信息,PSI統計數據為即將發生的資源短缺提供了預警功能,因而實現更主動、更細緻、更準確的響應。當然PSI統計數據不僅包含了Memory,它同時涵蓋了Memory、CPU、IO三大資源的pressure指標,來幫助工程師們及時管控系統資源短缺的情況。

/* Try to use psi monitor first if kernel has it */
    use_psi_monitors = property_get_bool("ro.lmk.use_psi", true) &&
        init_psi_monitors();//使用psi策略

    if (use_psi_vmpressure && use_psi_monitors) {
        if (!init_mp_common(VMPRESS_LEVEL_MEDIUM_EXT)) {//使用vmpressure策略
            ALOGE("Kernel does no support memory pressure events.use psi only.");
        }
    }

        本文我們將分析新策略,即PSI策略,來進一步分析lmkd。LMKD是支持新舊策略同時執行的。
step 5. 初始化PSI策略相關行為
        確認是使用PSI策略還是vmpressure策略,同時對於不同的策略,初始化相關操作。

static bool init_psi_monitors() {
    ...
    bool use_new_strategy =
        property_get_bool("ro.lmk.use_new_strategy", low_ram_device || !use_minfree_levels);//確認是使用PSI 策略還是vmpressure

    /* In default PSI mode override stall amounts using system properties */
    if (use_new_strategy) {
        /* Do not use low pressure level */
        psi_thresholds[VMPRESS_LEVEL_LOW].threshold_ms = 0;
        psi_thresholds[VMPRESS_LEVEL_MEDIUM].threshold_ms = psi_partial_stall_ms;//70ms,部分 PSI 失速閾值(以毫秒為單位),用於觸發記憶體不足通知。如果設備收到記憶體壓力通知的時間太晚,可以降低此值以在較早的時間觸發通知。
        psi_thresholds[VMPRESS_LEVEL_CRITICAL].threshold_ms = psi_complete_stall_ms;//700ms,完全 PSI 失速閾值(以毫秒為單位),用於觸發關鍵記憶體通知。如果設備收到關鍵記憶體壓力通知的時間太晚,可以降低該值以在較早的時間觸發通知。
    }
    //初始化PSI相關行為
    if (!init_mp_psi(VMPRESS_LEVEL_LOW, use_new_strategy)) {
        return false;
    }
    ...
    return true;
}

step 6. PSI記憶體壓力監聽&響應

#define PSI_MON_FILE_MEMORY "/proc/pressure/memory"
static bool init_mp_psi(enum vmpressure_level level, bool use_new_strategy) {
    ...
    fd = init_psi_monitor(psi_thresholds[level].stall_type,
        psi_thresholds[level].threshold_ms * US_PER_MS,
        PSI_WINDOW_SIZE_MS * US_PER_MS);//獲取/proc/pressure/memory節點
    ...
    vmpressure_hinfo[level].handler = use_new_strategy ? mp_event_psi_psi : mp_event_common;//記憶體壓力消息響應處理事件
    vmpressure_hinfo[level].data = level;
    if (register_psi_monitor(epollfd, fd, &vmpressure_hinfo[level]) < 0) {//註冊監聽器,監聽psi記憶體壓力
        destroy_psi_monitor(fd);
        return false;
    }
    ...
    return true;
}

5.3.3.4 lmkd->mainloop:epoll消息處理

mainloop主要是通過epoll_wait阻塞線程,有消息響應後,再分發消息給對應的handler處理對應邏輯。

static void mainloop(void) {
    ...
    while (1) {
        ...
        if (poll_params.poll_handler) {
            ...
            /* Wait for events until the next polling timeout */
            nevents = epoll_wait(epollfd, events, maxevents, delay);//阻塞等待epoll響應
            ...
        }
        ...
        /* Second pass to handle all other events */
        for (i = 0, evt = &events[0]; i < nevents; ++i, evt++) {
            ...
            if (evt->data.ptr) {
                handler_info = (struct event_handler_info*)evt->data.ptr;
                /* Set input params for the call */
                handler_info->handler(handler_info->data, evt->events, &poll_params);//執行handler
                ...
            }
        }
    }
}

epoll主要監聽了9個event,不同的fd 對應不同的handler處理邏輯,這些handler大致分為:

  1. 一個socket listener fd 監聽,主要是/dev/socket/lmkd,在init() 中添加到epoll;
  2. 三個客戶端socket data fd 的數據通信,在ctrl_connect_handler() 中添加到epoll;
  3. 三個presurre 狀態的監聽,在init_psi_monitors() -> init_mp_psi() 中添加到epoll;(或者init_mp_common 的舊策略)
  4. 一個是LMK event kpoll_fd 監聽,在init() 中添加到epoll,目前新的lmkd 不再使用這個監聽;
  5. 一個是wait 進程death 的pid fd 監聽,在 start_wait_for_proc_kill() 中添加到epoll;

5.3.3.5 lmkd->ctrl_command_handler():處理AMS下發事件

AMS下發事件主要有如下,其他的事件處理雷同:

  1. 更新OomLevels水位,將minfree和oom_adj_score進行保存&組裝,然後將組裝的字元串存入到prop sys.lmk.minfree_levels。後續會根據minfree和oom_adj_score,來決定進程的查殺。
  2. 更新oom_adj_score,將AMS 中傳下來的進程的oom_score_adj 寫入到節點 /proc/pid/oom_score_adj;
//根據指令id進行事件下發
static void ctrl_command_handler(int dsock_idx) {
    ...
    switch(cmd) {
    case LMK_TARGET://更新OomLevels水位時,觸發
        ...
        cmd_target(targets, packet);
        break;
    case LMK_PROCPRIO://oom_adj_score更新時,觸發
        ...
        cmd_procprio(packet);
        break;
    case LMK_PROCREMOVE://進程退出時,移除相關信息,觸發
        ...
        cmd_procremove(packet);
        break;
    case LMK_PROCPURGE://socket連接成功後,觸發
        ...
        cmd_procpurge();
        break;
    ...
    }
    ...
}

//更新OomLevels水位
static void cmd_target(int ntargets, LMKD_CTRL_PACKET packet) {
    ...
    for (i = 0; i < ntargets; i++) {
        lmkd_pack_get_target(packet, i, &target);
        lowmem_minfree[i] = target.minfree;//記憶體閾值數組
        lowmem_adj[i] = target.oom_adj_score;//adj等級數組
        pstr += snprintf(pstr, pend - pstr, "%d:%d,", target.minfree,
            target.oom_adj_score);
        ...
    }
    pstr[-1] = '\0';
    property_set("sys.lmk.minfree_levels", minfree_str);//重新寫入水位屬性
    ...
}

//更新oom_adj_score
static void cmd_procprio(LMKD_CTRL_PACKET packet) {
    ...
    snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", params.pid);
    snprintf(val, sizeof(val), "%d", params.oomadj);
    if (!writefilestring(path, val, false)) {
        ALOGW("Failed to open %s; errno=%d: process %d might have been killed",
              path, errno, params.pid);
        /* If this file does not exist the process is dead. */
        return;
    }
    ...
}

5.3.3.6 lmkd->mp_event_psi():進程查殺

step 1. 解析/proc/vmstat和/proc/meminfo節點

    if (vmstat_parse(&vs) < 0) {
        ALOGE("Failed to parse vmstat!");
        return;
    }

    if (meminfo_parse(&mi) < 0) {
        ALOGE("Failed to parse meminfo!");
        return;
    }

step 2. 根據vmstat節點的狀態,計算工作集refault值占據file-backed頁面緩存的抖動百分比。
vmstat(Virtual Memory Statistics),用於報告虛擬記憶體狀態的統計信息,不僅可以監測虛擬記憶體,也可監測進程、物理記憶體、記憶體分頁、磁碟和 CPU 等的活動信,是對系統的整體情況進行統計

    if (!in_reclaim) {
        /* Record file-backed pagecache size when entering reclaim cycle */
        base_file_lru = vs.field.nr_inactive_file + vs.field.nr_active_file;
        init_ws_refault = vs.field.workingset_refault;
        thrashing_limit = thrashing_limit_pct;
    } else {
        /* Calculate what % of the file-backed pagecache refaulted so far */
        thrashing = (vs.field.workingset_refault - init_ws_refault) * 100 / base_file_lru;
    }
    in_reclaim = true;

step 3. 間隔60s,解析/proc/zoneinfo,並計算min/low/hight水位線

    if (watermarks.high_wmark == 0 || get_time_diff_ms(&wmark_update_tm, &curr_tm) > 60000) {
        struct zoneinfo zi;

        if (zoneinfo_parse(&zi) < 0) {
            ALOGE("Failed to parse zoneinfo!");
            return;
        }

        calc_zone_watermarks(&zi, &watermarks);
        wmark_update_tm = curr_tm;
     }

step 4. 根據mi,判斷當前所處的水位線

enum zone_watermark {
    WMARK_MIN = 0,
    WMARK_LOW,
    WMARK_HIGH,
    WMARK_NONE
};

/* Find out which watermark is breached if any */
wmark = get_lowest_watermark(&mi, &watermarks);

step 5. 根據水位線、thrashing值、壓力值、swap_low值等數據,添加不同的kill原因

   if (cycle_after_kill && wmark < WMARK_LOW) {//cycle_after_kill 為true 表明此時還處於killing 狀態,並且水位已經低於low 水位
        kill_reason = PRESSURE_AFTER_KILL;
        strncpy(kill_desc, "min watermark is breached even after kill", sizeof(kill_desc));
    } else if (level == VMPRESS_LEVEL_CRITICAL && events != 0) {//記憶體壓力過大
        kill_reason = NOT_RESPONDING;
        do_multi_kill = low_ram_device ? true : false;
        strncpy(kill_desc, "device is not responding", sizeof(kill_desc));
    } else if (fast_kill_enabled) {
        kill_reason = DO_FAST_KILL;
        strncpy(kill_desc, "do fast kill", sizeof(kill_desc));
        min_score_adj = low_ram_device ? CACHED_APP_MIN_ADJ : PERCEPTIBLE_LOW_APP_ADJ;
        min_score_adj = swap_is_low ? PERCEPTIBLE_LOW_APP_ADJ : min_score_adj;
    } else if (swap_is_low && thrashing > thrashing_limit_pct) {//swap 空間已經超過底線,且記憶體抖動占比也超過限制
        /* Page cache is thrashing while swap is low */
        kill_reason = LOW_SWAP_AND_THRASHING;
        snprintf(kill_desc, sizeof(kill_desc), "device is low on swap (%" PRId64
            "kB < %" PRId64 "kB) and thrashing (%" PRId64 "%%)",
            mi.field.free_swap * page_k, swap_low_threshold * page_k, thrashing);
    } else if (swap_is_low && wmark < WMARK_HIGH) {//swap 空間已經超過底線,且處於低水位
        /* Both free memory and swap are low */
        kill_reason = LOW_MEM_AND_SWAP;
        snprintf(kill_desc, sizeof(kill_desc), "%s watermark is breached and swap is low (%"
            PRId64 "kB < %" PRId64 "kB)", wmark > WMARK_LOW ? "min" : "low",
            mi.field.free_swap * page_k, swap_low_threshold * page_k);
    } else if (wmark < WMARK_HIGH && thrashing > thrashing_limit) {//標記此時處於低水位並抖動狀態異常
        /* Page cache is thrashing while memory is low */
        kill_reason = LOW_MEM_AND_THRASHING;
        snprintf(kill_desc, sizeof(kill_desc), "%s watermark is breached and thrashing (%"
            PRId64 "%%)", wmark > WMARK_LOW ? "min" : "low", thrashing);
        cut_thrashing_limit = true;
        /* Do not kill perceptible apps because of thrashing */
        min_score_adj = PERCEPTIBLE_APP_ADJ;
    } else if (reclaim == DIRECT_RECLAIM && thrashing > thrashing_limit) {//kswap 進入reclaim狀態,並且抖動狀態異常
        /* Page cache is thrashing while in direct reclaim (mostly happens on lowram devices) */
        kill_reason = DIRECT_RECL_AND_THRASHING;
        snprintf(kill_desc, sizeof(kill_desc), "device is in direct reclaim and thrashing (%"
            PRId64 "%%)", thrashing);
        cut_thrashing_limit = true;
        /* Do not kill perceptible apps because of thrashing */
        min_score_adj = PERCEPTIBLE_APP_ADJ;
    }

step 6. 根據當前zone的信息,以及通過lowmem_adj和lowmem_minfree水位線,重新生成min_score_adj,用於決定需要殺死的進程等級

 if (other_free <= minfree && other_file <= minfree) {
            min_score_adj_adjust = lowmem_adj[0];
            strcat(kill_desc, ", adjust adj0.");
        } else if (other_free <= lowmem_minfree[1] && other_file <= lowmem_minfree[1]) {
            min_score_adj_adjust = lowmem_adj[1];
            strcat(kill_desc, ", adjust adj1.");
        }

        if (min_score_adj_adjust == -1 && !min_score_adj) {
            min_score_adj = VISIBLE_APP_ADJ;
            strcat(kill_desc, ", adjust critical adj.");
        } else if (min_score_adj_adjust != -1) {
            min_score_adj = min_score_adj_adjust;
        }

step 7. 根據min_score_adj水位線,查找並殺死對應的進程

        if (do_multi_kill) {
            do_multi_kill = false;
            if (NOT_RESPONDING == kill_reason) {
                min_score_adj = swap_is_low ? 0 : min_score_adj;
                strcat(kill_desc, " <kill all processes above>");
                pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, true);
            } else if (swap_is_low) {
                strcat(kill_desc, " <kill all processes above>");
                pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, true);
            } else {
                strcat(kill_desc, " <kill group processes>");
                pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, false);
            }
        } else if (fast_kill_enabled) {
            fast_kill_enabled = false;
            pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, true);
        } else
            pages_freed = find_and_kill_process(min_score_adj, kill_desc);

step 8. lmkd白名單,避免被殺死
調用棧:find_and_kill_process()->proc_adj_lru_skip()->adjslot_skip()->lmkd_skip_kill()->lmkd_config_skip_kill()

@system\core\lmkd\lmkconfig.c
/* For CONFIG_LMKD_SKIP_PROCESS_LIST */
#define LMKD_PARAMETER_NAME             "/vendor/etc/lmkd_param.conf"

bool lmkd_config_skip_kill(char *task_name)
{
    PARAM_INFO param_info;
    int count = 0;
    int number = 0;

    memset(&param_info, 0, sizeof(param_info));
    if (get_param_info(&param_info, CONFIG_LMKD_SKIP_PROCESS_LIST) == true) {
        count = param_info.proc_count;
        while(count) {
            number = count - 1;
            if (!memcmp(param_info.proc_info[number].task_info, task_name, strlen(param_info.proc_info[number].task_info) -1))
                return true;
                count --;
        }
    }
    return false;
}

5.4 LMKD小結

  1. 在kernel4.12之前,採用的是linux內核的lmk機制查殺進程;
  2. 在kernel4.12之後,Android 9採用用戶空間lmkd的vmpressure策略,來查殺進程;
  3. Android 10之後採用lmkd的Psi策略,查殺進程;
  4. framework層與Lmkd是通過socket實現ipc通信;
  5. lmkd的socket通信通過epoll機制管理;
  6. lmkd可以通過配置白名單,避免被查殺;

5.5 遺留問題

  1. 從業務代碼上看,PSI策略並沒有完全遵循sys.lmk.minfree_levels水位的查殺,vmpressure策略相對來說更加遵循一點,有點意外,需要再確認此問題?
  2. 記憶體管理實際是基於如下節點:/proc/vmstat、/proc/meminfo和/proc/zoneinfo,需要瞭解其含義以及由來?
  3. /proc/pressure/memory記憶體壓力的實現邏輯?

參考資料

JVM相關:
https://blog.csdn.net/Park33/article/details/129558206
https://www.cnblogs.com/xing901022/p/5243657.html
https://www.javaclub.cn/java/41736.html
https://blog.csdn.net/qq_36370187/article/details/113093764
https://segmentfault.com/a/1190000041118595

JAVA編譯相關:
https://blog.csdn.net/weixin_45987569/article/details/127848443

OOM相關:
https://blog.csdn.net/boyupeng/article/details/47726765
https://blog.csdn.net/baidu_40389775/article/details/130861616

LMK相關:
https://www.jianshu.com/p/4dbe9bbe0449
https://blog.csdn.net/Eqiqi/article/details/131538782
https://blog.csdn.net/shift_wwx/article/details/121593698
https://justinwei.blog.csdn.net/article/details/122268437

LINUX-EPOLL相關:
https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247515011&idx=2&sn=3812f80dd80bb27340d5849df8d1cec0&chksm=fbb1327dccc6bb6bfd5ab7f9da23220ade44e88e2f8d2506b7e0868bb84665a95f026eddb82d&scene=27


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # UGUI的Image(圖片)組件的介紹及使用 ## 1. 什麼是UGUI的Image(圖片)組件? UGUI的Image(圖片)組件是Unity引擎中的一種UI組件,用於顯示2D圖像。它提供了一種簡單而靈活的方式來在游戲中載入和顯示圖片。 ## 2. 為什麼要使用UGUI的Image(圖片)組件 ...
  • # 記錄http請求 ## 環境 * .net7 ## 一、過濾器(Filter) 這個過程用的的是操作過濾器(`ActionFilter`) ## 二、 ### 2.1 繼承`IAsyncActionFilter` ### 2.2 重寫`OnActionExecutionAsync` `OnAct ...
  • [toc] # Linux運維工程師面試題(2) > 祝各位小伙伴們早日找到自己心儀的工作。 > 持續學習才不會被淘汰。 > 地球不爆炸,我們不放假。 > 機會總是留給有有準備的人的。 > 加油,打工人! ## 1 訪問一個網站的流程 1. 打開瀏覽器,輸入網址。首先查找本地緩存,如果有就打開頁面, ...
  • 操作系統是電腦不可或缺的一部分,它連接著硬體和應用程式。內核是操作系統的核心,負責管理進程和線程、記憶體、硬體設備以及提供系統調用介面。電腦啟動過程中,ROM負責載入並執行BIOS程式,而RAM用於存儲運行中的程式和數據。系統調用是操作系統提供給應用程式的介面,通過系統調用可以訪問操作系統的功能。... ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202308/3076680-20230822120346228-1599813347.png) # 1. 不需要考慮排除任何列 ## 1.1. 清除數據表中所有的內容 ## 1.2. 暫存新數據倉庫的數據 # ...
  • [數據治理](https://www.dtstack.com/?src=szsm)是推動大型集團企業轉型升級、提升競爭優勢、實現高質量發展的重要引擎。 通過搭建[大數據平臺](https://www.dtstack.com/?src=szsm),實現對業務系統數據的採集、清理、建模、整合,建立一個符 ...
  • HP DP(Data Protector Manager)上一個剛剛遷移升級的資料庫備份作業失敗,具體失敗信息如下 .................................RMAN-08503: piece handle=c-1684727642-20230822-00 comment=A ...
  • # [TOC] [Android開發中的NDK到底是什麼?(詳細解析+案例) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/415536928) # NDK介紹 **(1)簡介** **定義:**`Native Development Kit`,是 ` ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...