从服务更新Android活动UI

121

看我的答案。使用监听器轻松定义接口在类之间通信。http://stackoverflow.com/questions/14660671/how-to-notify-an-activity-when-globalvariables-are-changed/14660808#14660808 - Simon
8个回答

246
请见下文我的原始答案-该模式一直效果很好,但最近我开始使用另一种方法来处理服务/活动之间的通信:
  • 使用绑定服务,使Activity可以直接获取到Service的引用,从而可以直接调用它,而不是使用Intents。
  • 使用RxJava执行异步操作。
  • 如果Service需要在没有运行Activity时继续后台操作,还可以从Application类中启动Service,以便在取消绑定时不被停止。
与startService()/LocalBroadcast技术相比,我发现这种方法的优点是:
  • 不需要数据对象实现Parcelable-这对我来说特别重要,因为我现在正在使用RoboVM在Android和iOS之间共享代码
  • RxJava提供了预设(并跨平台)的调度,以及顺序异步操作的简单组合。
  • 这应该比使用LocalBroadcast更有效率,尽管使用RxJava的开销可能会超过它。
以下是一些示例代码。首先是Service部分:
public class AndroidBmService extends Service implements BmService {

    private static final int PRESSURE_RATE = 500000;   // microseconds between pressure updates
    private SensorManager sensorManager;
    private SensorEventListener pressureListener;
    private ObservableEmitter<Float> pressureObserver;
    private Observable<Float> pressureObservable;

    public class LocalBinder extends Binder {
        public AndroidBmService getService() {
            return AndroidBmService.this;
        }
    }

    private IBinder binder = new LocalBinder();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        logMsg("Service bound");
        return binder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_NOT_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
        if(pressureSensor != null)
            sensorManager.registerListener(pressureListener = new SensorEventListener() {
                @Override
                public void onSensorChanged(SensorEvent event) {
                    if(pressureObserver != null) {
                        float lastPressure = event.values[0];
                        float lastPressureAltitude = (float)((1 - Math.pow(lastPressure / 1013.25, 0.190284)) * 145366.45);
                        pressureObserver.onNext(lastPressureAltitude);
                    }
                }

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {

                }
            }, pressureSensor, PRESSURE_RATE);
    }

    @Override
    public Observable<Float> observePressure() {
        if(pressureObservable == null) {
            pressureObservable = Observable.create(emitter -> pressureObserver = emitter);
            pressureObservable = pressureObservable.share();
        }
         return pressureObservable;
    }

    @Override
    public void onDestroy() {
        if(pressureListener != null)
            sensorManager.unregisterListener(pressureListener);
    }
} 

一个与服务绑定并接收气压高度更新的Activity:

public class TestActivity extends AppCompatActivity {

    private ContentTestBinding binding;
    private ServiceConnection serviceConnection;
    private AndroidBmService service;
    private Disposable disposable;

    @Override
    protected void onDestroy() {
        if(disposable != null)
            disposable.dispose();
        unbindService(serviceConnection);
        super.onDestroy();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.content_test);
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                logMsg("BlueMAX service bound");
                service = ((AndroidBmService.LocalBinder)iBinder).getService();
                disposable = service.observePressure()
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(altitude ->
                        binding.altitude.setText(
                            String.format(Locale.US,
                                "Pressure Altitude %d feet",
                                altitude.intValue())));
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                logMsg("Service disconnected");
            }
        };
        bindService(new Intent(
            this, AndroidBmService.class),
            serviceConnection, BIND_AUTO_CREATE);
    }
}

这个活动的布局是:
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.controlj.mfgtest.TestActivity">

        <TextView
            tools:text="Pressure"
            android:id="@+id/altitude"
            android:gravity="center_horizontal"
            android:layout_gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>

如果服务需要在后台运行且没有绑定的Activity,也可以从Application类中使用OnCreate()Context#startService()启动。


我的原始答案(来自2013年):

在您的服务中:(以下示例中使用COPA作为服务)。

使用LocalBroadcastManager。 在服务的onCreate中设置广播:

broadcaster = LocalBroadcastManager.getInstance(this);

当您想要通知UI某些事情时:

static final public String COPA_RESULT = "com.controlj.copame.backend.COPAService.REQUEST_PROCESSED";

static final public String COPA_MESSAGE = "com.controlj.copame.backend.COPAService.COPA_MSG";

public void sendResult(String message) {
    Intent intent = new Intent(COPA_RESULT);
    if(message != null)
        intent.putExtra(COPA_MESSAGE, message);
    broadcaster.sendBroadcast(intent);
}

在您的 Activity 中:
在 onCreate 中创建一个监听器:
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    super.setContentView(R.layout.copa);
    receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String s = intent.getStringExtra(COPAService.COPA_MESSAGE);
            // do something here.
        }
    };
}

并在 onStart 中注册它:

