Android子線程更新View的方法原理

????對于所有的Android開發者來說,“View的更新必須在UI線程中進行”是一項最基本常識。

????如果不在UI線程中更新View,系統會拋出CalledFromWrongThreadException異常。那么有沒有什么辦法可以不在UI線程中更新View?答案當然是有的!

一.ViewRootImpl渲染體系

????在Android系統中,ViewRootImpl負責View的繪制調度、事件分發、窗口管理等功能。

????各層級View遵循單一父View對應多個子View的關系,通過嵌套形成樹形結構。

????由于ViewRootImpl不是真正的View,因此ViewRootImpl只是View調度的根節點,并不是View樹的根節點。View樹真正的根節點是DecorView。DecorView繼承自FrameLayout,是真正的View容器。ViewRootImpl通過管理DecorView,間接統籌管理所有層級的View。

1.DecorView的創建

????當啟動Activity時,系統會調用ActivityThread的handleLaunchActivity方法處理Activity的啟動流程。

????在ActivityThread的handleLaunchActivity方法中,會分別調用performLaunchActivity方法、handleStartActivity方法、handleResumeActivity方法,反射創建Activity,并回調Activity的生命周期,如下圖所示:
在這里插入圖片描述
????在實際的開發過程中,通常會在Activity的onCreate方法中,調用setContentView方法,為Activity設置對應的View。在setContentView方法中,會調用installDecor方法,創建DecorView,如下圖所示:
在這里插入圖片描述

2.ViewRootImpl的創建

????在ActivityThread的handleResumeActivity方法中,主要做了兩件事:

1)回調Activity的onResume方法,切換生命周期。

2)調用Activity的makeVisible方法,創建ViewRootImpl與DecorView進行綁定。
在這里插入圖片描述
????在Activity的makeVisible方法中,會通過WindowManager創建ViewRootImpl對象,并與DecorView進行綁定,如下圖所示:
在這里插入圖片描述
????在ViewRootImpl的setView方法中,ViewRootImpl會與DecorView進行雙向綁定,如下圖所示:
在這里插入圖片描述

3.渲染體系與生命周期

????在Activity的首次啟動過程中:

  • 回調onCreate方法時:調用setContentView方法,觸發DecorView的創建。
  • 回調onStart方法時:DecorView完成創建,ViewRootImpl未創建。
  • 回調onResume方法時:DecorView完成創建,ViewRootImpl未創建。回調后立刻創建ViewRootImpl,并與DecorView完成綁定。

二.線程檢測機制

1.異常產生

????CalledFromWrongThreadException異常的拋出發生在ViewRootImpl類的checkThread方法中。當對View進行更新時,最終都會調用ViewRootImpl類的checkThread方法進行線程檢測,代碼如下:

void checkThread() {Thread current = Thread.currentThread();if (mThread != current) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."+ " Expected: " + mThread.getName()+ " Calling: " + current.getName());}
}

????當判斷調用checkThread方法的線程和mThread不一致時,會拋出CalledFromWrongThreadException異常。

2.檢測路徑

????在ViewRootImpl中,共有13個方法在執行時會進行線程檢測。如下所示:

  • requestFitSystemWindows:請求調整View的布局以適應系統窗口。
  • requestLayout:請求重新對View布局。
  • invalidateChildInParent:通知父View某個子View需要重繪。
  • setWindowStopped:設置Window的停止狀態。
  • requestTransparentRegion:請求計算View的透明區域。
  • requestChildFocus:請求將焦點設置到某個子View上。
  • clearChildFocus:清除子View焦點。
  • focusableViewAvailable:通知父View某個子View可以獲取焦點。
  • recomputeViewAttributes:重新計算View的屬性。
  • playSoundEffect:播放與View交互相關的音效。
  • focusSearch:在View樹中搜索下一個可以獲取焦點的View。
  • keyboardNavigationClusterSearch:在鍵盤導航集群中搜索下一個可以獲取焦點的View。
  • doDie:銷毀當前的ViewRootImpl。

????但與View更新最為密切的是requestLayout方法和invalidateChildInParent方法。

