通过adb命令获取某个window或View/子View的绘制内容并输出为png图片的方法

1,导读

如果您有androidSDK里的monitor + Hierarchy View 工具,则推荐您使用这个UI工具。

否则,可以用本文中提到的,通过adb的方式来获取View里的画面内容。

AndroidStudio SDK里提供了一个monitor工具(新版本sdk obsolete掉了,需要通过特殊方法下载),里面内置的Hierarchy View窗口可以查看一些Window和View/子View的画面,可以方便我们分析一些画面相关问题。

应该是有方法,可以通过adb dump的方式,来获取Window里的View/子View的绘制内容!

问了DeepSeek,以及阅读了相关系统源码后,终于可以实现期望的效果。

DeepSeek的提示如下,可以忽略此处,比较冗长:
https://blog.csdn.net/Railshiqian/article/details/156202129

下图是monitor工具-HierarchyView查看Launcher activity画面的截图:

2,通过adb获取Window/View画面的方法

我们用的方法,是通过adb工具,跟system_server进程里的ViewServer建立连接,然后通过一些命令,来获取png文件。

以下流程,是此功能的时序原理:

adb连接到system_server的ViewService对象->

-> ViewService通过参数,选择某个窗口Window

-> 通过Binder通信,调用到App进程 -> ViewRootImpl-class W extends IWindow.Stub

-> 调用executeCommand方法输出Window信息,View信息和png数据

2.1 通过adb跟ViewServer工具建立连接

android系统的ViewServer监听了4939端口,我们可以通过此端口来进行通信。

首先通过adb进行主机端口和android设备端口映射:

java 复制代码
命令:
adb forward tcp:4939 tcp:4939

确认是否已建立连接,有输出则代表连接已建立:

java 复制代码
命令:
adb shell service call window 3
输出:
Result: Parcel(	00000000 00000001   '........')

2.2 获取Window列表和Window相关的View树结构

我们通过以下命令,获取可以dump的window列表:

java 复制代码
echo "LIST" | nc localhost 4939
输出为:
e56af8b com.android.car.cluster.home/com.android.car.cluster.home.ClusterHomeActivity
8738156 com.android.car.cluster.home/com.android.car.cluster.home.FakeFreeNavigationActivity
77bc29e com.android.car.cluster.home/com.android.car.cluster.home.ClusterMusicActivity
b8ae3e5 com.android.car.cluster.osdouble/com.android.car.cluster.osdouble.ClusterOsDoubleActivity
915b393 com.android.car.carlauncher/com.android.car.carlauncher.CarLauncher
c02ee0f com.android.car.carlauncher/com.android.car.carlauncher.AppGridActivity
78d034a InputMethod
dc2d390 
7648fb TopCarSystemBar
3379692 SystemUIOverlayWindow
6cea9d5 HeadsUpNotification
f9d30e2 BottomCarSystemBar
a282872 ScreenDecorOverlay
ff053ca ScreenDecorOverlayBottom
f54d52b InputMethod
DONE.

然后我们选择其中一个Window(以7648fb TopCarSystemBar为例),获取此Window对应的View树结构:

java 复制代码
echo "DUMP 7648fb" | nc localhost 4939
输出较长,我们截取前几个ViewGroup的信息:
com.android.systemui.navigationbar.views.NavigationBarFrame@66fb06a padding:mForegroundPaddingBottom=1,0 padding:mForegroundPaddingLeft=1,0 padding:mForegroundPaddingRight=1,0 padding:mForegroundPaddingTop=1,0 measurement:mMeasureAllChildren=5,false drawing:getClipChildren()=4,true drawing:getClipToPadding()=4,true focus:getDescendantFocusability()=24,FOCUS_BEFORE_DESCENDANTS drawing:getPersistentDrawingCache()=9,SCROLLING focus:getTouchscreenBlocksFocus()=5,false drawing:isChildrenDrawingOrderEnabled()=5,false layout:mChildCountWithTransientState=1,0 mGroupFlags=8,0x224053 events:mLastTouchDownIndex=2,-1 events:mLastTouchDownTime=1,0 events:mLastTouchDownX=3,0.0 events:mLastTouchDownY=3,0.0 drawing:getAlpha()=3,1.0 layout:getBaseline()=2,-1 accessibility:getContentDescription()=4,null focus:getDefaultFocusHighlightEnabled()=4,true drawing:getElevation()=3,0.0 getFilterTouchesWhenObscured()=5,false getFitsSystemWindows()=5,false focus:getFocusable()=14,FOCUSABLE_AUTO layout:getHeight()=2,76 accessibility:getImportantForAccessibility()=3,yes getImportantForAutofill()=4,auto getImportantForContentCapture()=4,auto accessibility:getLabelFor()=2,-1 drawing:getLayerType()=4,NONE layout:getLayoutDirection()=22,RESOLVED_DIRECTION_LTR layout_flags=9,0x1840028 layout_horizontalWeight=3,0.0 layout_mFitInsetsSides_LEFT=3,0x1 layout_mFitInsetsSides_TOP=3,0x2 layout_mFitInsetsSides_RIGHT=3,0x4 layout_mFitInsetsSides_BOTTOM=3,0x8 layout_mFitInsetsSides=2,15 layout_mFitInsetsTypes=1,0 layout_privateFlags_FIT_INSETS_CONTROLLED=10,0x10000000 layout_privateFlags_INTERCEPT_GLOBAL_DRAG_AND_DROP=10,0x80000000 layout_privateFlags=11,-1879048192 layout_type=21,STATUS_BAR_ADDITIONAL layout_verticalWeight=3,0.0 layout_x=1,0 layout_y=1,0 layout:layout_height=2,76 layout:layout_width=12,MATCH_PARENT layout:getLocationOnScreen_x()=1,0 layout:getLocationOnScreen_y()=1,0 measurement:getMeasuredHeightAndState()=2,76 measurement:getMeasuredWidthAndState()=4,1848 drawing:getPivotX()=5,924.0 drawing:getPivotY()=4,38.0 layout:getRawLayoutDirection()=3,LTR text:getRawTextAlignment()=7,GRAVITY text:getRawTextDirection()=7,INHERIT drawing:getRotation()=3,0.0 drawing:getRotationX()=3,0.0 drawing:getRotationY()=3,0.0 drawing:getScaleX()=3,1.0 drawing:getScaleY()=3,1.0 getScrollBarStyle()=14,INSIDE_OVERLAY drawing:getSolidColor()=1,0 accessibility:getStateDescription()=4,null getTag()=4,null text:getTextAlignment()=7,GRAVITY text:getTextDirection()=12,FIRST_STRONG drawing:getTransitionAlpha()=3,1.0 getTransitionName()=4,null drawing:getTranslationX()=3,0.0 drawing:getTranslationY()=3,0.0 drawing:getTranslationZ()=3,0.0 getVisibility()=7,VISIBLE layout:getWidth()=4,1848 drawing:getX()=3,0.0 drawing:getY()=3,0.0 drawing:getZ()=3,0.0 focus:hasFocus()=5,false drawing:hasOverlappingRendering()=4,true drawing:hasShadow()=5,false layout:hasTransientState()=5,false accessibility:isAccessibilityDataSensitive()=5,false isActivated()=5,false isClickable()=5,false drawing:isDrawingCacheEnabled()=5,false isEnabled()=4,true focus:isFocusable()=5,false focus:isFocusableInTouchMode()=5,false focus:isFocused()=5,false focus:isFocusedByDefault()=5,false drawing:isForceDarkAllowed()=4,true isHapticFeedbackEnabled()=4,true drawing:isHardwareAccelerated()=4,true isHovered()=5,false isInTouchMode()=4,true focus:isKeyboardNavigationCluster()=5,false layout:isLayoutRtl()=5,false drawing:isOpaque()=5,false isPressed()=5,false isSelected()=5,false isSoundEffectsEnabled()=4,true drawing:willNotCacheDrawing()=5,false drawing:willNotDraw()=4,true bg_=4,null layout:mBottom=2,76 drawing:mClipBounds=4,null theme:com.android.systemui:style/Theme.SystemUI()=6,forced theme:android:style/Theme.DeviceDefault.Light.DarkActionBar()=6,forced fg_=4,null mID=21,id/car_top_bar_window layout:mLeft=1,0 measurement:mMeasuredHeight=2,76 measurement:mMeasuredWidth=4,1848 measurement:mMinHeight=1,0 measurement:mMinWidth=1,0 padding:mPaddingBottom=1,0 padding:mPaddingLeft=1,0 padding:mPaddingRight=1,0 padding:mPaddingTop=1,0 mPrivateFlags=9,0x10088B0 layout:mRight=4,1848 scrolling:mScrollX=1,0 scrolling:mScrollY=1,0 mSystemUiVisibility=3,0x0 layout:mTop=1,0 padding:mUserPaddingBottom=1,0 padding:mUserPaddingEnd=11,-2147483648 padding:mUserPaddingLeft=1,0 padding:mUserPaddingRight=1,0 padding:mUserPaddingStart=11,-2147483648 mViewFlags=10,0x18000090 
 com.android.systemui.car.systembar.CarSystemBarView@6d5948f layout:mBaselineAligned=4,true layout:mBaselineAlignedChildIndex=2,-1 measurement:mBaselineChildTop=1,0 measurement:mGravity=8,0x800033 measurement:mOrientation=1,1 measurement:mTotalLength=2,76 layout:mUseLargestChild=5,false layout:mWeightSum=4,-1.0 drawing:getClipChildren()=4,true drawing:getClipToPadding()=4,true focus:getDescendantFocusability()=24,FOCUS_BEFORE_DESCENDANTS drawing:getPersistentDrawingCache()=9,SCROLLING focus:getTouchscreenBlocksFocus()=5,false drawing:isChildrenDrawingOrderEnabled()=5,false layout:mChildCountWithTransientState=1,0 mGroupFlags=8,0x224053 events:mLastTouchDownIndex=2,-1 events:mLastTouchDownTime=1,0 events:mLastTouchDownX=3,0.0 events:mLastTouchDownY=3,0.0 drawing:getAlpha()=3,1.0 layout:getBaseline()=2,-1 accessibility:getContentDescription()=4,null focus:getDefaultFocusHighlightEnabled()=4,true drawing:getElevation()=3,0.0 getFilterTouchesWhenObscured()=5,false getFitsSystemWindows()=5,false focus:getFocusable()=13,NOT_FOCUSABLE layout:getHeight()=2,76 accessibility:getImportantForAccessibility()=4,auto getImportantForAutofill()=4,auto getImportantForContentCapture()=4,auto accessibility:getLabelFor()=2,-1 drawing:getLayerType()=4,NONE layout:getLayoutDirection()=22,RESOLVED_DIRECTION_LTR layout:layout_bottomMargin=1,0 layout:layout_endMargin=11,-2147483648 layout:layout_leftMargin=1,0 layout:layout_mMarginFlags=4,0x0C layout:layout_rightMargin=1,0 layout:layout_startMargin=11,-2147483648 layout:layout_topMargin=1,0 layout:layout_height=12,MATCH_PARENT layout:layout_width=12,MATCH_PARENT layout:getLocationOnScreen_x()=1,0 layout:getLocationOnScreen_y()=1,0 measurement:getMeasuredHeightAndState()=2,76 measurement:getMeasuredWidthAndState()=4,1848 drawing:getPivotX()=5,924.0 drawing:getPivotY()=4,38.0 layout:getRawLayoutDirection()=7,INHERIT text:getRawTextAlignment()=7,GRAVITY text:getRawTextDirection()=7,INHERIT drawing:getRotation()=3,0.0 drawing:getRotationX()=3,0.0 drawing:getRotationY()=3,0.0 drawing:getScaleX()=3,1.0 drawing:getScaleY()=3,1.0 getScrollBarStyle()=14,INSIDE_OVERLAY drawing:getSolidColor()=1,0 accessibility:getStateDescription()=4,null getTag()=4,null text:getTextAlignment()=7,GRAVITY text:getTextDirection()=12,FIRST_STRONG drawing:getTransitionAlpha()=3,1.0 getTransitionName()=4,null drawing:getTranslationX()=3,0.0 drawing:getTranslationY()=3,0.0 drawing:getTranslationZ()=3,0.0 getVisibility()=7,VISIBLE layout:getWidth()=4,1848 drawing:getX()=3,0.0 drawing:getY()=3,0.0 drawing:getZ()=3,0.0 focus:hasFocus()=5,false drawing:hasOverlappingRendering()=4,true drawing:hasShadow()=5,false layout:hasTransientState()=5,false accessibility:isAccessibilityDataSensitive()=5,false isActivated()=5,false isClickable()=4,true drawing:isDrawingCacheEnabled()=5,false isEnabled()=4,true focus:isFocusable()=5,false focus:isFocusableInTouchMode()=5,false focus:isFocused()=5,false focus:isFocusedByDefault()=5,false drawing:isForceDarkAllowed()=4,true isHapticFeedbackEnabled()=4,true drawing:isHardwareAccelerated()=4,true isHovered()=5,false isInTouchMode()=4,true focus:isKeyboardNavigationCluster()=5,false layout:isLayoutRtl()=5,false drawing:isOpaque()=4,true isPressed()=5,false isSelected()=5,false isSoundEffectsEnabled()=4,true drawing:willNotCacheDrawing()=5,false drawing:willNotDraw()=4,true bg_state_mUseColor=9,-16777216 layout:mBottom=2,76 drawing:mClipBounds=4,null theme:com.android.systemui:style/Theme.SystemUI()=6,forced theme:android:style/Theme.DeviceDefault.Light.DarkActionBar()=6,forced fg_=4,null mID=14,id/car_top_bar layout:mLeft=1,0 measurement:mMeasuredHeight=2,76 measurement:mMeasuredWidth=4,1848 measurement:mMinHeight=1,0 measurement:mMinWidth=1,0 padding:mPaddingBottom=1,0 padding:mPaddingLeft=1,0 padding:mPaddingRight=1,0 padding:mPaddingTop=1,0 mPrivateFlags=9,0x1808830 layout:mRight=4,1848 scrolling:mScrollX=1,0 scrolling:mScrollY=1,0 mSystemUiVisibility=3,0x0 layout:mTop=1,0 padding:mUserPaddingBottom=1,0 padding:mUserPaddingEnd=11,-2147483648 padding:mUserPaddingLeft=1,0 padding:mUserPaddingRight=1,0 padding:mUserPaddingStart=11,-2147483648 mViewFlags=10,0x18004080 
 ......
 DONE.