@Override
protected void onStart() {
    super.onStart();
    LocalBroadcastManager.getInstance(this).registerReceiver((receiver), 
        new IntentFilter(COPAService.COPA_RESULT)
    );
}

@Override
protected void onStop() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
    super.onStop();
}

4
是的,onStart()和onStop()是Activity生命周期的一部分 - 请参阅Activity Lifecycle - Clyde
8
小注释:您漏掉了COPA_MESSAGE的定义。 - Lior
1
那些询问COPA_RESULT的人,它只是一个用户生成的静态变量。“COPA”是他的服务名称,所以你可以完全用你自己的替换它。在我的情况下,它是static final public String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED"; - TheOnlyAnil
1
如果用户从Activity导航离开,并在服务完成后再返回,UI将不会更新。最好的处理方式是什么? - SavageKing
1
@SavageKing,你需要在onStart()onResume()方法中向服务发送适当的请求。通常情况下,如果Activity要求Service执行某些操作,但在接收结果之前退出了,则可以合理地假设不再需要结果。同样,在启动Activity时,它应该假定Service没有正在处理的未完成请求。 - Clyde
显示剩余18条评论

38

对我来说最简单的解决方案是发送广播,在Activity的oncreate中我注册并定义了广播,如下所示(updateUIReciver被定义为一个类实例):

 IntentFilter filter = new IntentFilter();

 filter.addAction("com.hello.action"); 

 updateUIReciver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {
                //UI update here

            }
        };
 registerReceiver(updateUIReciver,filter);

然后你可以像这样从服务中发送意图:

Intent local = new Intent();

local.setAction("com.hello.action");

this.sendBroadcast(local);

不要忘记在Activity onDestroy()方法中取消注册该监听器:

unregisterReceiver(updateUIReciver);

1
这是一个更好的解决方案,但如果它在应用程序内部使用,最好使用LocalBroadcastManager,这将更有效。 - Psypher
2
在服务中执行 this.sendBroadcast(local); 后的下一步是什么? - angel
@angel,没有下一步了,在意图的额外信息中添加您想要的UI更新即可。 - Eran Katsav

13

我会使用绑定服务来完成这个任务,并通过在我的活动中实现一个监听器与其通信。所以,如果您的应用程序实现了myServiceListener,您可以在绑定服务后将其注册为监听器,在绑定服务后从您的绑定服务中调用listener.onUpdateUI,然后在那里更新您的UI!


注意不要泄漏对您的活动的引用。因为在旋转时,您的活动可能会被销毁并重新创建。 - Eric
嗨,请查看这个链接。我分享了一个示例代码。我把链接放在这里,假设将来有人可能会觉得有帮助。http://myownandroid.blogspot.in/2012/08/android-bind-service-with-messenger-hi.html - jrh
这是一个可行的解决方案,但在我的情况下,我真的需要该服务保持运行,即使所有活动都取消绑定(例如用户关闭应用程序,操作系统过早终止),我别无选择,只能使用广播接收器。 - Neon Warge

9

我建议您查看Otto,这是一个专门为Android定制的事件总线。您的Activity/UI可以监听来自Service发布在Bus上的事件,并将自己与后端解耦。


5
Callback from service to activity to update UI.
ResultReceiver receiver = new ResultReceiver(new Handler()) {
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        //process results or update UI
    }
}

Intent instructionServiceIntent = new Intent(context, InstructionService.class);
instructionServiceIntent.putExtra("receiver", receiver);
context.startService(instructionServiceIntent);

5

Clyde的解决方案是可行的,但它是一种广播方式,我相信直接调用方法将会更有效率。我可能错了,但我认为广播更适合应用程序间通信。

我假设你已经知道如何将服务与活动绑定。 我做的处理这种问题的代码类似于以下代码:

class MyService extends Service {
    MyFragment mMyFragment = null;
    MyFragment mMyOtherFragment = null;

    private void networkLoop() {
        ...

        //received new data for list.
        if(myFragment != null)
            myFragment.updateList();
        }

        ...

        //received new data for textView
        if(myFragment !=null)
            myFragment.updateText();

        ...

        //received new data for textView
        if(myOtherFragment !=null)
            myOtherFragment.updateSomething();

        ...
    }
}


class MyFragment extends Fragment {

    public void onResume() {
        super.onResume()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyFragment=this;
    }

    public void onPause() {
        super.onPause()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyFragment=null;
    }

    public void updateList() {
        runOnUiThread(new Runnable() {
            public void run() {
                //Update the list.
            }
        });
    }

    public void updateText() {
       //as above
    }
}

class MyOtherFragment extends Fragment {
             public void onResume() {
        super.onResume()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyOtherFragment=this;
    }

    public void onPause() {
        super.onPause()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyOtherFragment=null;
    }