????在Android系統中,任何對View的更新操作,最終都要直接或間接調用View的invalidate方法或requestLayout方法。這兩個方法會觸發ViewRootImpl中的相應邏輯,在繪制調度前進行線程檢測。

????View的invalidate方法和requestLayout方法都會觸發ViewRootImpl對View重新進行繪制調度(measure、layout、draw),但二者的區別在于:

  • invalidate方法:標記當前區域為dirty,表示需要重新繪制,并在下一次繪制調度中觸發draw流程,不會觸發measure流程和layout流程。
  • requestLayout方法:清除已經測量的數據,并在下一次繪制調度中觸發measure流程和layout流程,如果在layout過程中發現View的大小發生變化,則會通過調用setFrame方法,間接觸發調用一次invalidate方法,并在下一次繪制調度中觸發draw流程。

1)invalidate方法觸發線程檢測

????當調用View的invalidate方法時,invalidate方法內部會調用父View的invalidateChild方法,通過循環的方式,一層一層的獲取父View,通知重新繪制,最終通知到ViewRootImpl,如下圖所示:
在這里插入圖片描述
????在ViewRootImpl的invalidateChildInParent方法中,會進行線程檢測,代碼如下:

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {// 線程檢測checkThread();...return null;
}

2)requestLayout方法觸發線程檢測

????當調用View的requestLayout方法時,會調用父View的requestLayout方法。通過一層一層的遞歸調用向上通知,最終通知到ViewRootImpl,如下圖所示:
在這里插入圖片描述
????在ViewRootImpl的requestLayout方法中,會進行線程檢測,代碼如下:

@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {// 線程檢測checkThread();mLayoutRequested = true;scheduleTraversals();}
}

三.子線程更新View

????子線程更新View的方式分為兩種:基于獨立渲染體系和基于ViewRootImpl渲染體系。需要注意的是,盡管ViewRootImpl渲染體系支持在子線程更新View,但為了保證View狀態的一致性,還是建議在UI線程更新View。

1.基于獨立渲染體系

1)使用SurfaceView繪制

????SurfaceView依靠自身維護BLASTBufferQueue獲取Surface,在SurfaceFlinger中擁有獨立的Layer。在繪制時不經過ViewRootImpl,詳情參考:SurfaceView與TextureView的繪制渲染,代碼如下:

class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 獲取SurfaceViewval view = findViewById<SurfaceView>(R.id.surface_view)// 創建調度器為IO線程的協程作用域val scope = CoroutineScope(Dispatchers.IO)// 監聽Surface變化view.holder.addCallback(object : SurfaceHolder.Callback {override fun surfaceCreated(holder: SurfaceHolder) {// Surface創建時啟動運行在IO線程的協程scope.launch {while (true) {// 每隔100ms繪制一次背景delay(100)val canvas = holder.lockCanvas()canvas.drawColor(Color.RED)holder.unlockCanvasAndPost(canvas)}}}override fun surfaceChanged(holder: SurfaceHolder,format: Int,width: Int,height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {// Surface銷毀時取消作用域內的協程scope.cancel()}})}
}

2)使用TextureView繪制

????TextureView依靠自身維護的SurfaceTexture獲取Surface,在繪制時不經過ViewRootImpl。

????但與SurfaceView不同的是,通過TextureView的Surface繪制后的內容,不會直接提交到SurfaceFlinger,而是通過回調的方式觸發調用一次invalidate方法,并在下一次繪制時通過硬件加速層的方式掛在View樹下一起繪制,詳情參考:SurfaceView與TextureView的繪制渲染,代碼如下:

class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 獲取TextureViewval view = findViewById<TextureView>(R.id.texture_view)// 創建調度器為IO線程的協程作用域val scope = CoroutineScope(Dispatchers.IO)// 監聽SurfaceTexture變化view.surfaceTextureListener = object : TextureView.SurfaceTextureListener {override fun onSurfaceTextureAvailable(surface: SurfaceTexture,width: Int,height: Int) {// SurfaceTexture創建時啟動運行在IO線程的協程scope.launch {while (true) {// 每隔100ms繪制一次背景delay(100)val canvas = view.lockCanvas() ?: continuecanvas.drawColor(Color.RED)view.unlockCanvasAndPost(canvas)}}}override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture,width: Int,height: Int) {}override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {// SurfaceTexture銷毀時取消作用域內的協程scope.cancel()return true}override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}}}
}

