Android异步UI从服务更新失败

时间:2017-01-02 15:31:10

标签: android android-layout

我有一个非常奇怪的问题,用于更新UI。我有一个前台开始有界服务,我的主要过程在后台。当我启动应用程序时,我想检查服务是否已在运行并更改切换按钮的状态。对于这个问题,我在OnResume()中启动应用程序时绑定到我启动的服务,服务将一个值发送回我的应用程序,该值显示服务的运行状态,并根据此值更新UI。但问题是在这种情况下UI不会更新。

因为这个错误是在非常复杂的情况下显示的,所以我编写了一个重现此问题的示例代码。以下是这些代码(抱歉名字不好,错过了很多错误检查,我已经快速编写了这段代码,只是为了重现问题)。我已经将每个代码作为概述进行了讨论。

activity_main layout

<ToggleButton
    android:id="@+id/ui_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textOff="Off State"
    android:textOn="On State"
    android:checked="false" />
<Button
    android:id="@+id/start_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Start"/>

MyTestService.java

首先,这是我的示例前景启动有界服务。如您所见,当我们开始服务时,我们创建一个前台服务,它只运行一个小线程,每隔10秒切换一个mStatus变量10次,然后停止。每当我们绑定到此服务时,我们都会使用ResultReceiver通过绑定意图发送,以便将mStatus发送到应用。我们还允许重新绑定,因为应用程序可能会多次关闭并重新打开。

public class MyTestService extends Service {
    private volatile boolean mStatus = false;
    private MyThread mTh = new MyThread();

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mTh.start();

        Intent notintent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notintent, 0);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setContentText("Test").setContentIntent(pendingIntent).setContentTitle("title").setSmallIcon(R.drawable.ic_launcher);
        Notification notification = builder.build();
        startForeground(100, notification);

        return START_STICKY;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        if (intent != null && intent.getAction().equals("checkstatus")) {
            ResultReceiver recv = (ResultReceiver)intent.getParcelableExtra("myrecvextra");
            Bundle data = new Bundle();
            data.putBoolean("status", mStatus);
            recv.send(0, data);
        }

        return null;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return true;
    }

    @Override
    public void onRebind(Intent intent) {
        if (intent != null && intent.getAction().equals("checkstatus")) {
            ResultReceiver recv = (ResultReceiver)intent.getParcelableExtra("myrecvextra");
            Bundle data = new Bundle();
            data.putBoolean("status", mStatus);
            recv.send(0, data);
        }
    }

    public class MyThread extends Thread {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++ ) {
                    Thread.sleep(10000);
                    mStatus = !mStatus;
                    Log.i("ASD", String.format("%d", mStatus? 1 : 0));
                }
            }catch (Exception e) {
            }

            stopSelf();
        }
    }
}

MyServiceAccessClass.java

此类用于访问服务。 start()启动服务,bind()unbind()用于绑定和解除绑定服务。 mRecv是绑定时发送到服务的ResultReceiver,用于获取状态。绑定后收到状态时,ResultReceiver通过回调更新UI。

public class MyServiceAccessClass {
    private MyResultRecv mRecv = new MyResultRecv(new Handler(Looper.getMainLooper()));
    private OnUpdateRequest mCallback = null;
    private Context mCtx = null;
    private ServiceConnection mCon = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {}
        @Override
        public void onServiceDisconnected(ComponentName name) {}
    };

    public MyServiceAccessClass(Context ctx) {
        mCtx = ctx;
        mCallback = (OnUpdateRequest)ctx;
    }

    public void bind() {
        Intent intent = new Intent(mCtx, MyTestService.class);
        intent.setAction("checkstatus");
        intent.putExtra("myrecvextra", mRecv);
        mCtx.bindService(intent, mCon, Context.BIND_AUTO_CREATE);
    }

    public void unbind() {
        mCtx.unbindService(mCon);
    }

    public void start() {
        Intent intent = new Intent(mCtx, MyTestService.class);
        mCtx.startService(intent);
    }

    private class MyResultRecv extends ResultReceiver {

        public MyResultRecv(Handler handler) {
            super(handler);
        }

        @Override
        protected void onReceiveResult(int resultCode, Bundle resultData) {
            if (resultCode == 0) {
                mCallback.updateUi(resultData.getBoolean("status"));
            }
        }
    }
}

MainActivity.java

这是测试应用程序的主要类。开始按钮开始服务。这个类绑定在OnResume()中并在OnPause()中绑定。如果应用在服务已经投放且其mStatustrue时运行,则updateUi将调用true值,并设置切换按钮的状态。

interface OnUpdateRequest {
    public void updateUi(boolean state);
}

public class MainActivity extends AppCompatActivity implements OnUpdateRequest{
    private MyServiceAccessClass mTest = new MyServiceAccessClass (this);
    private ToggleButton mBtn = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtn = (ToggleButton)findViewById(R.id.ui_btn);
        ((Button)findViewById(R.id.start_btn)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTest.start();
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        mTest.bind();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mTest.unbind();
    }

    @Override
    public void updateUi(boolean state) {
        mBtn.setChecked(state);
    }
}

好的,现在理论上一切都很好。但是,如果您尝试使用此代码,当服务启动且mStatus为真时,将使用setChecked()调用切换按钮true(到目前为止这是正确的),但UI不会已更新以显示正确的文本和状态。有趣的是,如果您为此切换按钮运行isChecked,它将返回true,但UI会显示其他内容。

