2017-01-02 88 views
0

我有一個更新UI的非常奇怪的問題。我有一個前臺開始有限的服務,我的主要過程在後臺。當我啓動應用程序時,我喜歡檢查服務是否已在運行並更改切換按鈕的狀態。對於這個問題,當在OnResume()中啓動應用程序時,我將綁定到我的啓動服務,並且服務將值發送回顯示服務運行狀態的應用程序,並根據此值更新UI。但問題是在這種情況下UI不會更新。Android異步用戶界面從服務更新失敗

由於此錯誤顯示在一個非常複雜的情況下,我寫了一個示例代碼來重現此問題。這裏是這些代碼(對於錯誤的名字抱歉,錯過了很多錯誤檢查,我已經很快寫了這些代碼來重現問題)。我已經討論過每個代碼作爲概述。

activity_main佈局:

<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()正在使用綁定和解除綁定服務。 mRecvResultReceiver,它在綁定時發送到服務並用於獲取狀態。當綁定後收到狀態時,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爲true時,切換按鈕的setChecked()將與true一起調用(直到現在這是正確的),但UI將不會更新以顯示正確的文本和狀態。有趣的是,如果您爲此切換按鈕運行isChecked,它將返回true,但UI會顯示其他內容。

任何想法爲什麼發生這種情況?對不起,很多代碼,這個問題發生在這種複雜的情況。

更新

我注意到的東西,我應該不在話下。如果我在setCheck之後使用isChecked,我會得到true,這是正確的。但是如果我稍後再次使用isChecked(例如在另一個按鈕事件處理程序中),它將返回false,而我還沒有再調用setChecked。我認爲這種情況與我的問題有關,但我不知道這是怎麼發生的。

此外,我認爲這個問題與更新用戶界面時有關,當你在綁定過程中的服務。因爲如果我不在綁定過程中嘗試使用相同的ResultReceiver更新應用主界面,則一切正常。

回答

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

這是爲了使基於簡單的綁定重新綁定服務,以服務您在網上找到的常用方法。這已通過在onUnbind()中返回true並使用onRebind()完成。但是這種做法是完全錯誤的

爲什麼?由於android中的奇怪設計。在Service OnRebind(),有一個小的18字的評論:

注意,在這一點上 被列入的意圖,任何演員都不會在這裏看到。

現在這意味着什麼?這意味着在重新綁定中將不會提供帶有ResultReceiver的額外數據,這意味着重新綁定後結果不會被髮回。但是由於不明原因,這段代碼沒有發生任何異常,甚至在調試時看到應用程序返回的結果,所以爲什麼這段代碼不起作用,這是非常含糊不清的。

現在有什麼解決方案?當您使用bindService()意圖綁定到服務時,千萬不要發送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); 
     } 
    } 
} 

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

找到這個荒謬的問題花了我很多時間。我希望每個人都喜歡這個解決方案,併爲重新綁定服務解決了很多問題。

0

可能需要在按鈕View上調用View.requestLayout()View.forceLayout()來刷新按鈕狀態。

+0

我試着調用'invalidate()',但沒有奏效。我也會嘗試這兩個。 – Afshin

+0

'requestLayout()'和'forceLayout()'都不能工作。 – Afshin

+0

你可以嘗試在'mCallback.updateUi(resultData.getBoolean(「status」))周圍放置'runOnUiThread()';'只是一個想法... – beeb