3)接管ViewRootImpl的Surface

????當在Activity中調用Window的takeSurface方法,會接管ViewRootImpl的Surface,Activity的渲染會脫離ViewRootImpl渲染體系,相當于整個Activity都變成了SurfaceView,代碼如下:

class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 創建調度器為IO線程的協程作用域val scope = CoroutineScope(Dispatchers.IO)// 接管ViewRootImpl的Surfacewindow.takeSurface(object : SurfaceHolder.Callback2 {override fun surfaceCreated(holder: SurfaceHolder) {// Surface創建時啟動運行在IO線程的協程scope.launch {while (true) {// 每隔100ms繪制一次背景delay(100)val canvas = holder.lockCanvas()canvas.drawColor(Color.RED)holder.unlockCanvasAndPost(canvas)}}}override fun surfaceChanged(holder: SurfaceHolder,format: Int,width: Int,height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {// Surface銷毀時取消作用域內的協程scope.cancel()}override fun surfaceRedrawNeeded(holder: SurfaceHolder) {}})}
}

2.基于ViewRootImpl渲染體系

1)ViewRootImpl渲染體系形成前

????當Activity首次啟動并在onCreate方法內調用setContentView方法后,在onCreate方法、onStart方法、onResume方法中,使用非UI線程更新View,不會觸發線程檢測,代碼如下:

class TestActivity : AppCompatActivity() {// 創建調度器為IO線程的協程作用域private val scope = CoroutineScope(Dispatchers.IO)// 標記在onResume方法中執行一次private var firstResume = true// 標記在onStart方法中執行一次private var firstStart = trueoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// onCreate方法中,啟動運行在IO線程的協程scope.launch {// 更新TextView的文字內容findViewById<TextView>(R.id.text_view)?.text = "hello world"}}override fun onStart() {super.onStart()// 使用標志位,確保只在首次調用onStart時執行if(!firstStart) returnfirstStart = false// onStart方法中,啟動運行在IO線程的協程scope.launch {// 更新TextView的文字內容findViewById<TextView>(R.id.text_view)?.text = "hello world !"}}override fun onResume() {super.onResume()// 使用標志位,確保只在首次調用onResume時執行if (!firstResume) returnfirstResume = false// onResume方法中,啟動運行在IO線程的協程scope.launch {// 更新TextView的文字內容findViewById<TextView>(R.id.text_view)?.text = "hello world !!"}}
}

2)綁定ViewRootImpl渲染體系前

????當動態創建完View后,在沒有添加到與ViewRootImpl有關聯的ViewGroup前,在非UI線程更新View,不會觸發線程檢測,代碼如下:

class TestActivity : AppCompatActivity() {// 創建調度器為IO線程的協程作用域private val scope = CoroutineScope(Dispatchers.IO)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 獲取TextView,監聽點擊事件findViewById<TextView>(R.id.text_view)?.setOnClickListener {// 當點擊TextView時,啟動運行在IO線程的協程scope.launch {// 創建一個TextViewval view = TextView(this@TestActivity)// 設置文本內容view.text = "hello world"// 切換到UI線程withContext(Dispatchers.Main) {// 添加到DecorView中this@TestActivity.addContentView(view, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT))}}}}
}

4)硬件渲染模式下的invalidate方法

????在軟件渲染模式下,當調用View的invalidate方法時,會調用父類的invalidateChild方法。但在硬件渲染模式下,為了防止循環遍歷耗時,會直接調用onDescendantInvalidated方法,代碼如下:

@Override
public final void invalidateChild(View child, final Rect dirty) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null && attachInfo.mHardwareAccelerated) {// HW accelerated fast pathonDescendantInvalidated(child, child);return;}...
}

????在ViewGroup的onDescendantInvalidated方法中,會通過遞歸調用的方式,最終調用ViewRootImpl的onDescendantInvalidated方法,如下圖所示:
在這里插入圖片描述
????在ViewRootImpl的onDescendantInvalidated方法中,會直接調用invalidate方法,跳過線程檢查,代碼如下:

private static boolean sToolkitEnableInvalidateCheckThreadFlagValue =Flags.enableInvalidateCheckThread();@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {// Android Tool Kit為debug留的開關,默認為falseif (sToolkitEnableInvalidateCheckThreadFlagValue) {checkThread();}if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {mIsAnimating = true;}invalidate();
}@UnsupportedAppUsage
void invalidate() {mDirty.set(0, 0, mWidth, mHeight);if (!mWillDrawSoon) {// 啟動繪制流程scheduleTraversals();}
}

????Android系統默認的渲染模式為硬件渲染,這里在AndroidManifest中再手動聲明一下,代碼如下:

<manifest xmlns:tools="http://schemas.android.com/tools"xmlns:android="http://schemas.android.com/apk/res/android"package="com.test.ui">...<!-- 啟動應用級別的硬件渲染模式 --><application android:hardwareAccelerated="true">...</application></manifest>

????在代碼使用上,硬件渲染與軟件渲染基本沒有差別。當開啟硬件渲染模式后,在子線程直接或間接調用View的invalidate方法不會產生崩潰,代碼如下:

class TestActivity : AppCompatActivity() {// 創建調度器為IO線程的協程作用域private val scope = CoroutineScope(Dispatchers.IO)// 文字大小private var size = 30foverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)}override fun onResume() {super.onResume()// 每次onResume時,啟動一個運行在IO線程的協程scope.launch {// 更新文字大小size += 10f// 獲取TextView,設置文字大小findViewById<TextView>(R.id.text_view)?.textSize = size}}
}

5)子線程中創建ViewRootImpl

????實際上,Android系統并未要求View的更新必須在UI線程中進行。

????通過分析CalledFromWrongThreadException異常拋出時的提示可以知道:View的更新必須在original thread中。而original thread就是ViewRootImpl中mThread字段保存的線程。

void checkThread() {Thread current = Thread.currentThread();if (mThread != current) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."+ " Expected: " + mThread.getName()+ " Calling: " + current.getName());}
}

????在ViewRootImpl的構造方法中,會對mThread進行初始化,代碼如下:

public ViewRootImpl(@UiContext Context context,Display display,IWindowSession session,WindowLayout windowLayout) {...// 獲取當前的線程并保存mThread = Thread.currentThread();...
}

????因此,Android系統要求View更新必須在UI線程執行,本質上是因為ViewRootImpl在UI線程被創建,并在構造方法中保存當前線程引用(mThread),并在每次操作時通過checkThread方法驗證調用線程是否與mThread一致。

????由于Activity的啟動需要系統調度,系統會將Activity的啟動安排在UI線程中進行,這也就導致無法在子線程中啟動Activity,進而無法在子線程中創建ViewRootImpl。

????但是在Android系統中,不僅Activity擁有ViewRootImpl,Dialog和PopupWindow等組件也各自擁有獨立的ViewRootImpl。

????如果在子線程中創建了Dialog或PopupWindow,那么后續對Dialog或PopupWindow中View的更新也必須在該子線程中進行,代碼如下:

class TestActivity : AppCompatActivity() {// 創建HandlerThread,并啟動子線程updateprivate val handleThread = HandlerThread("update").apply { start() }override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 創建子線程Handler,并向子線程update中提交一個任務Handler(handleThread.looper).post {// 獲取容器Viewval parent = findViewById<ViewGroup>(R.id.container)// 通過加載XML的方式,創建一個子Viewval view = LayoutInflater.from(this).inflate(R.layout.layout_test_popup_window, parent, false)// 創建PopupWindow,并將子View添加進去val popupWindow = PopupWindow(view,ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)// 從子View中獲取TextViewval textView = popupWindow.contentView.findViewById<TextView>(R.id.pop_text)// 監聽TextView的點擊事件textView.setOnClickListener {// 更新TextView的文字內容,// 這里注意點擊事件的回調線程變成了子線程updatetextView.text = "${Thread.currentThread()}"// 這里會產生CalledFromWrongThreadException異常// 因為沒有在子線程update中更新window.decorView.post { textView.text = "${Thread.currentThread()}" }}// 這里先將任務提交到UI線程執行// 因為在onCreate方法中,容器View對應的Window還未創建好// 獲取不到Window的Token,會產生異常window.decorView.post {// 切換到子線程,創建子線程Handler,并向子線程update中提交一個任務Handler(handleThread.looper).post {// 子線程中展示popupWindow,會觸發ViewRootImpl在子線程update中創建popupWindow.showAtLocation(parent, Gravity.CENTER, 0, 0)}}}}
}

三.總結

1.View的更新必須在UI線程進行的原因

????ViewRootImpl在UI線程中被創建,并在構造方法中保存了當前線程的引用(mThread)。在每次更新View時,通過調用View的invalidate方法或requestLayout方法觸發ViewRootImpl的checkThread方法,驗證調用線程是否與mThread一致。

2.Activity啟動流程中渲染體系的創建

  • 回調onCreate方法時:調用setContentView方法,觸發DecorView的創建。
  • 回調onStart方法時:DecorView完成創建,ViewRootImpl未創建。
  • 回調onResume方法時:DecorView完成創建,ViewRootImpl未創建。回調后立刻創建ViewRootImpl,并與DecorView完成綁定。

3.invalidate方法與requestLayout方法的區別

????View的invalidate方法和requestLayout方法都會觸發ViewRootImpl對View重新進行繪制調度(measure、layout、draw),但二者的區別在于:

  • invalidate方法:標記當前區域為dirty,表示需要重新繪制,并在下一次繪制調度中觸發draw流程,不會觸發measure流程和layout流程。
  • requestLayout方法:清除已經測量的數據,并在下一次繪制調度中觸發measure流程和layout流程,如果在layout過程中發現View的大小發生變化,則會通過調用setFrame方法,間接觸發調用一次invalidate方法,并在下一次繪制調度中觸發draw流程。

4.子線程更新View的方法

  • 基于獨立渲染體系
    • 使用SurfaceView,直接對Surface進行繪制。
    • 使用TextureView,直接對Surface進行繪制。
    • 接管ViewRootImpl的Surface,直接對Surface進行繪制。
  • 基于ViewRootImpl渲染體系
    • 在ViewRootImpl渲染體系形成前,使用子線程更新View。
    • 在綁定ViewRootImpl渲染體系前,使用子線程更新View。
    • 硬件渲染模式下,子線程直接或間接調用View的invalidate方法。
    • 對于獨立擁有ViewRootImpl的組件,在子線程中觸發組件創建ViewRootImpl,并在對應的子線程中更新View。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/73260.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/73260.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/73260.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Manus資料合集】激活碼內測渠道+《Manus Al:Agent應用的ChatGPT時刻》(附資源)

DeepSeek 之后&#xff0c;又一個AI沸騰&#xff0c;沖擊的不僅僅是通用大模型。 ——全球首款通用AI Agent的破圈啟示錄 2025年3月6日凌晨&#xff0c;全球AI圈被一款名為Manus的產品徹底點燃。由Monica團隊&#xff08;隸屬中國夜鶯科技&#xff09;推出的“全球首款通用AI…

Python----計算機視覺處理(opencv:像素,RGB顏色,圖像的存儲,opencv安裝,代碼展示)

一、計算機眼中的圖像 像素 像素是圖像的基本單元&#xff0c;每個像素存儲著圖像的顏色、亮度和其他特征。一系列像素組合到一起就形成 了完整的圖像&#xff0c;在計算機中&#xff0c;圖像以像素的形式存在并采用二進制格式進行存儲。根據圖像的顏色不 同&#xff0c;每個像…

