Android Jetpack Compose 中的分頁與緩存展示

Android Jetpack Compose 中的分頁與緩存展示

在幾乎任何類型的移動項目中,移動開發人員在某個時候都會處理分頁數據。如果數據列表太大,無法一次從服務器檢索完畢,這就是必需的。因此,我們的后端同事為我們提供了一個端點,返回分頁數據列表,并期望我們知道如何在客戶端處理它。
在本文中,我們將重點介紹如何使用 Android 在 2023 年 6 月推薦的最新方法來獲取、緩存和顯示分頁數據。我們將經過以下步驟:

  • 從公共 GraphQL API 中按頁獲取 Pokemon 數據列表
  • 使用 Room 將獲取的數據緩存到本地數據庫
  • 使用最新的 Paging 庫組件來處理分頁
  • 使用 LazyColumn 智能地顯示頁面項(只渲染可見內容)

對于示例項目,我將在文章末尾分享 GitHub 存儲庫鏈接,我們將使用 Hilt 作為我們的依賴注入庫,并使用干凈架構(表示層 → 領域層 ← 數據層)。因此,我將從數據層開始解釋事物,然后轉向領域層,最后結束在表示層。

數據層

這一層是關于分頁和緩存的大部分內容。因此,如果您能夠通過這一部分,您將基本完成了它。

遠程數據源

作為遠程數據源,我們將使用一個公共的 GraphQL Pokemon API。與我們用于與 REST API 交互的 Retrofit 不同,我們使用 Apollo 的 Kotlin 客戶端來處理 GraphQL API。它允許我們執行 GraphQL 查詢,并根據請求和響應自動生成 Kotlin 模型。
首先,我們需要將以下行添加到我們的模塊級別的 build.gradle 文件中:

plugins {// ...id "com.apollographql.apollo3" version "$apollo_version"
}apollo {service("pokemon") {packageName.set("dev.thunderbolt.pokemonpager.data")}
}dependencies {// ...implementation "com.apollographql.apollo3:apollo-runtime:$apollo_version"
}

在這里,我們在 apollo 塊中設置了 Apollo 庫的配置。它提供了許多設置,您可以通過其文檔查看所有設置。目前,我們只需要將包名設置為 dev.thunderbolt.pokemonpager.data,這樣生成的 Kotlin 文件將位于正確的包中,也就是數據層。

然后,我們需要下載服務器的模式,以便庫能夠生成模型,并且我們可以使用自動完成來編寫查詢。為了下載模式,我們使用 Apollo 提供的以下命令:

./gradlew :app:downloadApolloSchema --endpoint='https://graphql-pokeapi.graphcdn.app/graphql' --schema=app/src/main/graphql/schema.graphqls

這將在 app/src/main/graphql/schema.graphqls 目錄中下載服務器的模式。

現在,是時候在一個名為 pokemon.graphql 的文件中編寫我們的查詢,該文件與模式文件位于同一文件夾中。

query PokemonList($offset: Int!$limit: Int!
) {pokemons(offset: $offset,limit: $limit) {nextOffsetresults {idnameimage}}
}

當我們構建項目時,Apollo Kotlin 將通過自動運行名為 generateApolloSources 的 Gradle 任務為此查詢生成模型。

回到 Kotlin 的世界,我們將定義我們的 PokemonApi 類,以封裝與 GraphQL 的所有交互,如下所示:

class PokemonApi {private val BASE_URL = "https://graphql-pokeapi.graphcdn.app/graphql"private val apolloClient = ApolloClient.Builder().serverUrl(BASE_URL).addHttpInterceptor(LoggingInterceptor()).build()suspend fun getPokemonList(offset: Int, limit: Int): PokemonListQuery.Pokemons? {val response = apolloClient.query(PokemonListQuery(offset = offset,limit = limit,)).execute()// IF RESPONSE HAS ERRORS OR DATA IS NULL, THROW EXCEPTIONif (response.hasErrors() || response.data == null) {throw ApolloException(response.errors.toString())}return response.data!!.pokemons}
}

在這里,我們使用所需的配置初始化 Apollo Client 實例,并實現了我們執行在 pokemon.graphql 文件中編寫的生成的 Kotlin 版本查詢的函數。該函數基本上會獲取 offsetlimit 參數,執行查詢,如果一切順利,就會返回查詢的響應,這也是由 Apollo 自動生成的。

本地數據源/存儲

為了在本地存儲關系型數據并創建一個離線優先的應用程序,我們將依賴于 Room,這是一個在 SQLite 之上編寫的 Android 持久性庫。

首先,我們需要將 Room 依賴項添加到我們的 build.gradle 文件中:

dependencies {// ...implementation "androidx.room:room-ktx:$room_version"kapt "androidx.room:room-compiler:$room_version"implementation "androidx.room:room-paging:$room_version"
}

然后,我們將定義兩個實體類,一個用于在我們的數據庫中存儲 Pokemon 數據,另一個用于跟蹤要獲取的下一頁的頁數。

@Entity("pokemon")
data class PokemonEntity(@PrimaryKey val id: Int,val name: String,val imageUrl: String,
)@Entity("remote_key")
data class RemoteKeyEntity(@PrimaryKey val id: String,val nextOffset: Int,
)

在這方面,我們還需要兩個 DAO(數據訪問對象)類來定義其中的所有數據庫交互。

@Dao
interface PokemonDao {@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insertAll(items: List<PokemonEntity>)@Query("SELECT * FROM pokemon")fun pagingSource(): PagingSource<Int, PokemonEntity>@Query("DELETE FROM pokemon")suspend fun clearAll()
}@Dao
interface RemoteKeyDao {@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insert(item: RemoteKeyEntity)@Query("SELECT * FROM remote_key WHERE id = :id")suspend fun getById(id: String): RemoteKeyEntity?@Query("DELETE FROM remote_key WHERE id = :id")suspend fun deleteById(id: String)
}

在這里,我們需要特別關注的關鍵函數是 pagingSource()。Room 可以返回數據列表作為 PagingSource,以便我們稍后將創建的 Pager 對象將其用作生成 PagingData 流的單一源。

最后,我們需要一個 RoomDatabase 類,在本地數據庫中為這些實體創建表,并提供 DAO 以與這些表進行交互。

@Database(entities = [PokemonEntity::class, RemoteKeyEntity::class],version = 1,
)
abstract class PokemonDatabase : RoomDatabase() {abstract val pokemonDao: PokemonDaoabstract val remoteKeyDao: RemoteKeyDao
}

這兩個類,即 PokemonDatabase 和之前定義的 PokemonApi 類,都由我們數據層的 Hilt 模塊實例化并提供為單例對象。

@Module
@InstallIn(SingletonComponent::class)
class DataModule {@Provides@Singletonfun providePokemonDatabase(@ApplicationContext context: Context): PokemonDatabase {return Room.databaseBuilder(context,PokemonDatabase::class.java,"pokemon.db",).fallbackToDestructiveMigration().build()}@Provides@Singletonfun providePokemonApi(): PokemonApi {return PokemonApi()}// ...
}

遠程中介器(Remote Mediator)

現在,我們要實現我們的遠程中介器類(RemoteMediator),它將負責在需要時從遠程 API 加載分頁數據到本地數據庫中。需要注意的是,遠程中介器并不直接向用戶界面提供數據。如果分頁數據用盡,分頁庫會觸發遠程中介器的 load(…) 方法,以從遠程獲取并存儲更多的數據到本地。因此,我們的本地數據庫始終可以保持作為唯一的真實數據源。

load(…) 函數中,我們首先需要檢查我們正在處理哪種類型的加載。如果 LoadType 是:

  • REFRESH,這意味著我們要么處于初始加載狀態,要么數據已經無效,我們需要從頭開始獲取數據。因此,如果是這種情況,我們將偏移值設置為 “0”,以獲取第一頁的數據。
  • PREPEND,我們需要獲取當前頁面之前的頁面數據。在這個示例的范圍內,不需要在向上滾動時獲取任何內容。因此,我們只需返回 MediatorResult.Success(endOfPaginationReached = true),以指示不應再進行數據加載。
  • APPEND,我們需要獲取當前頁面之后的頁面數據。在這種情況下,我們會獲取已經由前一個數據加載存儲在本地數據庫中的遠程鍵(remote key)對象。如果沒有或者其 nextOffset 值為 “0”,則表示沒有更多數據可加載和追加。順便說一下,這就是該 API 的工作方式。你的 API 可能以不同方式指示數據的結束,因此需要相應地編寫你的 APPEND 邏輯。

在確定了正確的偏移值之后,現在是時候使用此偏移值和配置中提供的 pageSize 進行 API 調用了。我們將在下一步創建 Pager 對象時設置頁面大小。

如果 API 調用成功返回新的頁面數據,我們將使用相應的 DAO 函數將項目和下一個偏移值存儲在我們的數據庫中。在這里,我們需要在事務塊中執行所有數據庫交互,以便如果任何交互失敗,數據庫不會發生任何更改。

最后,如果在數據庫調用之后一切順利,我們將返回 MediatorResult.Success,通過將最新加載返回的項目數與我們將在配置中定義的頁面大小進行比較,來檢查是否已達到分頁的末尾。

Pager 對象

現在,我們要再次回到我們數據層的 Hilt 模塊,并創建我們的 Pager 對象。這個對象將把我們到目前為止所定義的所有內容整合在一起,作為 PagingData 流的構造函數工作。

@Module
@InstallIn(SingletonComponent::class)
class DataModule {// ...@Provides@Singletonfun providePokemonPager(pokemonDatabase: PokemonDatabase,pokemonApi: PokemonApi,): Pager<Int, PokemonEntity> {return Pager(config = PagingConfig(pageSize = 20),remoteMediator = PokemonRemoteMediator(pokemonDatabase = pokemonDatabase,pokemonApi = pokemonApi,),pagingSourceFactory = {pokemonDatabase.pokemonDao.pagingSource()},)}
}

在這里,我們向 Pager 的構造函數提供了三個要素。首先,我們設置了所需的頁面大小的 PagingConfig,正如我之前提到的。其次,我們提供了我們的遠程中介器實例。第三,我們將由 Room 提供的分頁源設置為 Pager 的唯一數據源。

倉庫(Repository)

由于我們在遠程中介器中完成了大部分工作,所以我們的倉庫實現將相當簡單。

class PokemonRepositoryImpl @Inject constructor(private val pokemonPager: Pager<Int, PokemonEntity>
) : PokemonRepository {override fun getPokemonList(): Flow<PagingData<Pokemon>> {return pokemonPager.flow.map { pagingData ->pagingData.map { it.toPokemon() }}}
}

使用我們的 Pager 實例,我們只需將其 PagingData 流返回給使用者。但在這之前,我們還需要將 PokemonEntity 映射到領域的 Pokemon 模型。這是因為根據 Clean Architecture 的基礎,我們的領域層不了解數據或表示層,因此不應將數據模型傳遞到領域層。

領域層(Domain Layer)

在這個純 Kotlin 層中,實際上沒有太多事情發生。在這里,我們有我們的 Pokemon 模型、倉庫接口以及與該倉庫交互的簡單用例類。

// REPOSITORY INTERFACE
interface PokemonRepository {fun getPokemonList(): Flow<PagingData<Pokemon>>
}// USE CASE
class GetPokemonList @Inject constructor(private val pokemonRepository: PokemonRepository
) {operator fun invoke(): Flow<PagingData<Pokemon>> {return pokemonRepository.getPokemonList().flowOn(Dispatchers.IO)}
}// MODEL
data class Pokemon(val id: Int,val name: String,val imageUrl: String,
)

在這里,你可能會有一個問題,即如何在純 Kotlin 層中使用PagingData,而在這里我們沒有依賴于任何 Android 組件。實際上很簡單:分頁庫為非 Android 模塊提供了特定的依賴項,因此我們可以訪問所有簡單的 Paging 組件,如 PagingSource、PagingData、Pager,甚至是 RemoteMediator

dependencies {// ...implementation "androidx.paging:paging-common:$paging_version"
}

表示層(Presentation Layer)
在快速涵蓋了領域層之后,讓我們直接跳入表示層,其中的關鍵內容都在這里。但首先,我們需要將以下 Paging 依賴項添加到我們的 build.gradle 文件中:

dependencies {// ...implementation "androidx.paging:paging-runtime-ktx:$paging_version"implementation "androidx.paging:paging-compose:$paging_version"
}

除了 runtime-ktx 依賴項之外,這里還需要 compose 依賴項,因為它在我們的分頁數據流和 UI 之間提供了一些中間件。

ViewModel

這又是本文中的一個簡單類,在這里我們只需獲取由用例提供的流(該流已由倉庫提供),并將其存儲在一個值中。

@HiltViewModel
class PokemonListViewModel @Inject constructor(private val getPokemonList: GetPokemonList
) : ViewModel() {val pokemonPagingDataFlow: Flow<PagingData<Pokemon>> = getPokemonList().cachedIn(viewModelScope)
}

我們通過調用cachedIn(viewModelScope)來存儲該流,以便在 ViewModel 的生命周期內保持其活動狀態。此外,它還可以在屏幕旋轉等配置更改時保持存活,這樣你就可以獲取相同的現有數據,而不必從頭開始獲取。
這種方法還可以保持我們的冷流狀態不變,并且不會像 stateIn(…) 方法一樣將其轉換為熱流(StateFlow)。這意味著如果流未被收集,就不會執行不必要的代碼。

屏幕(UI)

現在,我們來到了分頁的最后一步,在這一步中,我們將在LazyColumn中顯示我們的分頁項。在 Jetpack Compose 中,不再有 RecyclerView 或適配器。所有這些都在下面進行處理,而且我們大量的項目仍然可以智能布局,而不會引起任何性能問題。

@Composable
fun PokemonListScreen(snackbarHostState: SnackbarHostState
) {val viewModel = hiltViewModel<PokemonListViewModel>()val pokemonPagingItems = viewModel.pokemonPagingDataFlow.collectAsLazyPagingItems()if (pokemonPagingItems.loadState.refresh is LoadState.Error) {LaunchedEffect(key1 = snackbarHostState) {snackbarHostState.showSnackbar((pokemonPagingItems.loadState.refresh as LoadState.Error).error.message ?: "")}}Box(modifier = Modifier.fillMaxSize()) {if (pokemonPagingItems.loadState.refresh is LoadState.Loading) {CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))} else {LazyColumn(modifier = Modifier.fillMaxSize(),horizontalAlignment = Alignment.CenterHorizontally,) {items(count = pokemonPagingItems.itemCount,key = pokemonPagingItems.itemKey { it.id },) { index ->val pokemon = pokemonPagingItems[index]if (pokemon != null) {PokemonItem(pokemon,modifier = Modifier.fillMaxWidth(),)}}item {if (pokemonPagingItems.loadState.append is LoadState.Loading) {CircularProgressIndicator(modifier = Modifier.padding(16.dp))}}}}}
}

