通过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--------------------------------------------------------

相关推荐
XI锐真的烦5 小时前
新手该如何选择 Android 开发框架?
android
weixin_439706256 小时前
Windows MySQL的主从复制配置记录
windows·mysql·adb
走在路上的菜鸟6 小时前
Android学Dart学习笔记第二十六节 并发
android·笔记·学习·flutter
00后程序员张6 小时前
AppStoreInfo.plist 在苹果上架流程中的生成方式和作用
android·小程序·https·uni-app·iphone·webview
成都大菠萝6 小时前
2-2-10 快速掌握Kotlin-out协变
android
成都大菠萝6 小时前
2-2-8 快速掌握Kotlin-vararg关键字与get函数
android
成都大菠萝6 小时前
2-2-7 快速掌握Kotlin-泛型类型约束
android
城东米粉儿6 小时前
Collections.synchronizedMap()与ConcurrentHashMap的区别笔记
android
愤怒的代码6 小时前
深入解析 Binder 运行的状态
android·app