知道为什么会这样吗?很抱歉,很多代码出现了这个问题。

更新

我注意到了我应该提到的一些事情。如果我在isChecked之后立即使用setCheck,我会true这是正确的。但是如果我稍后再次使用isChecked(例如在另一个按钮事件处理程序中),它将返回false,而我还没有调用setChecked。我认为这种情况与我的问题有关,但我不知道这是怎么回事。

此外,我认为此问题与您在绑定服务的过程中更新UI有关。因为如果我在绑定过程中尝试使用相同的ResultReceiver更新应用主界面,那么一切正常。

2 个答案:

答案 0 :(得分:0)

可能需要在按钮上调用View.requestLayout()View.forceLayout()来刷新按钮状态。

答案 1 :(得分:0)

我终于发现了我的代码问题。 我花了很多时间来解决这个问题,所以我在这里发布给其他Android开发者。

通过ResultReceiver以某种方式显而易见地从服务发回结果。但是互联网上的大多数示例都没有显示服务重新绑定,而我从未发现重新绑定服务后发回结果

好的,现在有什么问题?从我的服务中查看以下代码部分:

@Nullable
@Override
public IBinder onBind(Intent intent) {
    if (intent != null && intent.getAction().equals("checkstatus")) {
        ResultReceiver recv = (ResultReceiver)intent.getParcelableExtra("myrecvextra");
        Bundle data = new Bundle();
        data.putBoolean("status", mStatus);
        recv.send(0, data);
    }

    return null;
}

@Override
public boolean onUnbind(Intent intent) {
    return true;
}

@Override
public void onRebind(Intent intent) {
    if (intent != null && intent.getAction().equals("checkstatus")) {
        ResultReceiver recv = (ResultReceiver)intent.getParcelableExtra("myrecvextra");
        Bundle data = new Bundle();
        data.putBoolean("status", mStatus);
        recv.send(0, data);
    }
}

这是一种制作重新绑定服务的常用方法,它基于您在Internet中找到的对服务的简单绑定。这已经在true中使用onUnbind()返回onRebind()并使用ResultReceiver完成了。但这种方法完全错误

为什么呢?因为android中的一个奇怪的设计。在Service OnRebind()中,有一个18字的小评论:

  

请注意此时Intent附带的任何额外内容   将不会在这里看到。

现在这意味着什么?这意味着带有ResultReceiver的额外值将在重新绑定时不可用,这反过来意味着重新绑定后不会发回结果。但由于未知原因,此代码不会出现任何异常,您甚至会在调试时在应用程序中看到结果,因此,为什么此代码不起作用,这是非常模糊的。

现在解决方案是什么?绑定到bindService()意图的服务时,永远不要发送ResultReceiver。尽管这对于非重新绑定服务是正确的,但我强烈建议避免使用它。在调用onServiceConnected时,通过单独的消息发送public static int SERVICE_SET_RECV = 1; public static String SERVICE_RECV = "SERVICE_RECV"; private Messenger mMessenger = new Messenger(new MyHandler(this)); private ResultReceiver mRecv = null; @Nullable @Override public IBinder onBind(Intent intent) { if (intent != null && intent.getAction().equals("checkstatus")) { return mMessenger.getBinder(); } return null; } @Override public boolean onUnbind(Intent intent) { mRecv = null; return true; } @Override public void onRebind(Intent intent) {} public void setRecv(ResultReceiver recv) { mRecv = recv; // Example to send some result back to app Bundle data = new Bundle(); data.putBoolean("status", mStatus); mRecv.send(0, data); } private static class MyHandler extends Handler { private final WeakReference<MyTestService> mService; public MyHandler(MyTestService service) { mService = new WeakReference<>(service); } @Override public void handleMessage(Message msg) { MyTestService service = mService.get(); Bundle data = msg.getData(); switch (msg.what) { case SERVICE_SET_RECV: { ResultReceiver recv = data.getParcelable(SERVICE_RECV); service.setRecv(recv); break; } default: super.handleMessage(msg); } } } 到服务,然后一切都像小菜一样。以下是我对代码的修改:

<强> MyTestService.java

private ServiceConnection mCon = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Messenger messenger = new Messenger(service);

        Bundle data = new Bundle();
        data.putParcelable(MyTestService.SERVICE_RECV, mRecv);
        Message msg = Message.obtain(null, MyTestService.SERVICE_SET_RECV, 0, 0);
        msg.setData(data);

        messenger.send(msg);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {}
};

public void bind() {
    Intent intent = new Intent(mCtx, MyTestService.class);
    mCtx.bindService(intent, mCon, Context.BIND_AUTO_CREATE);
}

<强> MyServiceAccessClass.java

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="MYACTIVITY"
    android:id="@+id/activity_MY"
    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">

   <Button
       android:text="Button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_centerVertical="true"
       android:id="@+id/button"/>

   <ProgressBar
       style="@android:style/Widget.ProgressBar.Horizontal"
       android:layout_width="match_parent"
       android:id="@+id/progressBar2"
       android:layout_above="@+id/button"
       android:layout_height="50dp"
       android:max="100"
       android:progress="50" />
</RelativeLayout>

找到这个荒谬的问题花了我很多时间。我希望每个人都喜欢这个解决方案,并为重新绑定服务解决了很多问题。