在我們的組合屏幕中,首先要做的是創建我們的 ViewModel 實例,并使用輔助函數 collectAsLazyPagingItems() 收集其中存儲的分頁數據流。這將冷流轉換為 LazyPagingItems 實例。通過這個實例,我們可以訪問已加載的項目,以及不同的加載狀態,以相應地改變 UI。除此之外,我們甚至可以使用此實例觸發數據刷新或重新嘗試以前失敗的加載。

在 Box 布局中,如果 LazyPagingItems 的“refresh”加載狀態為 Loading,則我們知道我們正在初始加載,并且尚無項目可顯示。因此,我們顯示一個進度指示器。否則,我們會顯示一個 LazyColumn,以及使用我們的 LazyPagingItems 實例設置的項目列表的數量和鍵參數。在每個項目中,我們只需使用給定的索引訪問相應的 Pokemon 對象,并呈現 PokemonItem 組合,出于簡單起見,這里不給出實現細節。

我們還有一種特殊情況,即需要在這些項目下方顯示加載指示器。這發生在我們正在獲取更多數據的過程中,可以通過 LazyPagingItems 的“append”加載狀態來檢測到。因此,如果是這種情況,我們將一個進度指示器追加到列表的末尾。

最后,請不要認為我們在開始部分忽略了LaunchedEffect部分。LaunchedEffect 組合用于在組合內部安全地調用掛起函數。在 Jetpack Compose 中,我們需要協程范圍來顯示 Snackbar,因為 SnackbarHostState.showSnackbar(…) 是一個掛起函數。在這里,我們顯示一個 Snackbar 消息,以防刷新錯誤,基本上對應于我們的情況下的“初始加載”錯誤。然而,正如我之前提到的,我們在這里構建了一個離線優先的應用,因此如果我們在 Room 中已經緩存了數據,用戶將看到該數據,以及錯誤消息。

希望您在 Android Jetpack Compose 中的分頁和緩存的這段具有挑戰性的旅程中能夠與我同行。我盡力堅持最新和推薦的操作方式。請隨時指出錯誤或可以做得更好的地方。整個項目已經作為 GitHub 存儲庫共享,以便您可以下載并進行測試。

GitHub

https://github.com/thunderbolt-codes/Pokemon-Pager

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

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

