Android 加殼應用運行流程 與 生命周期類處理方案

版權歸作者所有,如有轉發,請注明文章出處:https://cyrus-studio.github.io/blog/

DexClassLoader

DexClassLoader 可以加載任意路徑下的 dex,或者 jar、apk、zip 文件(包含classes.dex)。常用于插件化、熱修復以及 dex 加殼。

源碼如下:

public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}
}

http://aospxref.com/android-10.0.0_r47/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

參數說明

參數類型說明
dexPathString需要加載的 dex、apk、jar、zip 文件的路徑,多個路徑用 : 分隔。支持任意目錄下的文件。
optimizedDirectoryString用于存放優化后的 dex 文件(即 .odex),在 API level 26 已棄用(Android 8.0 Oreo)。
librarySearchPathString指定本地庫(native library,.so 文件)搜索路徑,多個路徑用 : 分隔。
parentClassLoader父類加載器,用于實現類加載的委托機制。

動態加載

動態加載 = 運行時按需加載代碼或資源

動態加載 是實現 dex加殼、插件化、熱更新、熱修復 的基礎。比如阿里的 AndFix 、騰訊 tinker、美團 Robust 等熱修復框架的基礎。

使用 DexClassLoader 加載一個外部 .dex 或 .apk 文件,然后反射調用里面的類和方法。

步驟如下:

  1. 準備好外部的 dex / apk / jar;

  2. 將它放在你 app 可以訪問的路徑(如 /data/data/包名/files/);

  3. 用 DexClassLoader 加載它;

  4. 使用反射調用其中的類和方法。

1. 插件工程示例

插件工程主要包含這兩個類:

word/media/image1.png

PluginActivity 源碼:使用 Jetpack Compose 創建一個白色背景、居中顯示文本的界面,文本內容為 “PluginActivity from plugin” 加上 ClassLoader 信息

package com.cyrus.example.pluginimport android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.spclass PluginActivity : ComponentActivity() {private val TAG = "PluginActivity"override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)Log.d(TAG, "onCreate")val classLoaderInfo = this.javaClass.classLoader.toString()setContent {PluginActivityContent(classLoaderInfo)}}@Composablefun PluginActivityContent(classLoaderInfo: String) {Box(modifier = Modifier.fillMaxSize().background(Color.White).padding(horizontal = 16.dp),contentAlignment = Alignment.Center) {Text(text = "PluginActivity from plugin\n\n$classLoaderInfo",fontSize = 18.sp,color = Color.Black)}}override fun onStart() {super.onStart()Log.d(TAG, "onStart")}override fun onResume() {super.onResume()Log.d(TAG, "onResume")}override fun onPause() {super.onPause()Log.d(TAG, "onPause")}override fun onStop() {super.onStop()Log.d(TAG, "onStop")}override fun onRestart() {super.onRestart()Log.d(TAG, "onRestart")}override fun onDestroy() {super.onDestroy()Log.d(TAG, "onDestroy")}}

PluginClass 源碼:

package com.cyrus.example.pluginclass PluginClass {fun getString(): String {return "String from plugin."}}

編譯 apk

word/media/image2.png

把 apk 推送到設備 sdcard

adb push plugin-debug.apk /sdcard/Android/data/com.cyrus.example/files

2. 動態加載示例

創建 DexClassLoader 實例,加載指定路徑下的 APK/DEX 文件

val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"// 創建 DexClassLoader 加載 sdcard 上的 apk
val classLoader = DexClassLoader(apkPath,null,this@ClassLoaderActivity.packageResourcePath,context.classLoader // parent 設為當前 context 的類加載器
)

調用示例:

// classLoader 加載 com.cyrus.example.plugin.PluginClass 類并通過反射調用 getString 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.PluginClass")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("getString")
method.isAccessible = true
val result = method.invoke(instance) as? Stringoutput = "動態加載:${pluginPath}\n\ncall ${method}\n\nreuslt=${result}"

效果如下,可以看到正常調用了 apk 中 PluginClass 的 getString 方法并拿到了返回值

word/media/image3.png

組件類 ClassNotFoundException

在 AndroidManifest.xml 中聲明 PluginActivity

<activityandroid:name="com.cyrus.example.plugin.PluginActivity"android:exported="true">
</activity>

通過自定義 ClassLoader 加載 PluginActivity 類并啟動

