自拍偷在线精品自拍偷|国产无码一区二区久久|最新版天堂资源中文官网|国产精品第一页爽爽影院|国产精品一区二区av不卡|久久久波多野av一区无码|国产欧美日本亚洲精品一4区|亚洲精品天堂在线观看2020

當前位置:首頁 > 網(wǎng)站建設 > 正文內(nèi)容

apk怎么拆包修改(如何拆apk安裝包)

網(wǎng)站建設1年前 (2023-10-20)446

安卓進階漲薪訓練營,讓一部分人先進大廠

大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術職場瓶頸,從而度過難關,進入心儀的公司。

詳情見文章: 沒錯!皇叔開了個訓練營

前言

在 Android 中,有個非常強大的功能,那就是輔助功能。

輔助功能本是用于服務殘障人士的。比如對于視障人士來說,輔助功能可以幫助他們讀出屏幕上的文字或圖片(閱讀圖片時會播放其 ContentDeion 屬性)。

除此之外,輔助功能還可以模擬點擊,模擬手勢等等,對于我這樣的懶癌人士,輔助功能可以幫助我做一些重復、機械的點擊操作。

模擬點擊功能非常強大,它不局限于本應用內(nèi),它就像模擬出了一只手,可以在任何時刻幫助我們點擊屏幕的任何位置。

比如我們可以開啟一個循環(huán),不斷地點擊某個位置,這在某些場景中可以解放我們的手指細胞。還可以實現(xiàn)類似這樣的點擊序列:等待 3s 點擊位置 A,然后等待 2s 點擊兩次位置 B,等待 500ms 再點擊 5 次位置 C 等等。以此完成一些日常的簽到打卡等功能。

缺點是它不知道當前頁面顯示的內(nèi)容是什么,這一點可以通過截圖 + 圖片識別來解決。

所以想要實現(xiàn)一個簡單的外掛,可以分三步走:

模擬點擊

應用外截屏

圖片識別

模擬點擊

應用外截屏

圖片識別

接下來我們就來一步步地攻克這三個技術點。

模擬點擊

新建 MyAccessibilityService 類

首先,新建一個 MyAccessibilityService 類,繼承自系統(tǒng)的 AccessibilityService 類:

classMyAccessibilityService: AccessibilityService{

展開全文

override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent?){

}

override fun onInterrupt{

}

}

繼承 AccessibilityService 后,需要實現(xiàn)兩個方法 onAccessibilityEvent 和 onInterrupt。

onAccessibilityEvent 方法中,帶有一個參數(shù) AccessibilityEvent,當界面發(fā)生改變時,這個方法就會被調用,界面改變的具體信息就會包含在這個參數(shù)中。onInterrupt 方法輔助服務被中斷了。

我們暫時先在這兩個方法中簡單地打印一行日志,待會再在其中添加具體的功能。

注冊 Service

寫好 MyAccessibilityService 類后,需要在 AndroidManifest 中注冊。注冊輔助服務和注冊一般的服務略有區(qū)別:

service

android:name= ".MyAccessibilityService"

android:deion= "@string/deion_in_manifest"

android:exported= "true"

android:label= "@string/label_in_manifest"

android:permission= "android.permission.BIND_ACCESSIBILITY_SERVICE"

intent-filter

action android:name= "android.accessibilityservice.AccessibilityService"/

/intent-filter

meta-data

android:name= "android.accessibilityservice"

android:resource= "@xml/accessibility_config"/

首先是需要聲明一個 label,這個 label 是在系統(tǒng)的輔助功能設置中顯示的名字

deion 屬性可以不寫,指的是在輔助功能設置中顯示的該輔助功能的描述

permission 屬性必須寫,表示這個服務需要綁定 AccssibilityService

在這個 service 中,有一個 inter-filter,這個也是必須寫的,不妨記作固定格式

還有一個 meta-data,其中的 resource 屬性指向一個 xml 文件,這個文件中可以配置允許這個輔助功能做哪些事

首先是需要聲明一個 label,這個 label 是在系統(tǒng)的輔助功能設置中顯示的名字

deion 屬性可以不寫,指的是在輔助功能設置中顯示的該輔助功能的描述

permission 屬性必須寫,表示這個服務需要綁定 AccssibilityService

在這個 service 中,有一個 inter-filter,這個也是必須寫的,不妨記作固定格式

還有一個 meta-data,其中的 resource 屬性指向一個 xml 文件,這個文件中可以配置允許這個輔助功能做哪些事

xml 文件如下:

?xml version= "1.0"encoding= "utf-8"?

accessibility-service xmlns:android= "http://schemas.android.com/apk/res/android"

android:accessibilityEventTypes= "typeAllMask"

android:accessibilityFeedbackType= "feedbackGeneric"

android:canPerformGestures= "true"

android:canRetrieveWindowContent= "true"

android:deion= "@string/deion_in_xml"

android:notificationTimeout= "100"/

AndroidManifest 和 xml 中,用到的字符串資源文件如下:

string name= "label_in_manifest"Label in manifest/string

string name= "deion_in_manifest"Deion in manifest/string

string name= "deion_in_xml"Deion in xml/string

這些都設置好之后,這個 Service 就注冊成功了。

現(xiàn)在就可以運行一下看看效果了。

開啟輔助服務

此時運行程序,會發(fā)現(xiàn)沒有任何 onAccessibilityEvent 事件打出。這是因為輔助功能是一項比較危險的功能,默認是關閉的。需要到系統(tǒng)設置中手動打開才可以使用。

通過圖中的三個步驟,確保 Use Label in manifest 的開關是打開的,我們的輔助功能就被正式啟用了。從圖中我們也可以看出注冊 service 時寫的字符串各自的使用場景。在程序中,也可以通過代碼到達輔助功能設置頁面,代碼如下:

object AccessibilitySettingUtils {

fun jumpToAccessibilitySetting(context: Context){

val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)

context.startActivity(intent)

}

}

開啟輔助功能后,點擊桌面就會在 Log 控制臺收到以下消息:

