深入解析Android系统中应用程序前后台切换的实现要点
作者:by2n
在介绍程序实现之前,我们先看下Android中Activities和Task的基础知识。
我们都知道,一个Activity 可以启动另一个Activity,即使这个Activity是定义在别一个应用程序里的,比如说,想要给用户展示一个地图的信息,现在已经有一个Activity可以做这件事情,那么现在你的Activity需要做的就是将请求信息放进一个Intent对象里,并且将这个Intent对象传递给startActivity(),那么地图就可显示出来了,但用户按下Back键之后,你的Activity又重新出现在屏幕上。
对用户来讲,显示地图的Activity和你的Activity好像在一个应用程序中的,虽然是他们是定义在其他的应用程序中并且运行在那个应有进程中。Android将你的Activity和借用的那个Activity被放进一个Task中以维持用户的体验。那么Task是以栈的形式组织起来一组相互关联的Activity,栈中底部的Activity就是开辟这个Task的,通常是用户在应用程序启动器中选择的Activity。栈的顶部的Activity是当前正在运行的Activity--用户正在交互操作的Activity。
当一个Activity启动另一个Activity时,新启动的Activity被压进栈中,成为正在运行的Activity。旧的Activity仍然在栈中。当用户按下BACK键之后,正在运行的Activity弹出栈,旧的Activity恢复成为运行的Activity。栈中包含对象,因此如果一个任务中开启了同一个Activity子类的的多个对象——例如,多个地图浏览器——则栈对每一个实例都有一个单独的入口。栈中的Activity不会被重新排序,只会被、弹出。Task是一组Activity实例组成的栈,不是在manifest文件里的某个类或是元素,所以无法设定一个Task的属性而不管它的Activity,一个Task的所有属性值是在底部的Activity里设置的,这就需要用于Affinity。关于Affinity这里不再详述,大家可以查询文档。
一个Task里的所有Activity作为一个整体运转。整个Task(整个Activity堆栈)可以被推到前台或被推到后台。假设一个正在运行的Task中有四个Activity——正在运行的Activity下面有三个Activity,这时用户按下HOME键,回到应有程序启动器然后运行新的应用程序(实际上是运行了一个新的Task),那么当前的Task就退到了后台,新开启的应用程序的root Activity此时就显示出来了,一段时间后,用户又回到应用程序器,又重新选择了之前的那个应用程序(先前的那个Task),那么先前的那个Task此时又回到了前台了,当用户按下BACK键时,屏幕不是显示刚刚离开的那个新开启的那个应用程序的Activity,而是被除回到前台的那个Task的栈顶Activity,将这个Task的下一个Activity显示出来。 上述便是Activity和Task一般的行为,但是这个行为的几乎所有方面都是可以修改的。Activity和Task的关系,以及Task中Activity的行为,是受启动该Activity的Intent对象的标识和在manifest文件中的Activity的<Activity>元素的属性共同影响的。
以上是关于Activity和Task的描述。
在开发Android项目时,用户难免会进行程序切换,在切换过程中,程序将进入后台运行,需要用时再通过任务管理器或是重新点击程序或是通过点击信息通知栏中的图标返回原来的界面。这种效果类似于腾讯QQ的效果,打开QQ后显示主界面,在使用其他的程序时,QQ将以图标的形式显示在信息通知栏里,如果再用到QQ时再点击信息通知栏中的图标显示QQ主界面。
先看下本示例实现效果图:
在上图第二个图中,我们点击时将会返回到的原来的Activity中。
当我们的程序进入后台运作时,在我们的模拟器顶部将以图标形式出现,如下图:
对于这种效果一般的做法是在Activity中的onStop()方法中编写相应代码,因为当Activity进入后台时将会调用onStop()方法,我们可以在onStop()方法以Notification形式显示程序图标及信息,其中代码如下所示:
@Override protected void onStop() { // TODO Auto-generated method stub super.onStop(); Log.v("BACKGROUND", "程序进入后台"); showNotification(); }
以上的showNotification()方法就是Notification。
然后点击信息通知栏的Notification后再返回到原来的Activity。
当然,我们也可以捕捉HOME键,在用户按下HOME键时显示Notification, 以下是代码示例:
// 点击HOME键时程序进入后台运行 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // TODO Auto-generated method stub // 按下HOME键 if(keyCode == KeyEvent.KEYCODE_HOME){ // 显示Notification notification = new NotificationExtend(this); notification.showNotification(); moveTaskToBack(true); return true; } return super.onKeyDown(keyCode, event); }
这里的NotificationExtend是对显示Notification的一个封装,类中的代码如下:
package com.test.background; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.graphics.Color; /** * Notification扩展类 * @Description: Notification扩展类 * @File: NotificationExtend.java * @Package com.test.background */ public class NotificationExtend { private Activity context; public NotificationExtend(Activity context) { // TODO Auto-generated constructor stub this.context = context; } // 显示Notification public void showNotification() { // 创建一个NotificationManager的引用 NotificationManager notificationManager = ( NotificationManager)context.getSystemService( android.content.Context.NOTIFICATION_SERVICE); // 定义Notification的各种属性 Notification notification = new Notification( R.drawable.icon,"阅读器", System.currentTimeMillis()); // 将此通知放到通知栏的"Ongoing"即"正在运行"组中 notification.flags |= Notification.FLAG_ONGOING_EVENT; // 表明在点击了通知栏中的"清除通知"后,此通知自动清除。 notification.flags |= Notification.FLAG_AUTO_CANCEL notification.flags |= Notification.FLAG_SHOW_LIGHTS; notification.defaults = Notification.DEFAULT_LIGHTS; notification.ledARGB = Color.BLUE; notification.ledOnMS = 5000; // 设置通知的事件消息 CharSequence contentTitle = "阅读器显示信息"; // 通知栏标题 CharSequence contentText = "推送信息显示,请查看……"; // 通知栏内容 Intent notificationIntent = new Intent(context,context.getClass()); notificationIntent.setAction(Intent.ACTION_MAIN); notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER); PendingIntent contentIntent = PendingIntent.getActivity( context, 0, notificationIntent,PendingIntent.FLAG_UPDATE_CURRENT); notification.setLatestEventInfo( context, contentTitle, contentText, contentIntent); // 把Notification传递给NotificationManager notificationManager.notify(0, notification); } // 取消通知 public void cancelNotification(){ NotificationManager notificationManager = ( NotificationManager) context.getSystemService( android.content.Context.NOTIFICATION_SERVICE); notificationManager.cancel(0); } }
这里需要在配置文件中设置每个Activity以单任务运行,否则,每次返回原Activity时会新增加一个Activity,而不会返回到原Activity。
在使用FLAG_ACTIVITY_NEW_TASK控制标识时也会出现不会返回到原Activity的现象。如果该标识使一个Activity开始了一个新的Task,然后当用户按了HOME键离开这个Activity,在用户按下BACK键时将无法再返回到原Activity。一些应用(例如Notification)总是在一个新的Task里打开Activity,而从来不在自己的Task中打开,所以它们总是将包含FLAG_ACTIVITY_NEW_TASK的Intent传递给startActivity()。所以如果有一个可以被其他的东西以这个控制标志调用的Activity,请注意让应用程序有独立的回到原Activity的方法。 代码如下:
<activity android:name="ShowMessageActivity" android:launchMode="singleTask"></activity>
Android应用前后台切换的判断
Android中没有提供一个应用前后台切换的回调或广播,这个功能只能我们自己来处理。以前遇到这个问题的处理方式是,实现一个BaseActivity,然后让其他所有Activity都继承自它,然后在生命周期函数中做相应的检测。具体检测方法如下:
在Activity的onStart和onStop方法中进行计数,计数变量为count,在onStart中将变量加1,onStop中减1,假设应用有两个Activity,分别为A和B。
情况一、首先启动A,A再启动B:启动A,count=1,A启动B,生命周期的顺序为B.onStart->A.onStop,count的计数仍然为1。
情况二、首先启动A,然后按Home键返回桌面:启动A,count=1,按Home键返回桌面,会执行A.onStop,count的计数变位0。
从上面的两种情况看出,可以通过对count计数为0,来判断应用被从前台切到了后台。同样的,从后台切到前台也是类似的道理。具体实现看后面的代码。
但是如果项目中不是所有的Activity都继承自同一个BaseActivity,就无法实现这个功能了。幸运的是,Android在API 14之后,在Application类中,提供了一个应用生命周期回调的注册方法,用来对应用的生命周期进行集中管理,这个接口叫registerActivityLifecycleCallbacks,可以通过它注册自己的ActivityLifeCycleCallback,每一个Activity的生命周期都会回调到这里的对应方法。其实这个注册方法的本质和我们实现BaseActivity是一样的,只是将生命周期管理移到了Activity本身的实现中。
具体使用方法如下:
public class MyApplication extends Application{ public int count = 0; @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityStopped(Activity activity) { Log.v("viclee", activity + "onActivityStopped"); count--; if (count == 0) { Log.v("viclee", ">>>>>>>>>>>>>>>>>>>切到后台 lifecycle"); } } @Override public void onActivityStarted(Activity activity) { Log.v("viclee", activity + "onActivityStarted"); if (count == 0) { Log.v("viclee", ">>>>>>>>>>>>>>>>>>>切到前台 lifecycle"); } count++; } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { Log.v("viclee", activity + "onActivitySaveInstanceState"); } @Override public void onActivityResumed(Activity activity) { Log.v("viclee", activity + "onActivityResumed"); } @Override public void onActivityPaused(Activity activity) { Log.v("viclee", activity + "onActivityPaused"); } @Override public void onActivityDestroyed(Activity activity) { Log.v("viclee", activity + "onActivityDestroyed"); } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { Log.v("viclee", activity + "onActivityCreated"); } }); } }
除此之外,有没有其他方法可以实现这个功能呢?
当应用切到后台的时候,运行在前台的进程由我们的app变成了桌面app,依据这一点,我们可以实现检测应用前后台切换的功能。在Activity的onStop生命周期中执行检测代码,如果发现当前运行在前台的进程不是我们自己的进程,说明应用切到了后台。
想想为什么要在onStop中检测,而不是onPause?这是由于A启动B时,生命周期的执行顺序如下:A.onPause->B.onCreate->B.onStart->B.onResume->A.onStop,也就是说,在A的onPause方法中,B的生命周期还没有执行,进程没有进入前台,当然是检测不到的。我们把代码移到onPause生命周期中,发现确实没有效果。
具体实现代码如下:
//用来控制应用前后台切换的逻辑 private boolean isCurrentRunningForeground = true; @Override protected void onStart() { super.onStart(); if (!isCurrentRunningForeground) { Log.d(TAG, ">>>>>>>>>>>>>>>>>>>切到前台 activity process"); } } @Override protected void onStop() { super.onStop(); isCurrentRunningForeground = isRunningForeground(); if (!isCurrentRunningForeground) { Log.d(TAG,">>>>>>>>>>>>>>>>>>>切到后台 activity process"); } } public boolean isRunningForeground() { ActivityManager activityManager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> appProcessInfos = activityManager.getRunningAppProcesses(); // 枚举进程 for (ActivityManager.RunningAppProcessInfo appProcessInfo : appProcessInfos) { if (appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { if (appProcessInfo.processName.equals(this.getApplicationInfo().processName)) { Log.d(TAG,"EntryActivity isRunningForeGround"); return true; } } } Log.d(TAG, "EntryActivity isRunningBackGround"); return false; }