// 通過 classLoader 加載 PluginActivity 類并啟動
val pluginActivityClass = classLoader.loadClass("com.cyrus.example.plugin.PluginActivity")
val intent = Intent(this@ClassLoaderActivity, pluginActivityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this@ClassLoaderActivity.startActivity(intent)

報錯如下:

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.cyrus.example/com.cyrus.example.plugin.PluginActivity}: java.lang.ClassNotFoundException: Didn't find class "com.cyrus.example.plugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/lib/arm64, /data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3194)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.cyrus.example.plugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/lib/arm64, /data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:44)
at android.app.Instrumentation.newActivity(Instrumentation.java:1250)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3182)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)?
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)?
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)?
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)?
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)?
at android.os.Handler.dispatchMessage(Handler.java:107)?
at android.os.Looper.loop(Looper.java:214)?
at android.app.ActivityThread.main(ActivityThread.java:7356)?
at java.lang.reflect.Method.invoke(Native Method)?
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)?
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)?

斷點調試看看,PluginActivity 類是正常加載了的

word/media/image4.png

原因是:

  • 動態加載的 dex 不具有生命周期特征,APP 中的 Activity 、Service 等組件無法正常工作,只能完成一般函數的調用;

  • 需要對 ClassLoader 進行修正,APP 才能夠正常運行。

生命周期類處理

DexClassLoader 加載的類是沒有組件生命周期的,也就是說即使 DexClassLoader 通過對 APK 動態加載完成了對組件類的加載,當系統啟動該組件時,依然會出現加載類失敗的異常。

為什么組件類被動態加載入虛擬機,但系統卻出現加載類失敗呢?

因為 系統在啟動組件(如 Activity、Service)時,是通過 AMS → ActivityThread → Instrumentation 最終調用 Class.forName(組件類名) 來加載組件類的,而這個過程默認使用的是系統的 ClassLoader(通常是 PathClassLoader),而不是我們自定義的 DexClassLoader。

從 ClassLoader 來看,兩種解決方案:

  1. 替換系統組件類加載器為我們的 DexClassLoader,同時設置 DexClassLoader 的 parent 為系統組件類加載器

  2. 打破原有的雙親關系,在系統組件類加載器和 BootClassLoader 的中間插入我們自己的 DexClassLoader 即可

或者可以對 PathClassLoader 中的 Elements 進行合并(常用于熱修復框架(如 Tinker、Robust))。

相關文章:Android 下的 ClassLoader 與 雙親委派機制

方案 1:替換 ClassLoader 為 自定義ClassLoader

變化前結構(系統默認)

[BootClassLoader]↑
[PathClassLoader]    ← 原來的 ClassLoader↑Activity、Application 加載組件類

變化后結構(方案1)

[BootClassLoader]↑
[PathClassLoader]    ← 原來的 ClassLoader↑
[DexClassLoader]     ← 反射替換 LoadedApk.mClassLoader↑Activity、Application 加載組件類

通過 反射替換掉 系統的 ClassLoader 為自定義的 ClassLoader,同時設置 parent 為 系統的 ClassLoader

示例代碼:

private fun replaceClassLoader(context: Context): ClassLoader? {try {// 1. 創建自定義 ClassLoader 實例,加載 sdcard 上的 apkval classLoader = DexClassLoader(apkPath,null,this@ClassLoaderActivity.packageResourcePath,context.classLoader // 設置 parent 為 系統的 ClassLoader)// 2. 拿到 ActivityThreadval activityThreadClass = Class.forName("android.app.ActivityThread")val currentActivityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null)// 3. 拿到 mPackages 字段: Map<String, WeakReference<LoadedApk>>val mPackagesField = activityThreadClass.getDeclaredField("mPackages")mPackagesField.isAccessible = trueval mPackages = mPackagesField.get(currentActivityThread) as Map<*, *>// 4. 拿到當前包名對應的 LoadedApk 實例val loadedApkRef = mPackages[context.packageName] as? WeakReference<*>?: throw IllegalStateException("LoadedApk not found for package: ${context.packageName}")val loadedApk = loadedApkRef.get()?: throw IllegalStateException("LoadedApk is null")// 5. 替換 LoadedApk.mClassLoaderval loadedApkClass = loadedApk.javaClassval mClassLoaderField = loadedApkClass.getDeclaredField("mClassLoader")mClassLoaderField.isAccessible = truemClassLoaderField.set(loadedApk, classLoader)// ? 替換成功Log.d(TAG, "? ClassLoader has been replaced successfully!")return classLoader} catch (e: Exception) {e.printStackTrace()Log.d(TAG, "? Failed to replace ClassLoader: ${e.message}")}return null
}
  • Android 每個 App 都有一個 LoadedApk 對象負責管理 dex 加載;

  • mClassLoader 是決定類加載的實際執行者;

  • 修改它就等于修改了整個 App 的“類查找邏輯”。

調用示例:

// 方案 1:替換 ClassLoader 為 自定義ClassLoader
val classLoader = replaceClassLoader(context)if (classLoader == null) {Log.d(TAG, "? Failed to replace ClassLoader")return@Button
}// classLoader 加載 com.cyrus.example.plugin.PluginClass 類并通過反射調用 getString 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.PluginClass")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("getString")
method.isAccessible = true
val result = method.invoke(instance) as? String// 通過 classLoader 加載 PluginActivity 類并啟動
val pluginActivityClass = classLoader.loadClass("com.cyrus.example.plugin.PluginActivity")
val intent = Intent(this@ClassLoaderActivity, pluginActivityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this@ClassLoaderActivity.startActivity(intent)output = "動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}"

成功啟動 PluginActivity,而且可以看到 ClassLoader 是自定義的 DexClassLoader

word/media/image5.png

日志輸出如下,生命周期相關方法也是正常執行

word/media/image6.png

方案 2:插入中間 ClassLoader / 打破雙親委派

變化前結構

[BootClassLoader]↑
[PathClassLoader]   ← 原始 ClassLoader

變化后結構(方案2)

[BootClassLoader]↑
[DexClassLoader]     ← 反射插入自定義的 DexClassLoader↑
[PathClassLoader]    ← 原始 ClassLoader

在 BootClassLoader 和 PathClassLoader 中間 通過反射插入 自定義的 DexClassLoader

通過反射修改 PathClassLoader 的 parent 為 自定義ClassLoader,并設置 自定義ClassLoader 的 parent 為 BootClassLoader

目標結構如下:

  • PathClassLoader.parent = 自定義ClassLoader

  • 自定義ClassLoader.parent = BootClassLoader

通過反射獲取并修改 PathClassLoader 的 parent 字段

private fun injectClassLoader(context: Context): DexClassLoader? {try {// 拿到當前 PathClassLoaderval appClassLoader = context.classLoaderval pathClassLoaderClass = ClassLoader::class.java// 反射訪問 parent 字段val parentField = pathClassLoaderClass.getDeclaredField("parent")parentField.isAccessible = trueval bootClassLoader = ClassLoader.getSystemClassLoader().parent// 自定義ClassLoader.parent = BootClassLoaderval classLoader = DexClassLoader(apkPath,null,this@ClassLoaderActivity.packageResourcePath,bootClassLoader // 設置 parent 為 BootClassLoader)// PathClassLoader.parent = 自定義ClassLoaderparentField.set(appClassLoader, classLoader)Log.d(TAG, "? 成功將 ${classLoader} 注入到 PathClassLoader.parent")return classLoader} catch (e: Exception) {e.printStackTrace()Log.d(TAG, "? 注入失敗:${e.message}")}return null
}

調用示例:

// 方案 2:插入中間 ClassLoader / 打破雙親委派
val classLoader = injectClassLoader(context)if (classLoader == null) {Log.d(TAG, "? Failed to replace ClassLoader")return@Button
}// classLoader 加載 com.cyrus.example.plugin.PluginClass 類并通過反射調用 getString 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.PluginClass")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("getString")
method.isAccessible = true
val result = method.invoke(instance) as? String// 通過 classLoader 加載 PluginActivity 類并啟動
val pluginActivityClass = classLoader.loadClass("com.cyrus.example.plugin.PluginActivity")
val intent = Intent(this@ClassLoaderActivity, pluginActivityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this@ClassLoaderActivity.startActivity(intent)output = "動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}"

成功啟動 PluginActivity,而且可以看到 ClassLoader 是自定義的 DexClassLoader

word/media/image7.png

日志輸出如下,生命周期相關方法也是正常執行

word/media/image8.png

這樣你就“無縫攔截”了類加載流程,同時也保留系統 PathClassLoader 的全部能力,比完全替換更可靠 💯

加殼應用的運行流程

參考文章:

  • 詳解 Android APP 啟動流程

  • FART:ART環境下基于主動調用的自動化脫殼方案

殼執行的時機必須比 app 原來的邏輯早。

在 APP啟動流程中我們最終可以得出結論,app 最先獲得執行權限的是 app 中聲明的 Application 類中的 attachBaseContext 和 onCreate 函數。因此,殼要想完成應用中加固代碼的解密以及應用執行權的交付就都是在這兩個函數上做文章。

加殼應用運行流程:

→ AMS 發起啟動
→ Zygote fork → ActivityThread.main
→ ActivityThread.handleBindApplication()
→ 殼Application.attachBaseContext(base)? 解密 dex? 自定義 ClassLoader 加載解密 dex? 反射設置 LoadedApk.mClassLoader 為自定義 ClassLoader
→ 殼Application.onCreate()? 反射生成真實的 Application 對象? 反射替換 ActivityThread.mInitialApplication 為真實 Application 對象? 已進入原始 app 世界

完整源碼

開源地址:https://github.com/CYRUS-STUDIO/AndroidExample

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

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

相關文章

c++進階——類與繼承

文章目錄 繼承繼承的基本概念繼承的基本定義繼承方式繼承的一些注意事項 繼承類模板 基類和派生類之間的轉換繼承中的作用域派生類的默認成員函數默認構造函數拷貝構造賦值重載析構函數默認成員函數總結 不能被繼承的類繼承和友元繼承與靜態成員多繼承及其菱形繼承問題繼承模型…

GAEA情感坐標背后的技術原理

基于GAEA的去中心化物理基礎設施網絡&#xff08;DePIN&#xff09;&#xff0c;用戶有機會在GAEA平臺上獲得寶貴的數據共享積分。為了提升這些洞察的豐富性&#xff0c;用戶必須花費一定數量的積分&#xff0c;將過去的網絡數據與當前的情感數據綁定&#xff0c;從而產生一種新…

圖形編輯器基于Paper.js教程27:對圖像描摹的功能實現,以及參數調整

本篇文章來講一下 圖像描摹的功能的實現。 我們知道要雕刻圖片可以通過分析圖片的像素來生成相應的gcode進行雕刻&#xff0c;但如果你想要將圖片轉換為線稿進行雕刻&#xff0c;這個時候就要從圖片中提取出 線稿。 例如下面的圖片&#xff1a; 你想要獲取到這個圖片的線稿&…

人工智能與機器學習,誰是誰的子集 —— 再談智能的邊界與演進路徑

人工智能&#xff08;Artificial Intelligence, AI&#xff09;作為當代最具影響力的前沿技術之一&#xff0c;常被大眾簡化為 “深度學習” 或 “大模型” 等標簽。然而&#xff0c;這種簡化認知往往掩蓋了AI技術內部結構的復雜性與多樣性。事實上&#xff0c;AI并非單一方法的…

Oracle_開啟歸檔日志和重做日志

在Oracle中&#xff0c;類似于MySQL的binlog的機制是歸檔日志&#xff08;Archive Log&#xff09;和重做日志&#xff08;Redo Log&#xff09; 查詢歸檔日志狀態 SELECT log_mode FROM v$database; – 輸出示例&#xff1a; – LOG_MODE – ARCHIVELOG (表示已開啟) – NO…

IDEA編寫flinkSQL(快速體驗版本,--無需配置環境)

相關資料 文檔內容鏈接地址datagen生成器https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/connectors/table/datagen/print 生成器https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/connectors/table/print/ 準備工作 優點就是下載個ide…

基于AI技術的高速公路交通引流系統設計與應用研究

基于AI技術的高速公路交通引流系統設計與應用研究 1. 研究背景與意義 1.1 交通系統演化脈絡 1.1.1 發展階段劃分 機械化時代&#xff08;1950-1990&#xff09;&#xff1a;固定式信號控制信息化時代&#xff08;1991-2010&#xff09;&#xff1a;SCATS/SCOOT系統智能化時代…

NEGATIVE LABEL GUIDED OOD DETECTION WITH PRETRAINED VISION-LANGUAGE MODELS

1. 介紹: 這篇論文也是基于CLIP通過后處理的方法實現的OOD的檢測,但是設計點在于,之前的方法是使用的ID的類別,這篇工作是通過添加一些在語義上非常不同于ID的類別的外分布類來做的OOD檢測。 CLIP做OOD檢測的這個系列里面我看的以及記錄的第一篇就是MCM的方法,這也是確實是…

Linux 網絡基礎三 (數據鏈路層協議:以太網協議、ARP 協議)

一、以太網 兩個不同局域網的主機傳遞數據并不是直接傳遞的&#xff0c;而是通過路由器 “一跳一跳” 的傳遞過去。 跨網絡傳輸的本質&#xff1a;由無數個局域網&#xff08;子網&#xff09;轉發的結果。 所以&#xff0c;要理解數據跨網絡轉發原理就要先理解一個局域網中數…

Azure Data Factory ETL設計與調度最佳實踐

一、引言 在Azure Data Factory (ADF) 中&#xff0c;調度和設計ETL&#xff08;抽取、轉換、加載&#xff09;過程需要綜合考量多方面因素&#xff0c;以確保數據處理高效、可擴展、可靠且易于維護。以下將詳細介紹相關關鍵考慮因素、最佳實踐&#xff0c;并輔以具體示例說明…

非序列實現MEMS聚焦功能

zemax非序列模式下有MEMS,但是沒有對應的代碼。無法修改成自己需要的功能 以下是實現MEMS聚焦功能: #include <windows.h> #include <cmath> #include <stdio.h> #include <string.h> #include <algorithm> #undef max #undef min#define D…

android studio sdk unavailable和Android 安裝時報錯:SDK emulator directory is missing

md 網上說的都是更換proxy代理什么的&#xff0c;換網的&#xff0c;還有一些二其他亂七八糟的&#xff0c;根本沒用&#xff0c;感覺很多就是解決不了問題&#xff0c;還賊多賊一致&#xff0c;同質化&#xff0c;感覺很坑人&#xff0c;讓人覺得他們和我的一樣的&#xff0c;…

三維重建模塊VR,3DCursor,MPR與VR的坐標轉換

MPR里的reslicecursor 的坐標與 vtkimage 坐標一致。 但三維窗格里的vtkvolume 的坐標是相對坐標&#xff0c;坐標值依然是MM單位。 用中心點的偏移量比較容易實現&#xff0c;交互中Reslicercursor中心點 距離 vtkimagedata 的中心點 的偏移量&#xff0c;用于vtkvolume即可…

Python Cookbook-6.9 快速復制對象

任務 為了使用 copy.copy&#xff0c;需要實現特殊方法__copy__。而且你的類的__init__比較耗時所以你希望能夠繞過它并獲得一個“空的”未初始化的類實例。 解決方案 下面的解決方案可同時適用于新風格和經典類: def empty_copy(obj):class Empty(obj.__class__):def __in…

kubernets集群的安裝-node節點安裝-(簡單可用)-超詳細

一、kubernetes 1、簡介 kubernetes&#xff0c;簡稱K8s&#xff08;庫伯內特&#xff09;&#xff0c;是用8代替名字中間的8個字符“ubernete”而成的縮寫 云計算的三種主要服務模式——基礎設施即服務&#xff08;IaaS&#xff09;、平臺即服務&#xff08;PaaS&#xff0…

【Linux學習筆記】進程的fork創建 exit終止 wait等待

【Linux學習筆記】進程的fork創建 exit終止 wait等待 &#x1f525;個人主頁&#xff1a;大白的編程日記 &#x1f525;專欄&#xff1a;Linux學習筆記 文章目錄 【Linux學習筆記】進程的fork創建 exit終止 wait等待前言1.進程創建1.1 fork函數初識1.2fork函數返回值1.3寫時拷…

鴻蒙應用開發證書考試的一點想法

一、介紹&#xff1a; 直接上圖 二、體驗后的想法&#xff1a; 1.知識點在指南API參考最佳實踐里面找 2.沒有明確說明考試不能查第1點的文檔&#xff0c;但是考試只有1個小時&#xff0c;合理分配時間 3.切屏三次后自動提交要注意&#xff0c;每月3次機會下月又有3次機會&a…

含錫廢水處理的經濟效益

主要體現在成本節約和資源回收兩方面&#xff0c;具體收益因處理工藝、廢水濃度及規模差異而不同。以下結合不同技術路線進行量化分析&#xff1a; 一、直接經濟效益 資源回收收益 金屬錫回收&#xff1a; 若廢水中錫濃度為100 mg/L&#xff0c;日處理量100噸&#xff0c;則每…

Base64編碼原理:二進制數據與文本的轉換技術

&#x1f504; Base64編碼原理&#xff1a;二進制數據與文本的轉換技術 開發者的數據編碼困境 作為開發者&#xff0c;你是否曾遇到這些與Base64相關的挑戰&#xff1a; &#x1f4ca; 需要在JSON中傳輸二進制數據&#xff0c;但不確定如何正確編碼&#x1f5bc;? 想要在HT…

day49—雙指針+貪心—驗證回文串(LeetCode-680)

題目描述 給你一個字符串 s&#xff0c;最多 可以從中刪除一個字符。 請你判斷 s 是否能成為回文字符串&#xff1a;如果能&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 示例 1&#xff1a; 輸入&#xff1a;s "aba" 輸出&#xff1a;true…