You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UiAutomator中UiDevice.hasObject方法二次执行过慢问题求助

UiDevice.hasObject 首次快速返回,第二次耗时超10秒的原因分析与解决

问题描述

我编写了一个TikTok自动化测试用例,在切换到下一个视频前会检查当前视频类型并执行对应操作。其中VR视频检查的代码片段如下:

Log.i(TAG, "check if it's a vr video")
val byRule = By.clazz("android.view.View").descContains("点击体验VR直播,按钮")
if(device.hasObject(byRule)){
     Log.i(TAG, "vr video")
     device.findObject(byRule)?.click()
    SystemClock.sleep(time*1000L)
    device.pressBack()
} else {
    Log.d(TAG, "no")
}

测试中发现,UiDevice.hasObject方法首次执行时立即返回结果,但第二次执行耗时超过10秒,求问原因。完整测试代码如下:

package com.dvdface.qq.uitestdemo

import android.content.Context
import android.content.Intent
import android.os.SystemClock
import android.util.Log
import android.view.Surface
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.*
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Before

private const val TIMEOUT = 5000L
private const val PKG_NAME = "com.ss.android.ugc.aweme"


private const val TAG = "TiktokTest"

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See testing documentation.
 */
@RunWith(AndroidJUnit4::class)
class DouYinTest {


    private lateinit var device:UiDevice
    private lateinit var context:Context

    @Before
    fun setUp() {

        // init device
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

        // init context
        context = ApplicationProvider.getApplicationContext<Context>()
        assertNotNull(context)
    }

    @After
    fun tearDown() {

    }

    @Test
    @LargeTest
    fun fastFlingLiveVideos() {

        // launch
        launch(PKG_NAME)

        // click suggestion menu
        gotoSuggestionMenu()

        // fling video
        flingVideo(true, 5, 2*60*60)
    }

    /**
     * watch video by fling gesture
     *  videos have many categories:
     *      short video
     *      fullscreen video
     *      live video
     *      VR video
     *      picture video
     *      ai video
     *      reminder video
     * Params:
     * enter - whether to enter play page by click full screen watch / landscape watch / VR watch
     * time - how long to play in single video, in seconds
     * duration - how long to test, in seconds
     * Returns:
     *  None
     */
    private fun flingVideo(enter:Boolean=false, time:Int=3, duration:Long=14400) {

        val actionsForVideos = listOf<()->Unit>(
            {
                // fullscreen video
                Log.i(TAG, "check if it's a fullscreen video")
                val byRule = By.clazz("android.widget.LinearLayout").descContains("全屏观看,按钮")
                if(device.hasObject(byRule)) {
                    Log.i(TAG, "fullscreen video")
                    device.findObject(byRule)?.click()
                    SystemClock.sleep(time*1000L)
                    device.pressBack()
                } else {
                    Log.d(TAG, "no")
                }
            },  {
                // live video
                Log.i(TAG, "check if it's a live video")
                val byRule = By.clazz("android.widget.TextView").text("点击进入直播间")
                if(device.hasObject(byRule)){
                    Log.i(TAG, "live video")
                    device.findObject(byRule)?.click()
                    SystemClock.sleep(time*1000L)
                    device.pressBack()
                } else {
                    Log.d(TAG, "no")
                }
            },  {
                // vr video
                Log.i(TAG, "check if it's a vr video")
                val byRule = By.clazz("android.view.View").descContains("点击体验VR直播,按钮")
                if(device.hasObject(byRule)){
                    Log.i(TAG, "vr video")
                    device.findObject(byRule)?.click()
                    SystemClock.sleep(time*1000L)
                    device.pressBack()
                } else {
                    Log.d(TAG, "no")
                }
            },  {
                // picture video
                Log.i(TAG, "check if it's a picture video")
                val byRule = By.clazz("android.widget.LinearLayout").hasChild(By.clazz("android.widget.TextView").text("图文"))
                if(device.hasObject(byRule)) {
                        Log.i(TAG, "picture video")
                        SystemClock.sleep(time*1000L)
                } else {
                    Log.d(TAG, "no")
                }
            },  {
                // ai video
                Log.i(TAG, "check if it's a ai video")
                val byRule = By.clazz("android.widget.TextView").textContains("特效")
                if(device.hasObject(byRule)){
                    Log.i(TAG, "ai video")
                    SystemClock.sleep(time*1000L)
                } else {
                    Log.d(TAG, "no")
                }
            }
        )

        val startTime = SystemClock.elapsedRealtime()
        while((SystemClock.elapsedRealtime() - startTime) < duration * 1000L) {

            // according to video type , do something
            if(enter) {
                actionsForVideos.forEach{
                    it()
                }
            }

            // next
            fling()

            Log.i(TAG, "elapse ${(SystemClock.elapsedRealtime() - startTime)/1000}s")
        }

    }