相關文章

ArcGIS Pro應用—暨基礎入門、制圖、空間分析、影像分析、三維建模、空間統計分析與建模、python融合、案例應用全流程科研能力提升教程

詳情點擊鏈接&#xff1a;ArcGIS Pro應用—暨基礎入門、制圖、空間分析、影像分析、三維建模、空間統計分析與建模、python融合、案例應用全流程科研能力提升教程 第一&#xff1a;GIS及ArcGIS Pro 1.GIS基本原理及常用軟件 2.ArcGIS Pro 安裝與配置 3.ArcGIS Pro 3.0 的新…

C語言自動抓取淘寶商品詳情網頁數據,實現輕松高效爬蟲

你是否曾經遇到過需要大量獲取網頁上的數據&#xff0c;但手動復制粘貼又太過費時費力&#xff1f;那么這篇文章就是為你而寫。今天我們將會詳細討論如何使用C語言實現自動抓取網頁上的數據。本文將會從以下8個方面進行逐步分析討論。 1. HTTP協議的基本原理 在開始之前&…

小白到運維工程師自學之路 第七十三集 (kubernetes應用部署)

一、安裝部署 1、以Deployment YAML方式創建Nginx服務 這個yaml文件在網上可以下載 cat nginx-deployment.yaml apiVersion: apps/v1 #apiVersion是當前配置格式的版本 kind: Deployment #kind是要創建的資源類型&#xff0c;這里是Deploymnet metadata: #metadata是該資源…

Photoshop多圖片與多窗口下排列操作方法

首先&#xff0c;在Photoshop中打開6張圖片&#xff0c;在“窗口”菜單下切換窗口排列狀態&#xff1a; 在 “窗口”菜單下對窗口進行排列&#xff0c;分別呈現如下&#xff1a; &#xff08;一&#xff09;. 點擊“窗口” -> “排列”->"全部垂直拼貼": &am…

本地oracle登錄賬號鎖定處理,the account is locked

1.打開cmd命令窗口 2.打開sqlplus: sqlplus /nolog(加/nolog是不登錄服務器的意思&#xff0c;不加就需要輸賬號密碼) 3.切換到管理員&#xff1a;conn / as sysdba; 第2步第3步可以合并&#xff0c;直接使用sysdba登錄&#xff1a;sqlplus / as sysdba; 4.解鎖賬號&#x…

大端和小端

大端和小端 大端&#xff08;Big Endian&#xff09;和小端&#xff08;Little Endian&#xff09;是兩種不同的字節序排列方式&#xff0c;用于解釋多字節數據在內存中的存儲順序。 在大端字節序中&#xff0c;高位字節&#xff08;最高有效位&#xff09;存儲在低位地址&am…

1. 兩數之和

題目&#xff1a; 給定一個整數數組 nums 和一個整數目標值 target&#xff0c;請你在該數組中找出 和為目標值 target 的那 兩個 整數&#xff0c;并返回它們的數組下標。 你可以假設每種輸入只會對應一個答案。但是&#xff0c;數組中同一個元素在答案里不能重復出現。 你…

如何從cpu改為gpu,pytorch,cuda

1.cmd輸入nvcc -V 2.得到 cuda版本后&#xff0c;去pytorch官網 3.根據自己的cuda進行選擇 4.復制上述鏈接&#xff0c;進入cmd 5.cmd中輸入activate XXX,這里的"XXX"指代自己在工程中用到的環境 6.進入后&#xff0c;將剛才鏈接粘貼&#xff0c;回車等待下載結束 …

qt實現截取屏幕

利用qt提供的函數實現截屏: QPixmap QPixmap::grabWindow(WID window, int x 0, int y 0, int width -1, int height -1) window: 表示窗口ID號 x、y: 截取屏幕的其實坐標 width:截取屏幕的寬度 -1表示當前窗口寬度 height:截取屏幕的高度 -1表示當前窗口高度 示例…

《高性能MySQL》——查詢性能優化(筆記)

文章目錄 六、查詢性能優化6.1 查詢為什么會慢6.2 慢查詢基礎&#xff1a;優化數據訪問6.2.1 是否向數據庫請求了不需要的數據查詢不需要的記錄多表關聯時返回全部列總是取出全部列重復查詢相同的數據 6.2.2 MySQL 是否在掃描額外的記錄響應時間掃描的行數與返回的行數掃描的行…

新增守護進程管理、支持添加MySQL遠程數據庫,支持PHP版本切換,1Panel開源面板v1.5.0發布

2023年8月14日&#xff0c;現代化、開源的Linux服務器運維管理面板1Panel正式發布v1.5.0版本。 在這個版本中&#xff0c;1Panel新增了守護進程管理功能&#xff1b;支持添加MySQL遠程數據庫&#xff1b;支持添加FTP/S和WebDAV的SFTP服務&#xff1b;支持PHP版本切換。此外&am…

jupyter打開ipynb后,還沒有運行cell,反復報錯

今天遇到了一個比較奇怪的問題&#xff1a; 這個原因是當前目錄下有一個code.py的文件&#xff0c;一旦打開ipynb&#xff0c;就是先執行code.py&#xff0c;而且遇到報錯&#xff0c;還會反復執行&#xff0c;導致內核崩潰。

創建一個 React+Typescript 項目

接下來 我們來一起探索一下用TypeScript 來編寫react 這也是一個非常好的趨勢&#xff0c;目前也非常多人使用 那么 我們就先從創建項目開始 首先 我們先找一個 或者 之前創建一個目錄 用來放我們的項目 然后 在這個目錄下直接輸入 例如 這里 我想創建一個叫 tsReApp 的項目…

Leangoo領歌敏捷工具全面開啟免費模式

轉發自&#xff1a;Leangoo.com 為了更好的服務敏捷社區&#xff0c;更好的幫助敏捷企業加速產業升級&#xff0c;更好的幫助企業打造敏捷團隊&#xff0c;Leangoo領歌軟件團隊版和企業版全面開啟永久免費模式&#xff0c;同時&#xff0c;Leangoo領歌仍然繼續提供專業的私有部…

面向對象設計與分析40講(20)消息驅動編程和事件驅動編程模型

文章目錄 消息驅動編程事件驅動編程消息驅動和事件驅動的區別 消息驅動編程 消息驅動是一種編程模型&#xff0c;它基于事件和消息的傳遞來驅動程序的執行流程。在消息驅動的模型中&#xff0c;系統中的各個組件&#xff08;或對象&#xff09;通過發送和接收消息進行通信和協…

matplotlib繪制位置-時序甘特圖

文章目錄 1 前言2 知識點2.1 matplotlib.pyplot.barh2.2 matplotlib.legend的handles參數 3 代碼實現4 繪制效果5 總結參考 1 前言 這篇文章的目的是&#xff0c;總結記錄一次使用matplotlib繪制時序甘特圖的經歷。之所以要繪制這個時序甘特圖&#xff0c;是因為22年數模研賽C…

【IDEA問題】下載不了源代碼

引出問題 最近不知道怎么打開 IDEA&#xff0c;本想查看源代碼&#xff0c;然后點擊下載源碼&#xff0c;總是報找不到此對象的源代碼。百度找了半天&#xff0c;GPT問了半天還是解決不了&#xff0c;直到遇到了這篇&#xff1a;idea中無法下載源碼問題解決&#xff0c;終于得…

取證--實操

2022年美亞杯個人賽 運用軟件DB Browser for SQLite &#xff08;一款用于查看SQLlite數據庫文件的瀏覽器工具&#xff09; 火眼&#xff0c;盤古石手機取證系統等 案件詳情 于2022年10月&#xff0c;有市民因接獲偽冒快遞公司的電郵&#xff0c;不慎地于匪徒架設的假網站提…

網絡安全 Day31-運維安全項目-容器架構下

容器架構下 6. Dockerfile6.1 Docker自動化DIY鏡像之Dockerfile1) 環境準備2) 書寫Dockerfile內容3&#xff09; 運行Dockerfile生成鏡像4) 運行容器5) 小結 6.2 案例14&#xff1a;Dockerfile-RUN指令1) 書寫Dockerfile2) 構建鏡像3) 啟動容器4) 測試結果 6.3 Dockerfile指令 …

騰訊面試題算法還原【游戲安全】

本題的參考鏈接&#xff1a;https://share.weiyun.com/5Xg2b7v 其實拿到這個題我就感覺在哪里看過&#xff0c;后來想想是在旺仔那里看到的&#xff0c;以下是旺仔寫的分析過程可以參考一下https://bbs.kanxue.com/thread-276536.htm 但是這個題要比旺仔拿到的那個要增加些許…