从服务更新Android异步UI失败

从服务更新Android异步UI失败,android,android-layout,Android,Android Layout,我在更新UI时遇到了一个非常奇怪的问题。我有一个前台启动的有界服务,我的主要进程在后台。当我启动应用程序时,我喜欢检查服务是否已经在运行,并更改切换按钮的状态。对于这个问题,在OnResume()中启动应用程序时,我绑定到我启动的服务,服务将一个值发送回我的应用程序,该值显示服务的运行状态,我根据该值更新UI。但问题是,在这种情况下,UI不会更新 因为这个bug是在非常复杂的情况下出现的,所以我编写了一个示例代码来重现这个问题。下面是这些代码(很抱歉名称不好,并且错过了很多错误检查,我很快编写了

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

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

活动\u主布局:

<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"/>
MyServiceAccessClass.java

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);
        }
    }
}
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);
}
此类用于访问服务
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

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);
        }
    }
}
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);
}
这是测试应用程序的主要类。启动按钮启动服务。这个类在
OnResume()
中绑定,在
OnPause()中解除绑定。如果应用程序在服务已经运行时运行,并且其
mStatus
true
,则将使用
true
值调用
updateUi
,并设置切换按钮的状态

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
为true时,将使用
true
调用切换按钮的
setChecked()。有趣的是,如果您运行此切换按钮的
isChecked
,它将返回
true
,但UI显示其他内容

知道为什么会这样吗?很抱歉出现了很多代码,出现的这个问题就是这种复杂的情况

更新

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


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

可能需要调用按钮视图上的
视图。requestLayout()
视图。forceLayout()
,以刷新按钮状态。

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

通过
ResultReceiver
,从服务返回结果在某种程度上是显而易见的。但internet上的大多数示例都没有显示服务重新绑定,而且我从未发现在重新绑定服务后返回结果

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

@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中找到的服务的简单绑定。这是通过在
onUnbind()
中返回
true
并使用
onRebind()
完成的。但这种方法是完全错误的

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

请注意,在该点上包含的任何附加内容都是出于意图 在这里看不到

这意味着什么?这意味着在重新绑定时,携带
ResultReceiver
的额外数据将不可用,这反过来意味着在重新绑定后结果将不会返回。但由于未知的原因,这段代码并没有出现任何异常,你们甚至在调试时可以在应用程序中看到结果,所以这段代码不工作的原因非常模糊

现在解决办法是什么?使用
bindService()
intent绑定到服务时,切勿发送
ResultReceiver
。尽管这对于非重新绑定服务是正确的,但我强烈建议避免它。调用服务连接上的
onServiceConnected
时,通过单独的消息将
ResultReceiver
发送到服务,然后一切都像小菜一碟一样工作。以下是我对代码的修改:

MyTestService.java

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);
        }
    }
}
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);
}

发现这个可笑的问题花了我很多时间。我希望每个人都喜欢这个解决方案,并解决了许多重新绑定服务的问题。

我尝试调用
invalidate()
,但没有成功。我也会尝试这两个。无论是
requestLayout()
还是
forceLayout()
都不起作用。你能试着把
runOnUiThread()
放在
mCallback.updateUi(resultData.getBoolean(“status”)只是一个想法…不起作用。顺便说一句,我对主要问题做了一个小的更新。我认为您的绑定过程问题是因为这一行:
intent.putExtra(“myrecvextra”,mRecv)在这里,您将获得此对象的副本(因为parcelable)。我认为回调也将被复制,因此您将更新副本的togglebutton。也许此链接可以帮助您: