UiAutomator中UiDevice.hasObject方法二次执行过慢问题求助
问题描述
我编写了一个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