D/~~~: accessibilityEvent: EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 101990739; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: [CONTENT_CHANGE_TYPE_SUBTREE]; WindowChangeTypes: [] [ ClassName: android.widget.FrameLayout; Text: []; ContentDeion: null; ItemCount: - 1; CurrentItemIndex: - 1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: - 1; ToIndex: - 1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: - 1; ScrollDeltaY: - 1; AddedCount: - 1; RemovedCount: - 1; ParcelableData: null]; recordCount: 0

這表示我們接收到了一個 accessibilityEvent 消息,他的類型是 TYPE_WINDOW_CONTENT_CHANGED,意思是窗口內(nèi)容發(fā)生了變化,PackageName 中表示這個變化的內(nèi)容所在的包名。

說明我們的輔助功能已經(jīng)開始工作了。

點擊對應坐標

想要查看屏幕上的坐標,可以在開發(fā)人員選項中打開顯示坐標的設置:

打開這個設置后,每次點擊屏幕,都會在頂部顯示當前點擊的位置坐標。點擊對應坐標的代碼如下:

object ClickUtils {

fun click(accessibilityService: AccessibilityService, x: Float, y: Float){

Log.d( "~~~", "click: ($x, $y)")

val builder = GestureDeion.Builder

val path = Path

path.moveTo(x, y)

path.lineTo(x, y)

builder.addStroke(GestureDeion.StrokeDeion(path, 0, 1))

val gesture = builder.build

accessibilityService.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback {

override fun onCancelled(gestureDeion: GestureDeion){

super.onCancelled(gestureDeion)

}

override fun onCompleted(gestureDeion: GestureDeion){

super.onCompleted(gestureDeion)

}

}, null)

}

}

在這個工具類中,我們將 AccessibilityService 和坐標傳入。

通過 GestureDeion 的 Builder 構建一個手勢,通過 Builder 的 addStoke 方法傳入一條 path,這條 path 我們設置為從 (x, y) 坐標移動到 (x, y) 坐標。StrokeDeion 的后兩個參數(shù)表示 startTime 和 duration,分別表示手勢的開始時間以及持續(xù)時間,以毫秒為單位。我將其設置為 0 和 1,也就是 1ms 以內(nèi)完成從 (x, y) 坐標移動到 (x, y) 坐標。

這樣就模擬出了一個點擊事件。通過 accessibilityService 的 dispatchGesture 方法觸發(fā)這個手勢,這個方法接收兩個參數(shù),第一個參數(shù)是手勢的具體配置,第二個參數(shù)表示手勢執(zhí)行的結果,包含執(zhí)行完成和取消兩種結果。

測試

我們不妨寫個簡單的頁面來測試一下。先寫一個頁面,包含兩個按鈕:

?xml version= "1.0"encoding= "utf-8"?

androidx.constraintlayout.widget.ConstraintLayout xmlns:android= "http://schemas.android.com/apk/res/android"

xmlns:app= "http://schemas.android.com/apk/res-auto"

xmlns:tools= "http://schemas.android.com/tools"

android:layout_width= "match_parent"

android:layout_height= "match_parent"

tools:context= ".MainActivity"

Button

android:id= "@+id/btn_jump_to_settings"

android:layout_width= "match_parent"

android:layout_height= "wrap_content"

android:text= "Jump to Settings"

android:textAllCaps= "false"

app:layout_constraintTop_toTopOf= "parent"/

Button

android:id= "@+id/btn_test"

android:layout_width= "match_parent"

android:layout_height= "wrap_content"

android:text= "Test"

app:layout_constraintTop_toBottomOf= "@id/btn_jump_to_settings"/

/androidx.constraintlayout.widget.ConstraintLayout

這個頁面的效果圖:

在 app/build.gradle 中,開啟 ViewBinding,目的是使用這些按鈕更方便:

buildFeatures {

viewBinding true

}

在 MainActivity 中,設置按鈕的點擊事件:

classMainActivity: AppCompatActivity{

override fun onCreate(savedInstanceState: Bundle?){

super.onCreate(savedInstanceState)

val binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)

binding.btnJumpToSettings.setOnClickListener {

AccessibilitySettingUtils.jumpToAccessibilitySetting( this)

}

binding.btnTest.setOnClickListener {

Toast.makeText( this, "I'm clicked", Toast.LENGTH_SHORT).show

}

}

}

第一個按鈕 btnJumpToSettings 的作用是點擊跳轉到輔助服務設置頁

第二個按鈕用來做測試,點擊時會彈出 Toast:"I'm clicked"。待會我們就模擬點擊這個按鈕。

第一個按鈕 btnJumpToSettings 的作用是點擊跳轉到輔助服務設置頁

第二個按鈕用來做測試,點擊時會彈出 Toast:"I'm clicked"。待會我們就模擬點擊這個按鈕。

查看一下第二個按鈕的坐標位置:

從圖中可以看出,第三個按鈕的坐標大約是 (622,406)。在 MyAccessibilityService 的 onServiceConnected 方法中,模擬點擊此坐標:

override fun onServiceConnected{

super.onServiceConnected

Log.d( "~~~", "onServiceConnected")

thread {

Thread.sleep( 5000)

ClickUtils.click( this, 622f, 406f)

}

}

可以看到,我們在 onServiceConnected 方法中,開啟了一個線程,先睡眠 5s,再調用 ClickUtils.click(this, 622f, 406f) 方法點擊 (622,406)。之所以要睡眠 5s,是因為在設置中開啟了輔助服務后,onServiceConnected 方法就會立刻回調,而我們要從設置頁面返回到此頁面才能看到這個按鈕被點擊的效果,返回過程需要一點時間。

開測

可以看到,我先點擊了第一個按鈕到達輔助服務設置頁面,在開啟輔助服務后,我立即返回了 MainActivity,等待幾秒后,Test 按鈕被自動點擊了。說明我們的輔助點擊功能已經(jīng)正常工作了。

注:實際上這里的點擊并不局限于本應用內(nèi),之所以要返回這個頁面再點擊,只是為了講解時更方便,讓大家能更清楚地看到效果。