DONE

2.3 获取View的绘制内容并保存到png文件

java 复制代码
命令如下, 7648fb是window的id,NavigationBarFrame@66fb06a是View的ID :

echo "CAPTURE 7648fb com.android.systemui.navigationbar.views.NavigationBarFrame@66fb06a" | nc localhost 4939 |tee 1.png
或
echo "CAPTURE 7648fb com.android.systemui.navigationbar.views.NavigationBarFrame@66fb06a" | nc localhost 4939 > 1.png

1.png的内容为,确认是我们认为的状态栏的图像:

3,原理分析和系统源码导读

3.1 通过adb跟system_server里的ViewServer功能建立连接

android-15.0.0_r17/frameworks/base/services/core/java/com/android/server/wm/ViewServer.java

java 复制代码
class ViewServer implements Runnable {
    /**
     * The default port used to start view servers.
     */
    public static final int VIEW_SERVER_DEFAULT_PORT = 4939;
    ......
        public void run() {

            BufferedReader in = null;
            try {
                in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);

                final String request = in.readLine();

                String command;
                String parameters;

                int index = request.indexOf(' ');
                if (index == -1) {
                    command = request;
                    parameters = "";
                } else {
                    command = request.substring(0, index);
                    parameters = request.substring(index + 1);
                }

                ...
            } ...
        }