    public void updateSomething() {//etc... }
}

我省略了线程安全的部分,这是必要的。在检查、使用或更改服务上的片段引用时,请务必使用锁或类似的东西。


8
LocalBroadcastManager旨在用于应用程序内部通信,因此非常高效。当您的服务客户端数量有限且服务不需要独立运行时,绑定服务方法是可行的。本地广播方法允许服务与其客户端更有效地解耦,并使线程安全不再成为问题。 - Clyde

1
我的解决方案可能不是最简洁的,但它应该可以没有问题地工作。 逻辑很简单,只需创建一个静态变量来存储您在Service上的数据,并在Activity上每秒更新一次您的视图。
假设您在Service上有一个String要发送到Activity上的TextView,应该像这样:
您的服务:
public class TestService extends Service {
    public static String myString = "";
    // Do some stuff with myString

您的活动:

public class TestActivity extends Activity {
    TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        tv = new TextView(this);
        setContentView(tv);
        update();
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    while (!isInterrupted()) {
                        Thread.sleep(1000);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                update();
                            }
                        });
                    }
                } catch (InterruptedException ignored) {}
            }
        };
        t.start();
        startService(new Intent(this, TestService.class));
    }
    private void update() {
        // update your interface here
        tv.setText(TestService.myString);
    }
}

3
不要在Service或任何Activity中使用静态变量。 - Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - 你能详细说明一下吗? - Desolator
2
静态成员是活动中内存泄漏的源头(有很多相关文章),而将它们保留在服务中会使情况变得更糟。在 OP 的情况下,广播接收器是更合适的选择,或者将其保留在持久性存储中也是一种解决方案。 - Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - 假如我有一个类(不是服务类,只是用于填充数据的某个模型类),其中有一个静态哈希映射,每分钟会重新填充一些数据(来自 API)。这被认为是内存泄漏吗? - Desolator
Android的环境下,如果是列表,尝试创建一个适配器或使用sqllite/realm数据库来持久化数据。 - Murtaza Khursheed Hussain
移动设备为应用程序分配了有限的内存。如果这些类拥有未回收的位图,则会消耗内存。如果您的类是对某个需要进行垃圾回收的对象的硬引用,由于您具有静态引用并且它没有被清除,因此它将不会被垃圾回收。 - Murtaza Khursheed Hussain

1
您可以使用Android Jetpack的LiveData
根据文档:
您可以使用单例模式扩展LiveData对象,以包装系统服务,以便在应用程序中共享它们。LiveData对象仅连接一次系统服务,然后任何需要资源的观察者都可以观察LiveData对象。有关更多信息,请参见Extend LiveData。
以下是我为Service和Activity以及Service和Fragment之间通信所做的工作示例。
在此示例中,我有:
- 一个扩展LiveData的SyncLogLiveData类,其中包含SpannableStringBuilder。 - 一个名为SyncService的服务 - 一个名为SyncFragment的片段

这个Fragment "观察" LiveData(即SyncLogLiveData)并在LiveData更改时执行动作。
LiveData由Service更新。
我也可以以同样的方式从Fragment更新LiveData,但这里不展示。

SyncLogLiveData

public class SyncLogLiveData extends LiveData<SpannableStringBuilder> {
    private static SyncLogLiveData sInstance;
    private final static SpannableStringBuilder log = new SpannableStringBuilder("");

    @MainThread
    public static SyncLogLiveData get() {
        if (sInstance == null) {
            sInstance = new SyncLogLiveData();
        }
        return sInstance;
    }

    private SyncLogLiveData() {
    }

    public void appendLog(String text) {
        log.append(text);
        postValue(log);
    }

    public void appendLog(Spanned text) {
        log.append(text);
        postValue(log);
    }
}

在类SyncService

这行代码将会更新LiveData的内容。

SyncLogLiveData.get().appendLog(message);

您也可以直接使用 LiveDatasetValue(...)postValue(...) 方法。
SyncLogLiveData.get().setValue(message);

类 SyncFragment

public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {

    //...

    // Create the observer which updates the UI.
    final Observer<SpannableStringBuilder> ETAObserver = new Observer<SpannableStringBuilder>() {
        @Override
        public void onChanged(@Nullable final SpannableStringBuilder spannableLog) {
            // Update the UI, in this case, a TextView.
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    textViewLog.setText(spannableLog);
                }
            });
        }
    };
    // Observe the LiveData, passing in this activity/fragment as the LifecycleOwner and the observer.
    SyncLogLiveData.get().observe(getViewLifecycleOwner(), ETAObserver);

    //...
}

在活动中,它的工作方式相同,但对于.observe(...),您可以改用此方法

SyncLogLiveData.get().observe(this, ETAObserver);

您可以在代码中的任何时候以这种方式获取LiveData的当前值。
SyncLogLiveData.get().getValue();

希望这能对某人有所帮助。在这个答案中还没有提到LiveData。


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