應用外截屏

應用內(nèi)截屏

在講解 Android 應用外截屏之前,我們先看一下 Android 應用內(nèi)截屏。在 Android 應用內(nèi)截屏非常簡單,只需要獲取 View 的緩存即可:

fun screenShot(activity: Activity): Bitmap {

returnview2Bitmap(activity.window.decorView)

}

fun view2Bitmap(view: View): Bitmap {

view.isDrawingCacheEnabled = true

returnview.drawingCache

}

本文重點講述應用外截屏。應用外截屏其實也不復雜,只需要兩步:

通過 MediaProjectionManager 的 getMediaProjection 方法獲取到 MediaProjection 對象。

再通過 MediaProjection 的 createVirtualDisplay 方法就能截取屏幕了。

通過 MediaProjectionManager 的 getMediaProjection 方法獲取到 MediaProjection 對象。

再通過 MediaProjection 的 createVirtualDisplay 方法就能截取屏幕了。

應用外截屏

構建 MediaProjectionManager 對象的方式非常簡單,調用 getSystemService(MEDIA_PROJECTION_SERVICE) 方法就可以了:

privateval mediaProjectionManager: MediaProjectionManager by lazy { getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager }

構建 MediaProjection 稍微復雜一點,構建 MediaProjection 對象需要兩個參數(shù),一個 resultCode,一個 resultData。

這兩個參數(shù)什么意思呢,為什么需要它們呢?

這是因為截取應用外屏幕有侵犯用戶隱私的風險,所以截屏之前需要獲得用戶的同意。所以在截屏前需要調用 startActivityForResult 方法詢問用戶:這個應用準備截屏了,你同意嗎?

在用戶同意后,onActivityResult 方法中就會攜帶 resultCode 和 resultData 參數(shù)。有了這兩個參數(shù),我們就可以構建 MediaProjection 對象了。

Talk is cheap, show me the code. 我們來一起寫個 Demo。

首先是布局文件:

?xml version= "1.0"encoding= "utf-8"?

androidx.constraintlayout.widget.ConstraintLayout xmlns:android= "http://schemas.android.com/apk/res/android"

xmlns:app= "http://schemas.android.com/apk/res-auto"

xmlns:tools= "http://schemas.android.com/tools"

android:layout_width= "match_parent"

android:layout_height= "match_parent"

tools:context= ".MainActivity"

SurfaceView

android:id= "@+id/surfaceView"

android:layout_width= "match_parent"

android:layout_height= "0dp"

app:layout_constraintBottom_toTopOf= "@id/btnStart"

app:layout_constraintTop_toTopOf= "parent"/

Button

android:id= "@+id/btnStart"

android:layout_width= "match_parent"

android:layout_height= "wrap_content"

android:text= "Start Screen Capture"

android:textAllCaps= "false"

app:layout_constraintBottom_toTopOf= "@id/btnStop"

app:layout_constraintTop_toBottomOf= "@id/surfaceView"/

Button

android:id= "@+id/btnStop"

android:layout_width= "match_parent"

android:layout_height= "wrap_content"

android:text= "Stop Screen Capture"

android:textAllCaps= "false"

app:layout_constraintBottom_toBottomOf= "parent"/

/androidx.constraintlayout.widget.ConstraintLayout

效果圖:

布局文件中,有一個 SurfaceView,待會我們將用它來展示截圖內(nèi)容。底部有兩個按鈕,一個 Start Screen Capture,一個 Stop Screen Capture,分別表示開始截圖和停止截圖。在 build.gradle 中開啟 ViewBinding,使得引用控件更加方便:

buildFeatures {

viewBinding true

}

在 MainActivity 中:

constval REQUEST_MEDIA_PROJECTION = 1

classMainActivity: AppCompatActivity{

privateval binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

privateval mediaProjectionManager: MediaProjectionManager by lazy { getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager }

privatevar mediaProjection: MediaProjection? = null

privatevar virtualDisplay: VirtualDisplay? = null

override fun onCreate(savedInstanceState: Bundle?){

super.onCreate(savedInstanceState)

setContentView(binding.root)

binding.btnStart.setOnClickListener {

Log.d( "~~~", "Requesting confirmation")

startActivityForResult(mediaProjectionManager.createScreenCaptureIntent, REQUEST_MEDIA_PROJECTION)

}

binding.btnStop.setOnClickListener {

Log.d( "~~~", "Stop screen capture")

stopScreenCapture

}

}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?){

super.onActivityResult(requestCode, resultCode, data)

if(requestCode == REQUEST_MEDIA_PROJECTION) {

if(resultCode != RESULT_OK) {

Log.d( "~~~", "User cancelled")

return

}

Log.d( "~~~", "Starting screen capture")

mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!)

virtualDisplay = mediaProjection!!.createVirtualDisplay(

"ScreenCapture",

ScreenUtils.getScreenWidth, ScreenUtils.getScreenHeight, ScreenUtils.getScreenDensityDpi,

DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,

binding.surfaceView.holder.surface, null, null

)

}

}

privatefun stopScreenCapture{

Log.d( "~~~", "stopScreenCapture, virtualDisplay = $virtualDisplay")

virtualDisplay?.release

virtualDisplay = null

}

}

其中,用到的 ScreenUtils 的作用是獲取屏幕的寬高和密度。代碼如下:

object ScreenUtils {

fun getScreenWidth: Int {

returnResources.getSystem.displayMetrics.widthPixels

}

fun getScreenHeight: Int {

returnResources.getSystem.displayMetrics.heightPixels

}

fun getScreenDensityDpi: Int {

returnResources.getSystem.displayMetrics.densityDpi

}

}

當點擊 Start 按鈕時,調用 startActivityForResult 詢問用戶是否同意截屏,這個方法中傳入的 Intent 是 mediaProjectionManager.createScreenCaptureIntent,這是專門用于詢問用戶是否同意截屏的 Intent,調用這行代碼后,會彈出這樣一個彈窗:

