为什么在 JUnit 测试中,当自定义 ImageView 调用 startAnimation(Animation) 时 getActivity() 会被阻塞?

5
我写了一个Android应用程序,其中包含一个自定义的ImageView,它会定期旋转自身,使用startAnimation(Animation)。这个应用程序运行良好,但是如果我创建一个类型为ActivityInstrumentationTestCase2的JUnit测试,并且测试调用getActivity(),那么对getActivity()的调用直到应用程序进入后台(例如按下设备的主页按钮)才返回。

经过长时间的尝试和挫折,我发现如果我在我的自定义ImageView类中注释掉对startAnimation(Animation)的调用,那么getActivity()将立即返回。但是这将打败我自定义ImageView的目的,因为我确实需要对其进行动画处理。

有人能告诉我为什么getActivity()在我的JUnit测试中阻塞,但只有在使用startAnimation时才会出现吗?感谢任何可以提供解决方法或告诉我我的错误的人。

注意:解决方案需要与Android API级别10最低兼容。

以下是运行此程序所需的所有源代码(将PNG图像放入res/drawable中并将其命名为the_image.png):

activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <com.example.rotatingimageviewapp.RotatingImageView 
        android:id="@+id/rotatingImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/the_image" />

</RelativeLayout>

MainActivity.java:

package com.example.rotatingimageviewapp;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    private RotatingImageView rotatingImageView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        rotatingImageView = (RotatingImageView) findViewById(
                R.id.rotatingImageView);
        rotatingImageView.startRotation();
    }

    @Override
    protected void onPause() {
        super.onPause();
        rotatingImageView.stopRotation();
    }

    @Override
    protected void onResume() {
        super.onResume();
        rotatingImageView.startRotation();
    }

}

RotatingImageView.java(自定义ImageView):

package com.example.rotatingimageviewapp;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;

public class RotatingImageView extends ImageView {

    private static final long ANIMATION_PERIOD_MS = 1000 / 24;

    //The Handler that does the rotation animation
    private final Handler handler = new Handler() {

        private float currentAngle = 0f;
        private final Object animLock = new Object();
        private RotateAnimation anim = null;

        @Override
        public void handleMessage(Message msg) {
            float nextAngle = 360 - msg.getData().getFloat("rotation");
            synchronized (animLock) {
                anim = new RotateAnimation(
                        currentAngle,
                        nextAngle,
                        Animation.RELATIVE_TO_SELF,
                        .5f,
                        Animation.RELATIVE_TO_SELF,
                        .5f);
                anim.setDuration(ANIMATION_PERIOD_MS);
                /**
                 * Commenting out the following line allows getActivity() to
                 * return immediately!
                 */
                startAnimation(anim);
            }

            currentAngle = nextAngle;
        }

    };

    private float rotation = 0f;
    private final Timer timer = new Timer(true);
    private TimerTask timerTask = null;

    public RotatingImageView(Context context) {
        super(context);
    }

    public RotatingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RotatingImageView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    public void startRotation() {
        stopRotation();

        /**
         * Set up the task that calculates the rotation value
         * and tells the Handler to do the rotation
         */
        timerTask = new TimerTask() {

            @Override
            public void run() {
                //Calculate next rotation value
                rotation += 15f;
                while (rotation >= 360f) {
                    rotation -= 360f; 
                }

                //Tell the Handler to do the rotation
                Bundle bundle = new Bundle();
                bundle.putFloat("rotation", rotation);
                Message msg = new Message();
                msg.setData(bundle);
                handler.sendMessage(msg);
            }

        };
        timer.schedule(timerTask, 0, ANIMATION_PERIOD_MS);
    }

    public void stopRotation() {
        if (null != timerTask) {
            timerTask.cancel();
        }
    }

}

MainActivityTest.java:

package com.example.rotatingimageviewapp.test;

import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;

import com.example.rotatingimageviewapp.MainActivity;

public class MainActivityTest extends
        ActivityInstrumentationTestCase2<MainActivity> {

    public MainActivityTest() {
        super(MainActivity.class);
    }

    protected void setUp() throws Exception {
        super.setUp();
    }

    protected void tearDown() throws Exception {
        super.tearDown();
    }

    public void test001() {
        assertEquals(1 + 2, 3 + 0);
    }

    public void test002() {
        //Test hangs on the following line until app goes to background
        Activity activity = getActivity();
        assertNotNull(activity);
    }

    public void test003() {
        assertEquals(1 + 2, 3 + 0);
    }

}
5个回答

10

我不确定你们是否已经解决了这个问题。 但这是我的解决方案,只需覆盖方法getActivity():