SQLiteStudio:一款免費跨平臺的SQLite管理工具

SQLiteStudio 是一款專門用于管理和操作 SQLite 數據庫的免費工具。它提供直觀的圖形化界面&#xff0c;簡化了數據庫的創建、編輯、查詢和維護&#xff0c;適合數據庫開發者和數據分析師使用。 功能特性 SQLiteStudio 提供的主要功能包括&#xff1a; 免費開源&#xff0c;可…

【軟考網工-實踐篇】DHCP 動態主機配置協議

一、DHCP簡介 DHCP&#xff0c;Dynamic Host Configuration Protocol&#xff0c;動態主機配置協議。 位置&#xff1a;DHCP常見運行于路由器上&#xff0c;作為DHCP服務器功能&#xff1a;用于自動分配IP地址及其他網絡參數給網絡中的設備作用&#xff1a;簡化網絡管理&…

【Linux學習筆記】Linux用戶和文件權限的深度剖析

【Linux學習筆記】Linux用戶和文件權限的深度剖析 &#x1f525;個人主頁&#xff1a;大白的編程日記 &#x1f525;專欄&#xff1a;Linux學習筆記 前言 文章目錄 【Linux學習筆記】Linux用戶和文件權限的深度剖析前言一. Linux權限管理1.1 文件訪問者的分類&#xff08;人)…

Centos離線安裝openssl-devel

文章目錄 Centos離線安裝openssl-devel1. openssl-devel是什么&#xff1f;2. openssl-devel下載地址3. openssl-devel安裝4. 安裝結果驗證 Centos離線安裝openssl-devel 1. openssl-devel是什么&#xff1f; openssl-devel 是 Linux 系統中與 OpenSSL 加密庫相關的開發包&…

深度學習篇---Opencv中Haar級聯分類器的自定義

文章目錄 1. 準備工作1.1安裝 OpenCV1.2準備數據集1.2.1正樣本1.2.2負樣本 2. 數據準備2.1 正樣本的準備2.1.1步驟2.1.2生成正樣本描述文件2.1.3示例命令2.1.4正樣本描述文件格式 2.2 負樣本的準備2.2.1步驟2.2.2負樣本描述文件格式 3. 訓練分類器3.1命令格式3.2參數說明 4. 訓…

Smart Time Plus smarttimeplus-MySQLConnection SQL注入漏洞(CVE-2024-53544)

免責聲明 本文所描述的漏洞及其復現步驟僅供網絡安全研究與教育目的使用。任何人不得將本文提供的信息用于非法目的或未經授權的系統測試。作者不對任何由于使用本文信息而導致的直接或間接損害承擔責任。如涉及侵權,請及時與我們聯系,我們將盡快處理并刪除相關內容。 0x01…

58.Harmonyos NEXT 圖片預覽組件架構設計與實現原理

溫馨提示&#xff1a;本篇博客的詳細代碼已發布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下載運行哦&#xff01; Harmonyos NEXT 圖片預覽組件架構設計與實現原理 文章目錄 Harmonyos NEXT 圖片預覽組件架構設計與實現原理效果預覽一、組件架構概述1. 核心組件層…

虛擬機下ubuntu進不了圖形界面

6.844618] piix4_smbus 0000:07.3: SMBus Host ContrFoller not enabled! 7.859836] sd 2:0:0:0:0: [sda] Assuming drive cache: wirite through /dev/sda1: clean, 200424/1966080 files, 4053235/7864064 blocks ubuntu啟動時&#xff0c;卡在上面輸出位置 當前遇到的原因…

Appium高級操作--從源碼角度解析--模擬復雜手勢操作

書接上回&#xff0c;Android自動化--Appium基本操作-CSDN博客文章瀏覽閱讀600次&#xff0c;點贊10次&#xff0c;收藏5次。書接上回&#xff0c;上一篇文章已經介紹了appium在Android端的元素定位方法和識別工具Inspector&#xff0c;本次要介紹使用如何利用Appium對找到的元…

SpringBoot學生宿舍管理系統的設計與開發