如果用戶點了確認,也就是上圖中的 “Start now” 按鈕,onActivityResult 就會收到 resultCode == RESULT_OK,以及用戶確認后的 data,通過這兩個參數(shù),我們就能構建出 mediaProjection 對象了。

獲取到 mediaProjection 對象后,通過 createVirtualDisplay 方法開始截屏。這個方法接收多個參數(shù),第一個參數(shù)表示 VirtualDisplay 的名字,隨意傳入一個字符串即可。

緊跟著的三個參數(shù)表示屏幕的寬高和密度。下一個參數(shù) DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 表示 VirtualDisplay 的 flag,有多種值可選,我暫時不清楚幾種 flag 的區(qū)別,不妨先記做固定寫法。下一個參數(shù)表示展示截圖結果的 Surface,這里傳入 binding.surfaceView.holder.surface,截圖結果就會展示到 SurfaceView 上了。最后兩個參數(shù)一個是 callback,一個是 handler,是用來處理截圖的回調的,我們暫時用不上,都傳入 null 即可。

需要注意的是,當 createVirtualDisplay 方法調用后,設備就會不斷地獲取當前屏幕,直到 createVirtualDisplay 創(chuàng)建的 virtualDisplay 對象被 release 才會停止截屏。所以我們在 Stop 按鈕的點擊事件中,調用了 virtualDisplay 的 release 方法。

整體來說代碼還是很簡單的,我們運行一下試試:

可以看到,直接 crash 了...查看 Logcat 控制臺:

java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION

報了一個 SecurityException,Media projections 需要一個帶有 ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION 類型的前臺 Service。

前臺 Service

我在編寫這個 Demo 時,targetSdk 設置的是最新的版本:31,事實上,如果讀者在編寫此 Demo 時,targetSdk 的版本在 28 或以下,就不會遇到這個錯誤,此時就已經(jīng)能正常截屏了。

只有 targetSdk 在 28 以上時,才會出現(xiàn)這個錯誤。SDK 28 代表 Android 9.0,在 Android 9.0 以后,才要求截屏時必須運行一個前臺 Service。

所以修復這個 crash 有兩種方案:

把 targetSdk 改成 28,

創(chuàng)建前臺 Service,適配 Android 9.0 以上版本。

把 targetSdk 改成 28,

創(chuàng)建前臺 Service,適配 Android 9.0 以上版本。

我更傾向于第二種方案,因為這個項目是我寫給自己練手的,我希望用最新的 API;并且將截圖功能放到 Service 中其實也更符合我的需求。

首先新建一個 Service:

classCaptureService: Service{

override fun onBind(intent: Intent?): IBinder? {

returnnull

}

}

在 AndroidManifest 中,添加 FOREGROUND_SERVICE 權限,注冊此 Service:

uses-permission android:name= "android.permission.FOREGROUND_SERVICE"/

application

...

...

service

android:name= ".CaptureService"

android:foregroundServiceType= "mediaProjection"/

/application

此 Service 需要添加 android:foregroundServiceType="mediaProjection" 屬性,表示這是用于截屏的 Service。

新建 MyApplication,注冊前臺 Notification Channel:

constval SCREEN_CAPTURE_CHANNEL_ID = "Screen Capture ID"

constval SCREEN_CAPTURE_CHANNEL_NAME = "Screen Capture"

classMyApplication: Application{

override fun onCreate{

super.onCreate

createScreenCaptureNotificationChannel

apk怎么拆包修改(如何拆apk安裝包)

}

privatefun createScreenCaptureNotificationChannel{

val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager

// Create the channel for the notification

val screenCaptureChannel = NotificationChannel(SCREEN_CAPTURE_CHANNEL_ID, SCREEN_CAPTURE_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW)

// Set the Notification Channel for the Notification Manager.

notificationManager.createNotificationChannel(screenCaptureChannel)

}

}

不要忘了在 AndroidManifest 中聲明此 Application:

application

android:name= ".MyApplication"

.../

然后,在 CaptureService 中,啟用前臺通知:

classCaptureService: Service{

override fun onCreate{

super.onCreate

startForeground( 1, NotificationCompat.Builder( this, SCREEN_CAPTURE_CHANNEL_ID).build)

}

override fun onBind(intent: Intent?): IBinder? {

returnnull

}

}

這樣就寫好了一個前臺 Service。

修改 MainActivity 中的代碼,點擊 Start 后,先啟動 Service,再調用截屏:

binding.btnStart.setOnClickListener {

startForegroundService(Intent( this, CaptureService:: class. java))

Log. d("~~~", " Requestingconfirmation")

startActivityForResult( mediaProjectionManager. createScreenCaptureIntent, REQUEST_MEDIA_PROJECTION)

}

此時運行就不會報錯了,效果如下:

可以看到,已經(jīng)可以成功截圖了,前文說過,當 createVirtualDisplay 方法調用后,設備就會不斷地獲取當前屏幕,所以才會看到截圖畫面層層疊疊的效果。

在 Google 官方提供的截圖 Demo 中,運行效果也是類似的,感興趣的讀者可以在 github 上查看 Google 官方的 Demo:

https://github.com/android/media-samples/tree/main/ScreenCapture

https://github.com/android/media-samples/tree/main/ScreenCapture

注:只要啟動了這樣一個前臺 Service,即使沒有把截屏邏輯移到 Service 中,也已經(jīng)可以正常截屏了。但更好的做法是把截圖邏輯移到 Service 中,感興趣的讀者可以自行實現(xiàn)。

截圖一次并取其 Bitmap

雖然現(xiàn)在截圖成功了,但運行效果并不是我們想要的。一般我們想要的效果是,截圖一次并取其 Bitmap。

為了實現(xiàn)這個效果,我們需要使用一個新的類:ImageReader。ImageReader 中包含一個 Surface 對象,在 createVirtualDisplay 方法中,將 binding.surfaceView.holder.surface 替換成 ImageReader 的 Surface 對象,就可以將截圖結果記錄到 ImageReader 中了。

創(chuàng)建 ImageReader:

privateval imageReader by lazy { ImageReader.newInstance(ScreenUtils.getScreenWidth, ScreenUtils.getScreenHeight, PixelFormat.RGBA_8888, 1) }

創(chuàng)建時需要傳入屏幕的寬高,第三個參數(shù)表示圖片的格式,這里傳入的是 PixelFormat.RGBA_8888。

注:實際上寫 PixelFormat.RGBA_8888 時,Android Studio 會報錯,因為它預期的是傳入一個 ImageFormat。PixelFormat.RGBA_8888 對應的常量是 1,但 ImageFormat 中沒有對應常量 1 的格式。我嘗試過換成 ImageFormat 中的其他格式,但換了之后始終運行不了。而這里的報錯卻并不影響程序運行,所以我就任由它報紅了。如果讀者有更好的方案,望不吝賜教:

最后一個參數(shù)表示最多保存幾張圖片,我們傳入 1 就可以了。

創(chuàng)建好 ImageReader 后,接下來替換掉 createVirtualDisplay 方法中的參數(shù),并獲取 imageReader 中的截圖結果:

virtualDisplay = mediaProjection!!.createVirtualDisplay(

"ScreenCapture",

ScreenUtils.getScreenWidth, ScreenUtils.getScreenHeight, ScreenUtils.getScreenDensityDpi,

DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,

imageReader.surface, null, null

)

Handler(Looper.getMainLooper).postDelayed({

val image = imageReader.acquireLatestImage

if(image != null) {

Log.d( "~~~", "get image: $image")

} else{

Log.d( "~~~", "image == null")

}

stopScreenCapture

}, 1000)

可以看到,代碼中先是將 imageReader.surface 傳入了 createVirtualDisplay 方法中,使得截圖結果記錄到 ImageReader 中。再等待了 1s 鐘,然后調用 imageReader.acquireLatestImage 獲取 imageReader 中記錄的截圖結果,它是一個 Image 對象。之所以等待 1s 是因為截圖需要一定的時間,并且在獲取到截圖結果后,我們需要調用 stopScreenCapture 將 virtualDisplay 對象釋放掉,否則這里會一直截圖。并且如果不釋放的話,在下一次截圖時會報以下錯誤:

java.lang.IllegalStateException: maxImages ( 1) has already been acquired, call #close before acquiring more.

獲取到 Image 對象后,可以將其轉換成 Bitmap 對象,轉換工具類如下:

object ImageUtils {

fun imageToBitmap(image: Image): Bitmap {

val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)

bitmap.copyPixelsFromBuffer(image.planes[ 0].buffer)

image.close

returnbitmap

}

}

這樣我們就實現(xiàn)了截圖一次并取其 Bitmap。不妨將這個 Bitmap 設置到 ImageView 上,看看效果。首先修改布局文件:

?xml version= "1.0"encoding= "utf-8"?

androidx.constraintlayout.widget.ConstraintLayout xmlns:android= "http://schemas.android.com/apk/res/android"

xmlns:app= "http://schemas.android.com/apk/res-auto"

xmlns:tools= "http://schemas.android.com/tools"

android:layout_width= "match_parent"

android:layout_height= "match_parent"

tools:context= ".MainActivity"

ImageView

android:id= "@+id/iv"

android:layout_width= "match_parent"

android:layout_height= "0dp"

app:layout_constraintBottom_toTopOf= "@id/btnStart"

app:layout_constraintTop_toTopOf= "parent"/

Button

android:id= "@+id/btnStart"

android:layout_width= "match_parent"

android:layout_height= "wrap_content"

android:text= "Start Screen Capture"

android:textAllCaps= "false"

app:layout_constraintBottom_toTopOf= "@id/btnStop"

app:layout_constraintTop_toBottomOf= "@id/iv"/

Button

android:id= "@+id/btnStop"

android:layout_width= "match_parent"

android:layout_height= "wrap_content"

android:text= "Stop Screen Capture"

android:textAllCaps= "false"

app:layout_constraintBottom_toBottomOf= "parent"/

/androidx.constraintlayout.widget.ConstraintLayout

唯一的修改是把之前布局文件中的 SurfaceView 換成了 ImageView,id 也對應換成了 iv。然后將獲取到的 Image 轉成 Bitmap,并設置到 ImageView 上:

binding.iv.setImageBitmap(ImageUtils.imageToBitmap(image))

運行效果如下:

可以看到,點擊 Start 按鈕后,等待 1s 后,就完成了截圖,并且展示到了 ImageView 上。這里的截圖并不局限于本應用內(nèi),不妨看一個截取應用外屏幕的效果:(注:我在錄制這個效果時將截圖等待時間延長到了 3s,以保證截圖時完全退到了桌面)

可以看到,確實可以截取到應用外的屏幕。

只讓用戶同意一次

現(xiàn)在的截圖還有一個問題,每次截圖前都會詢問用戶是否同意截圖。雖然我們可以通過上文介紹的模擬點擊幫用戶點同意,但更好的做法是將用戶同意的結果保存起來,下次截圖前直接使用即可。我們修改一下 Demo 看看效果。

MainActivity 修改如下:

constval REQUEST_MEDIA_PROJECTION = 1

classMainActivity: AppCompatActivity{

privateval binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

privateval mediaProjectionManager: MediaProjectionManager by lazy { getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager }

privatevar mediaProjection: MediaProjection? = null

privatevar virtualDisplay: VirtualDisplay? = null

privateval handler by lazy { Handler(Looper.getMainLooper) }

privateval imageReader by lazy { ImageReader.newInstance(ScreenUtils.getScreenWidth, ScreenUtils.getScreenHeight, PixelFormat.RGBA_8888, 1) }

override fun onCreate(savedInstanceState: Bundle?){

super.onCreate(savedInstanceState)

setContentView(binding.root)

binding.btnStart.setOnClickListener {

startForegroundService(Intent( this, CaptureService:: class. java))

startScreenCapture

}

binding. btnStop. setOnClickListener{

Log.d( "~~~", "Stop screen capture")

stopScreenCapture

}

}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?){

super.onActivityResult(requestCode, resultCode, data)

if(requestCode == REQUEST_MEDIA_PROJECTION) {

if(resultCode != RESULT_OK) {

Log.d( "~~~", "User cancelled")

return

}

Log.d( "~~~", "Starting screen capture")

mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!)

setUpVirtualDisplay

}

}