    /**
     * fling gesture
     * Params:
     *  step - steps to fling, more steps more slower, default 6
     * Returns:
     *  none
     */
    private fun fling(step:Int = 6) {

        Log.i(TAG, "fling")
        when(device.displayRotation) {
            Surface.ROTATION_0, Surface.ROTATION_180 -> { Log.d(TAG, "fling in portrait"); device.swipe(500, 1400, 500, 800, step) }
            Surface.ROTATION_90, Surface.ROTATION_270 ->  { Log.d(TAG, "fling in landscape"); device.swipe(1200, 800, 1200, 300, step) }
            else -> Log.e(TAG, "unknown direction")
        }
    }

    /**
     * goto suggestion menu
     * Params:
     *  None
     * Returns:
     *  None
     */
    private fun gotoSuggestionMenu() {

        // click suggestion menu
        Log.i(TAG, "enter suggestion")

        Log.d(TAG, "find first page button")
        device.findObject(By.clazz("android.widget.TextView").textStartsWith("首页").descContains("首页,按钮"))?.let {
            Log.d(TAG, "click first page button")
            it.click()
        }

        Log.d(TAG, "find suggestion")
        device.findObject(By.clazz("android.widget.TextView").textStartsWith("推荐").descContains("推荐,按钮"))?.let {
            Log.d(TAG, "click suggestion button")
            it.click()
            it.wait(Until.descContains("已选中"), TIMEOUT)
        }
    }



    /**
     * launch app by clear Intent.FLAG_ACTIVITY_CLEAR_TASK
     * Params:
     *  package - package to launch
     *  timeout - launching timeout, default 5000 ms
     * Returns:
     *  None
     */
    private fun launch(packageName:String, timeout:Long=5000) {

        Log.i(TAG, "launch app")

        // get launch intent
        Log.d(TAG, "get launch intent")
        var intent = context.packageManager.getLaunchIntentForPackage(packageName)

        // intent can't be null
        assertNotNull(intent)

        intent?.apply {
            addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
        }

        // start app
        Log.d(TAG, "launch app by intent")
        context.startActivity(intent)

        // wait app
        Log.d(TAG, "wait app to launch")
        device.wait(Until.hasObject(By.pkg(packageName).depth(0)), timeout)

    }
}

原因分析

1. UI缓存未及时更新

UiAutomator首次执行hasObject时会缓存当前的UI层次结构,当你执行click()进入VR页面,再pressBack()返回视频流后,APP的UI已经发生了变化,但UiAutomator的缓存可能没有同步更新,导致第二次检查时需要重新遍历整个UI树,这会显著增加耗时。

2. UI未稳定就执行检查

从VR页面返回后,APP需要加载新的视频内容,此时UI处于动态加载状态,hasObject方法会使用默认的超时时间(约10秒)等待元素出现,直到超时后才返回false,表现为耗时过长。

3. 定位规则过于宽泛

你使用的By.clazz("android.view.View").descContains("点击体验VR直播,按钮")定位规则中,android.view.View是Android最基础的控件类,整个UI树中这类控件数量极多,每次检查都需要遍历大量控件,第二次检查时如果UI树因视频加载变得更复杂,耗时会进一步增加。

解决方案

1. 增加UI稳定等待

在pressBack()之后,等待页面回到稳定状态(比如等待推荐页的关键元素出现),再执行下一次检查:

device.pressBack()
// 等待推荐页标识元素出现,确保UI稳定
device.wait(Until.hasObject(By.clazz("android.widget.TextView").textStartsWith("推荐")), TIMEOUT)

2. 优化定位规则

缩小控件定位范围,结合父控件或更具体的属性,避免使用过于宽泛的android.view.View:

// 比如结合父布局的类型,缩小查找范围
val byRule = By.clazz("android.widget.FrameLayout").hasChild(
    By.clazz("android.view.View").descContains("点击体验VR直播,按钮")
)

3. 复用定位规则

将所有定位规则的定义移到循环外部,避免每次循环都重复创建规则,减少不必要的开销:

private fun flingVideo(enter:Boolean=false, time:Int=3, duration:Long=14400) {
    // 提前定义所有定位规则
    val fullscreenRule = By.clazz("android.widget.LinearLayout").descContains("全屏观看,按钮")
    val liveRule = By.clazz("android.widget.TextView").text("点击进入直播间")
    val vrRule = By.clazz("android.view.View").descContains("点击体验VR直播,按钮")
    // ...其他规则

    val actionsForVideos = listOf<()->Unit>(
        {
            Log.i(TAG, "check if it's a fullscreen video")
            if(device.hasObject(fullscreenRule)) {
                // ...操作
            }
        },
        {
            Log.i(TAG, "check if it's a vr video")
            if(device.hasObject(vrRule)){
                // ...操作
            }
        }
        // ...其他检查逻辑
    )

    // ...循环逻辑
}

4. 自定义超时时间的查找

使用findObject并指定较短的超时时间,替代hasObject的默认长超时:

// 自定义2秒超时,避免等待过久
val vrView = device.findObject(vrRule, 2000)
if(vrView != null){
    Log.i(TAG, "vr video")
    vrView.click()
    SystemClock.sleep(time*1000L)
    device.pressBack()
} else {
    Log.d(TAG, "no")
}

内容的提问来源于stack exchange,提问作者Kevin Ding

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 10:05:32