項目概述 幽絡源分享的《SpringBoot學生宿舍管理系統的設計與開發》是一款專為校園宿舍管理設計的智能化系統&#xff0c;基于SpringBoot框架開發&#xff0c;功能全面&#xff0c;操作便捷。該系統涵蓋管理員、宿管員和學生三大角色&#xff0c;分別提供宿舍管理、學生信息管…

愛普生溫補晶振 TG5032CFN高精度穩定時鐘的典范

在科技日新月異的當下&#xff0c;眾多領域對時鐘信號的穩定性與精準度提出了極為嚴苛的要求。愛普生溫補晶振TG5032CFN是一款高穩定性溫度補償晶體振蕩器&#xff08;TCXO&#xff09;。該器件通過內置溫度補償電路&#xff0c;有效抑制環境溫度變化對頻率穩定性的影響&#x…

【原創】在高性能服務器上,使用受限用戶運行Nginx,充當反向代理服務器[未完待續]

起因 在公共高性能服務器上運行OllamaDeepSeek&#xff0c;如果按照默認配置啟動Ollama程序&#xff0c;則自己在遠程無法連接你啟動的Ollama服務。 如果修改配置&#xff0c;則會遇到你的Ollama被他人完全控制的安全風險。 不過&#xff0c;我們可以使用一個方向代理&#…

Bash和Zsh的主要差異是?

Bash&#xff08;GNU Bourne-Again Shell&#xff09; 和 Zsh&#xff08;Z Shell&#xff09; 都是功能強大的Unix/Linux Shell&#xff0c;廣泛用于交互式使用和腳本編寫。 盡管它們有很多相似之處&#xff0c;但在功能、語法、配置選項等方面也存在一些顯著的區別。 是Bas…

芯科科技推出的BG29超小型低功耗藍牙?無線SoC,是藍牙應用的理想之選

具有擴大的內存和超低功耗特性的超小型BG29是互聯健康設備的理想之選 低功耗無線領域內的領導性創新廠商Silicon Labs&#xff08;亦稱“芯科科技”&#xff0c;NASDAQ&#xff1a;SLAB&#xff09;今日宣布&#xff1a;推出全新的第二代無線開發平臺產品BG29系列無線片上系統…

【數據挖掘】知識蒸餾(Knowledge Distillation, KD)

1. 概念 知識蒸餾&#xff08;Knowledge Distillation, KD&#xff09;是一種模型壓縮和知識遷移技術&#xff0c;旨在將大型復雜模型&#xff08;稱為教師模型&#xff09;中的知識傳遞給一個較小的模型&#xff08;稱為學生模型&#xff09;&#xff0c;以減少計算成本&…

選型消息隊列(MQ):ActiveMQ、RabbitMQ、RocketMQ、Kafka對比

選型消息隊列&#xff08;MQ&#xff09;&#xff1a;ActiveMQ、RabbitMQ、RocketMQ、Kafka對比 選型消息隊列&#xff08;MQ&#xff09;1. 引言2. 消息隊列核心指標3. MQ 技術對比分析4. 詳細分析及案例4.1 ActiveMQ&#xff1a;傳統企業級 MQ 方案4.2 RabbitMQ&#xff1a;高…

AWK 入門教程:強大的文本處理工具

AWK 是一種強大的文本處理工具&#xff0c;廣泛用于 Linux/Unix 系統中對文本文件或數據流進行操作。它能夠基于條件篩選、統計字段、重新排列數據等。主要特點包括&#xff1a; 2. AWK 的基本語法 2.1 AWK 程序的結構 AWK 程序的結構: awk pattern { action } file 2.2 常…

mysql select distinct 和 group by 哪個效率高

在有索引的情況下&#xff0c;SELECT DISTINCT和GROUP BY的效率相同&#xff1b;在沒有索引的情況下&#xff0c;SELECT DISTINCT的效率高于GROUP BY?。這是因為SELECT DISTINCT和GROUP BY都會進行分組操作&#xff0c;但GROUP BY可能會進行排序&#xff0c;觸發filesort&…