privatefun startScreenCapture{

if(mediaProjection == null) {

Log.d( "~~~", "Requesting confirmation")

// This initiates a prompt dialog for the user to confirm screen projection.

startActivityForResult(mediaProjectionManager.createScreenCaptureIntent, REQUEST_MEDIA_PROJECTION)

} else{

Log.d( "~~~", "mediaProjection != null")

setUpVirtualDisplay

}

}

privatefun setUpVirtualDisplay{

Log.d( "~~~", "setUpVirtualDisplay")

virtualDisplay = mediaProjection!!.createVirtualDisplay(

"ScreenCapture",

ScreenUtils.getScreenWidth, ScreenUtils.getScreenHeight, ScreenUtils.getScreenDensityDpi,

DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,

imageReader.surface, null, null

)

handler.postDelayed({

val image = imageReader.acquireLatestImage

if(image != null) {

Log.d( "~~~", "get image: $image")

binding.iv.setImageBitmap(ImageUtils.imageToBitmap(image))

} else{

Log.d( "~~~", "image == null")

}

stopScreenCapture

}, 1000)

}

privatefun stopScreenCapture{

Log.d( "~~~", "stopScreenCapture, virtualDisplay = $virtualDisplay")

virtualDisplay?.release

virtualDisplay = null

}

}

主要修改在于多了一個 startScreenCapture 方法,在這個方法中,先判斷 mediaProjection 是否已經(jīng)存在,如果不存在,則執(zhí)行剛才的邏輯,調用 startActivityForResult 請求用戶同意截屏。如果已經(jīng)存在,則直接調用 createVirtualDisplay 截屏即可。

運行效果:

這樣就實現(xiàn)了用戶只需同意一次截屏權限,應用就能多次截屏的功能。

通過上文介紹的模擬點擊,在獲取截屏權限時,可以實現(xiàn)自動點擊同意。然后就可以愉快地多次截屏了。

由于這種截屏方式不局限于本應用內(nèi),所以可以在后臺默默地不斷截取屏幕。接下來我們再學習一點基本的圖像識別技術,把截取到的屏幕利用起來。

圖片識別

我采用的方式是對比圖片的相似度,以達到知道當前在哪一屏的效果,然后就能通過輔助功能點擊這一屏中設定好的坐標了

第一種對比方式

第一種對比方式是:取出兩張 bitmap 中的所有像素,然后一一進行對比。匹配的點除以總點數(shù)就能得到一個相似度。代碼如下:

object SimilarityUtils {

fun similarity(bitmap1: Bitmap, bitmap2: Bitmap): Double {

// 獲取圖片所有的像素

val pixels1 = getPixels(bitmap1)

val pixels2 = getPixels(bitmap2)

// 總的像素點數(shù)以較大圖片為準

val totalCount = pixels1.size.coerceAtLeast(pixels2.size)

if(totalCount == 0) return0.00

var matchCount = 0

var i = 0

while(i pixels1.size i pixels2.size) {

if(pixels1[i] == pixels2[i]) {

// 統(tǒng)計相同的像素點數(shù)量

matchCount++

}

i++

}

// 相同的像素點數(shù)量除以總的像素點數(shù),得到相似比例。

returnString.format( "%.2f", matchCount.toDouble / totalCount).toDouble

}

privatefun getPixels(bitmap: Bitmap): IntArray {

val pixels = IntArray(bitmap.width * bitmap.height)

// 獲取每個像素的 RGB 值

bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)

returnpixels

}

}

可以看到,similarity 函數(shù)接收兩個 Bitmap,返回一個 Double 值,這個值的取值范圍是 0.00~1.00,表示相似度。

首先通過 bitmap.getPixels 取出所有的像素點,以其中較多的像素點作為總點數(shù)。

然后通過 pixels1[i] == pixels2[i] 對比每個像素點,如果相同則 matchCount 加一,最后用 matchCount / totalCount 計算出相似度。

這種比較方式特別直觀,容易理解,通過每個像素點依次比較得出相似度。我們也很容易想到它的缺點:如果第二張圖片是由第一張圖片縮放、變形、旋轉等變換得來的,那么每個像素點可能都無法匹配上,所以相似度會很低很低。

也就是說,這個算法幾乎只能用于比較圖片是否一模一樣,只要兩張圖的像素點有細微的錯位,比較結果就會完全不準確。

不過其實這種算法已經(jīng)能夠滿足我們的需求了,只要我們每次都取一樣的 Bitmap 進行比較就可以了。只要保證整張圖都一樣,或者從 Bitmap 裁剪出的固定區(qū)域一樣就可以了。此時比較結果可以供我們正常使用。

但更好的做法是通過 SIFT 算法計算相似度。

通過 SIFT 算法計算相似度

SIFT 算法指的是尺度不變特征轉換 (Scale Invariant Feature Transform)。它是計算機視覺領域中描述圖片特征的一種算法,應用非常廣泛。

這個算法是由一些大神們研究出來的,由于本文不是在寫論文,所以我也不會對這個算法進行深究,簡單介紹一下它的大概原理:

先將圖片映射為空間中的坐標:

再從所有坐標中過濾出其中的特征點:

再為特征點分配一個方向值,使得圖片變形后仍然能夠正確匹配:

將這些信息轉換成數(shù)學描述:

注:算法原理的這段內(nèi)容,只是我個人一點粗淺的理解,可能和算法的實際實現(xiàn)有出入。但這個算法的實現(xiàn)不是本文的重點,重點在于這個算法可以用于對比兩張圖片的相似度。所以于我而言,我愿將其稱之為魔法。

這個算法被封裝在 OpenCV 庫中,所以使用前需要導入 OpenCV 庫。

OpenCV 官方?jīng)]有提供 gradle 導入的方式,所以網(wǎng)上有許多導入 OpenCV 庫的教程,講的都是去下載 OpenCV 的源碼,再通過 Module 的方式加入項目中。

但國外有民間大佬為我們封裝了 gradle 導入的方式,大佬封裝的 github 地址:

https://github.com/quickbirdstudios/opencv-android

https://github.com/quickbirdstudios/opencv-android

所以現(xiàn)在我們可以直接在 build.gradle 中直接導入 OpenCV 庫:

implementation 'com.quickbirdstudios:opencv:4.5.3.0'

需要注意的是,OpenCV 庫非常大,導入這個庫會讓 apk 的體積增加 100 多 M,所以要慎用。

有了 OpenCV 庫,就可以編寫圖片相似度對比工具類了:

object SIFTUtils {

// SIFT detector

privateval siftDetector by lazy { SIFT.create }

fun similarity(bitmap1: Bitmap, bitmap2: Bitmap): Double {

// 計算每張圖片的特征點

val deors1 = computeDeors(bitmap1)

val deors2 = computeDeors(bitmap2)

// 比較兩張圖片的特征點

val deorMatcher = DeorMatcher.create(DeorMatcher.FLANNBASED)

val matches: ListMatOfDMatch = ArrayList

// 計算大圖中包含多少小圖的特征點。

// 如果計算小圖中包含多少大圖的特征點,結果會不準確。

// 比如:若小圖中的 50 個點都包含在大圖中的 100 個特征點中,則計算出的相似度為 100%,顯然不符合我們的預期

if(bitmap1.byteCount bitmap2.byteCount) {

deorMatcher.knnMatch(deors1, deors2, matches, 2)

} else{

deorMatcher.knnMatch(deors2, deors1, matches, 2)

}

Log.i( "~~~", "matches.size: ${matches.size}")

if(matches.isEmpty) return0.00

// 獲取匹配的特征點數(shù)量

var matchCount = 0

// 鄰近距離閥值,這里設置為 0.7,該值可自行調整

val nndrRatio = 0.7f

matches.forEach { match -

val array = match.toArray

// 用鄰近距離比值法(NNDR)計算匹配點數(shù)

if(array[ 0].distance = array[ 1].distance * nndrRatio) {

matchCount++

}

}

Log.i( "~~~", "matchCount: $matchCount")

returnString.format( "%.2f", matchCount.toDouble / matches.size).toDouble

}

privatefun computeDeors(bitmap: Bitmap): MatOfKeyPoint {

val mat = Mat

Utils.bitmapToMat(bitmap, mat)

val keyPoints = MatOfKeyPoint

siftDetector.detect(mat, keyPoints)

val deors = MatOfKeyPoint

// 計算圖片的特征點

siftDetector.compute(mat, keyPoints, deors)

returndeors

}

}

在這個類中,同樣有一個 similarity 方法,接收兩個 Bitmap,返回一個 0.00~1.00 的 Double 型數(shù)據(jù),表示圖片的相似度。

首先通過 SIFT.create 構建出用 SIFT 算法實現(xiàn)的圖片檢測器 siftDetector,再通過 siftDetector.compute 計算出圖片的特征點。

再通過 DeorMatcher.create 構建出 deorMatcher 對象,通過 deorMatcher.knnMatch 方法比較出兩張圖片相似的特征點數(shù)量。

這里比較時有一個 if 條件判斷,它的作用是保證比較的是大圖中包含多少小圖中的特征點。因為如果計算小圖中包含多少大圖的特征點,結果會不準確。

比如:若小圖中的 50 個點都包含在大圖中的 100 個特征點中,則計算出的相似度為 100%,顯然不符合我們的預期。

最后通過 array[0].distance = array[1].distance * nndrRatio 判斷特征點是否相似,統(tǒng)計出相似的特征點數(shù)量后,通過 matchCount / matches.size 計算出相似度。

測試

先在 res/drawable 文件夾下放一張圖片,比如我放了一張我的頭像,命名為 img.png:

然后修改 MainActivity 中的代碼:

classMainActivity: AppCompatActivity{

override fun onCreate(savedInstanceState: Bundle?){

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

val bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.img)

val bitmap2 = Bitmap.createBitmap(bitmap1, 0, 0, bitmap1.width / 2, bitmap1.height / 2)

Log.d( "~~~", "similarity: ${SIFTUtils.similarity(bitmap1, bitmap2)}")

}

}

首先通過 BitmapFactory.decodeResource 將 res/drawable 文件夾中的圖片取出來,轉換成 Bitmap,構建出 bitmap1。bitmap2 由 bitmap1 裁剪而來,通過 Bitmap.createBitmap 方法,從 bitmap1 的 (0, 0) 位置開始,裁剪出寬為原圖一半、高為原圖一半的 Bitmap。然后調用 SIFTUtils.similarity(bitmap1, bitmap2) 比較兩張圖片的相似度。

非常完美!

運行代碼,立馬 crash:

E/AndroidRuntime: FATAL EXCEPTION: main

Process: com.example.imagesimilarity, PID: 21924

java.lang.UnsatisfiedLinkError: No implementation found forlongorg.opencv.core.Mat.n_Mat (tried Java_org_opencv_core_Mat_n_1Mat and Java_org_opencv_core_Mat_n_1Mat__)

at org.opencv.core.Mat.n_Mat(Native Method)

at org.opencv.core.Mat.init(Mat.java: 23)

at com.example.imagesimilarity.SIFTUtils.computeDeors(SIFTUtils.kt: 50)

at com.example.imagesimilarity.SIFTUtils.similarity(SIFTUtils.kt: 19)

at com.example.imagesimilarity.MainActivity.onCreate(MainActivity.kt: 38)

at android.app.Activity.performCreate(Activity.java: 8000)

果然凡事都沒有一帆風順的。這個報錯大致意思是沒有找到 OpenCV 中的某個方法的具體實現(xiàn)。奇了怪了,我們明明已經(jīng)導入過 OpenCV 庫了。