@Override
    public MyActivity getActivity() {
        if (mActivity == null) {
            Intent intent = new Intent(getInstrumentation().getTargetContext(), MyActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            // register activity that need to be monitored.
            monitor = getInstrumentation().addMonitor(MyActivity.class.getName(), null, false);
            getInstrumentation().getTargetContext().startActivity(intent);
            mActivity = (MyActivity) getInstrumentation().waitForMonitor(monitor);
            setActivity(mActivity);
        }
        return mActivity;
    }

1
setActivity 方法在哪里? - kunal.c

1
我可以告诉你为什么会发生这种情况,并提供一个简单的解决方法。我认为你应该能够通过修改视图来解决,但是现在这个办法可以起作用。
问题在于,当你调用getActivity()时,它会经过一系列的方法,直到它遇到InstrumentationTestCase.java中的以下代码段。
public final <T extends Activity> T launchActivityWithIntent(
            String pkg,
            Class<T> activityCls,
            Intent intent) {
        intent.setClassName(pkg, activityCls.getName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        T activity = (T) getInstrumentation().startActivitySync(intent);
        getInstrumentation().waitForIdleSync();
        return activity;
    }

问题在于以下这条讨厌的线路:
getInstrumentation().waitForIdleSync();

由于您的动画,主线程永远不会闲置,因此它永远不会从此方法返回!如何解决?很容易,您需要重写此方法,以便不再有该行。您可能需要添加一些代码来等待确保活动已启动,否则此方法将返回得太快!我建议等待与此活动特定的视图。

谢谢您查看,保罗!我很感激您的见解。我认为了解这个waitForIdleSync()调用可以让我更接近解决方法。这将使它成为与这个(未回答的)问题相同的问题。然而,launchActivityWithIntent是一个final方法,因此无法被覆盖,所以我不确定如何实现您建议的解决方法。但正如我所说,我很感激这些信息。 - Gary Sheppard
1
你可以将该方法复制到测试类中并使用不同的名称,getActivity() 方法也是如此,然后使用它而不是专门重载它。我建议将其放入一个扩展您当前使用的类的类中,这样您就可以在所有测试中使用它。 - Paul Harris
好的,我现在懂了。谢谢你的解释。我已经实现了它,但不幸的是,阻塞调用实际上是在 startActivitySync(intent) 之前发生的,还没到 waitForIdleSync()。在 startActivitySync 中,阻塞调用是 mSync.wait() …… 非常复杂的线程处理,我想我需要更深入地研究才能弄清楚。如果我跳过 startActivitySync,那么我就无法启动要测试的活动。如果您有更多想法,请告诉我,尽管您已经帮了很多忙。 - Gary Sheppard
1
你可以尝试使用 getTargetContext().startActivity(intent); 来启动你的活动 (activity),其中 intent 是用于启动你的活动的意图。它应该能够按照我之前提到的相同警告工作,但这意味着某些奇怪的东西正在发生。请问您能否发布您的活动代码或其子集? - Paul Harris
谢谢,但是startActivity不会返回实际的活动。我在上面的原始问题中发布了活动代码。然而,我已经找到了一种不使用“Animation”旋转“ImageView”的不同方法,这不会阻止对“getActivity”的调用。我将在单独的答案中写出它。如果您(或任何人)发布一个有效并回答此问题的[SSCCE](http://www.sscce.org/),我将很乐意将其标记为接受的答案。再次感谢! - Gary Sheppard
显示剩余4条评论

0

我了解了这个问题的每一个解决方法,这是我的解决方案,它运行良好,谢谢大家;)

public class TestApk extends ActivityInstrumentationTestCase2 {

    private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME =
        "com.notepad.MainActivity";
    private static Class launcherActivityClass;
    static {

        try {
            launcherActivityClass = Class
                    .forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public TestApk () throws ClassNotFoundException {
        super(launcherActivityClass);
    }

    private Solo solo;

    @Override
    protected void setUp() throws Exception {
        solo = new Solo(getInstrumentation());
        Intent intent = new Intent(getInstrumentation().getTargetContext(), launcherActivityClass);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getInstrumentation().getTargetContext().startActivity(intent);
    }

    public void test_ookla_speedtest() {
        Boolean expect = solo.waitForText("Login", 0, 60*1000);
        assertTrue("xxxxxxxxxxxxxxxxxxx", expect);
    }

    @Override
    public void tearDown() throws Exception {
        solo.finishOpenedActivities();
        super.tearDown();
    }

}

0

更新:感谢@nebula提供的答案:https://dev59.com/83rZa4cB1Zd3GeqP68pU#24506584


我了解到一个简单的解决方法:使用不涉及Animation的不同方法来旋转图像:

Android: 通过角度旋转imageview中的图像

这并没有真正回答我的问题,但它可以解决这个问题。如果有人知道如何在自定义ImageView中使用Animation类时获取ActivityInstrumentationTestCase2.getActivity()返回的Activity,请发布一个SSCCE作为答案,如果它有效,我会接受它而不是这个答案。


0

我相信Paul Harris正确地回答了这个问题发生的原因。那么,如何更轻松地解决这个问题呢?答案很简单,如果您处于测试模式,请不要启动动画。那么,如何判断您是否处于测试模式?有几种方法可以做到这一点,但其中一种简单的方法是在您用于启动活动的意图中添加一些额外的数据。我将以使用AndroidJUnit为例提供代码(我的理解是ActivityInstrumentationTestCase2已被弃用,或者至少AndroidJUnit是执行仪器测试的新方法;我还假设AndroidJUnit也会调用waitForIdleSync,但我尚未验证)

@Rule
public ActivityTestRule<MainActivity> mActivityRule =
        new ActivityTestRule<>(MainActivity.class, true, false);

@Before
    public init() {
    Activity mActivity;
    Intent intent = new Intent();
    intent.put("isTestMode, true);
    mActivity = mActivityRule.launchActivity(intent);
}

在你的MainActivity的onCreate方法中,执行以下操作:
Boolean isTestMode = (Boolean)savedInstanceState.get("isTestMode");
if (isTestMode == null || !isTestMode) {
    rotatingImageView.startRotation();
}

在活动启动后,如果这对您很重要,您可以使用其他方式来启动旋转。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接