3.2 找到目标Window并通过binder通信调用到app端

android-15.0.0_r17/frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

java 复制代码
    boolean viewServerWindowCommand(Socket client, String command, String parameters) {
        ......

        // Any uncaught exception will crash the system process
        try {
            // Find the hashcode of the window
            int index = parameters.indexOf(' ');
            if (index == -1) {
                index = parameters.length();
            }
            final String code = parameters.substring(0, index);
            int hashCode = (int) Long.parseLong(code, 16);

            // Extract the command's parameter after the window description
            if (index < parameters.length()) {
                parameters = parameters.substring(index + 1);
            } else {
                parameters = "";
            }

            final WindowState window = findWindow(hashCode);
            if (window == null) {
                return false;
            }

            data = Parcel.obtain();
            data.writeInterfaceToken("android.view.IWindow");
            data.writeString(command);
            data.writeString(parameters);
            data.writeInt(1);
            ParcelFileDescriptor.fromSocket(client).writeToParcel(data, 0);

            reply = Parcel.obtain();

            final IBinder binder = window.mClient.asBinder();
            // TODO: GET THE TRANSACTION CODE IN A SAFER MANNER
            // 通过binder调用到App进程ViewRootImpl的executeCommand方法
            binder.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);

        } ......

        return success;
    }

3.3 App进程执行command命令

android-15.0.0_r17/frameworks/base/core/java/android/view/ViewRootImpl.java

java 复制代码
        @Override
        public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {
            final ViewRootImpl viewAncestor = mViewAncestor.get();
            if (viewAncestor != null) {
                final View view = viewAncestor.mView;
                if (view != null) {
                    ......

                    OutputStream clientStream = null;
                    try {
                        clientStream = new ParcelFileDescriptor.AutoCloseOutputStream(out);
                        ViewDebug.dispatchCommand(view, command, parameters, clientStream);
                    } ......
                }
            }
        }

3.4 App进程将View画面保存到png文件中

android-15.0.0_r17/frameworks/base/core/java/android/view/ViewDebug.java

java 复制代码
    public static void capture(View root, final OutputStream clientStream, View captureView)
            throws IOException {
        Bitmap b = performViewCapture(captureView, false);

        if (b == null) {
            Log.w("View", "Failed to create capture bitmap!");
            // Send an empty one so that it doesn't get stuck waiting for
            // something.
            b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
                    1, 1, Bitmap.Config.ARGB_8888);
        }

        BufferedOutputStream out = null;
        try {
            out = new BufferedOutputStream(clientStream, 32 * 1024);
            b.compress(Bitmap.CompressFormat.PNG, 100, out);
            out.flush();
        } ......
    }

给个赞吧~

---------------------------------------------------end line--------------------------------------------------------

相关推荐
阿巴斯甜18 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker19 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952720 小时前
Andorid Google 登录接入文档
android
黄林晴21 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android