查詢一番后,在 StackOverflow 上找到了答案,原因是 OpenCV 使用前需要先初始化。

MainActivity 代碼修改如下:

classMainActivity: AppCompatActivity{

override fun onCreate(savedInstanceState: Bundle?){

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

val loaded = OpenCVLoader.initDebug

Log.d( "~~~", "loaded: $loaded")

if(loaded) {

val bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.img)

val bitmap2 = Bitmap.createBitmap(bitmap1, 0, 0, bitmap1.width / 2, bitmap1.height / 2)

Log.d( "~~~", "similarity: ${SIFTUtils.similarity(bitmap1, bitmap2)}")

}

}

}

在 onCreate 方法中,先調用 OpenCVLoader.initDebug 方法初始化 OpenCV,通過其返回值判斷是否加載成功,當加載成功后再執(zhí)行我們剛才的比較相似度邏輯。

運行程序,Logcat 控制臺輸出如下:

D/~~~: loaded: true

I/~~~: matches.size: 190

I/~~~: matchCount: 88

D/~~~: similarity: 0.46

表示兩張圖片的相似度為 46%,說明我們的程序已經(jīng)正常工作了。

后記

到這里,我們的外掛三部曲就完結了。這三章講述了三個獨立的技術點:模擬點擊、應用外截屏、圖像識別。這些技術對用戶而言有些風險,所以通常都需要用戶手動授權。比如模擬點擊前需要用戶開啟輔助功能,截取屏幕前需要用戶同意應用讀取屏幕。

為什么沒有講他們的綜合運用呢?這實際上是我無奈之舉。這些技術綜合運用起來像是黑魔法,有些黑科技成分,不便細講,我平時也只運用在自己的個人手機上,讓它們幫我做一些機械的重復工作。這幾篇文章只是給大家介紹錘子、釘子、板子,如何用它們制作桌椅板凳還需要讀者親自動手。

為了防止失聯(lián),歡迎關注我防備的小號

微信改了推送機制,真愛請星標本公號??

掃描二維碼推送至手機訪問。

版權聲明:本文由飛速云SEO網(wǎng)絡優(yōu)化推廣發(fā)布,如需轉載請注明出處。

本文鏈接:http://m.thonggone.com/post/62289.html

分享給朋友:

“apk怎么拆包修改(如何拆apk安裝包)” 的相關文章

幼兒園中班家長會ppt模板免費下載(幼兒園中班家長會ppt模板免費下載手機版)

幼兒園中班家長會ppt模板免費下載(幼兒園中班家長會ppt模板免費下載手機版)

今天給各位分享幼兒園中班家長會ppt模板免費下載的知識,其中也會對幼兒園中班家長會ppt模板免費下載手機版進行解釋,如果能碰巧解決你現(xiàn)在面臨的問題,別忘了關注本站,現(xiàn)在開始吧!本文目錄一覽: 1、中班下學期家長會總結模板 2、中班下學期家長會PPT應該從那幾個方面入手 3、中班家長會方案及流...

電影先生模板2.0(電影先生模板下載)

電影先生模板2.0(電影先生模板下載)

本篇文章給大家談談電影先生模板2.0,以及電影先生模板下載對應的知識點,希望對各位有所幫助,不要忘了收藏本站喔。 本文目錄一覽: 1、如何評價電影《樹先生》? 2、求電影《迪茲先生》 720P下載資源 3、影片ID獲取失敗可能模板不支持 4、誰有韓國電影“蘇格拉底先生”完整版? 5、...

給別人做網(wǎng)站賺錢嗎(做一個網(wǎng)站可以掙錢嗎)

給別人做網(wǎng)站賺錢嗎(做一個網(wǎng)站可以掙錢嗎)

本篇文章給大家談談給別人做網(wǎng)站賺錢嗎,以及做一個網(wǎng)站可以掙錢嗎對應的知識點,希望對各位有所幫助,不要忘了收藏本站喔。 本文目錄一覽: 1、怎么在網(wǎng)上工作賺錢 2、上班族副業(yè)做什么能賺錢 3、家庭電腦(內(nèi)網(wǎng)),做網(wǎng)站賺錢嗎,做什么網(wǎng)站好,怎么做 4、幫別人做網(wǎng)站賺錢嗎?需要注意哪些方面的問...

會員商城積分騙局是真的嗎(會員商城積分騙局是真的嗎嗎)

會員商城積分騙局是真的嗎(會員商城積分騙局是真的嗎嗎)

今天給各位分享會員商城積分騙局是真的嗎的知識,其中也會對會員商城積分騙局是真的嗎嗎進行解釋,如果能碰巧解決你現(xiàn)在面臨的問題,別忘了關注本站,現(xiàn)在開始吧!本文目錄一覽: 1、今日兌積分是真的嗎 2、悅影繪積分兌換是真的嗎 3、淘品商城里面的積分是真的嗎? 今日兌積分是真的嗎 假的。今日兌積分...

淘寶主圖模板下載(淘寶主圖模板制作軟件)

淘寶主圖模板下載(淘寶主圖模板制作軟件)

本篇文章給大家談談淘寶主圖模板下載,以及淘寶主圖模板制作軟件對應的知識點,希望對各位有所幫助,不要忘了收藏本站喔。 本文目錄一覽: 1、如何制作淘寶主圖視頻?求大師幫忙 2、怎樣做淘寶主圖 3、淘寶主圖下載軟件哪個好用? 如何制作淘寶主圖視頻?求大師幫忙 1、 暴風影音:獲益最大的應用,...

首涂模板(首涂模板影視大全)

首涂模板(首涂模板影視大全)

本篇文章給大家談談首涂模板,以及首涂模板影視大全對應的知識點,希望對各位有所幫助,不要忘了收藏本站喔。 本文目錄一覽: 1、我想做個影視網(wǎng)站用什么模板比較好? 我想做個影視網(wǎng)站用什么模板比較好? 模板 只是個皮囊而已。。?!W(wǎng)站 并不是說一個程序安裝了能訪問了...