fred-ye / summary

my blog
43 stars 9 forks source link

[Android]一次关于SingleTask的填坑 #46

Open fred-ye opened 9 years ago

fred-ye commented 9 years ago

一次关于SingleTask的填坑

这个milestone客户那边做了一个功能,在做这个功能的时候,那边的开发把我们app中的activity的launchmode给改了。之前我们都是采用standard模式的,整个app中维持着一个activity,每次跳屏前会将当前的activity finish掉。下次再进到这个屏,重新执行onCreate,创建这个activity。同时我们有很多初始化UI和数据的代码写在onCreate方法中。现在因为修改了activity的launchmode,导致了每次进到一个屏时,其activity的onCreate方法不一定执行。从而引起了一堆的UI和数据不一致的问题。

在谈到这些问题之前,我们先来自一下activity的launchmode。都知道activity的启动模式有四种,Google推荐我们尽量不要去修改activity的launchmode,对于大多数的应用standard模式就可以适用。这里我们只讲singleTask这一个launchmode.

例子

假设有四个activity。如果采用默认的launchmode, 依次启动 A->B->C->D。栈中将会保留这四个Activity,如果再由D启动A,A启动B, 则栈中顺序会是A->B->C->D->A->B。 这个很好理解。

如果我把B的launchmode 改为singleTask. 依次启动A->B->C->D。栈中仍会保留这四个activity,如果再由D启动A,会发现栈中有5个activity.

采用adb shell dump sys activity命令,我们可以查看当前activity和栈的情况:

Running activities (most recent first): TaskRecord{423f84e0 #81 A=com.example.fredye.myapplication U=0 sz=5} Run #5: ActivityRecord{42635c08 u0 com.example.fredye.myapplication/.lanuchmode.ActivityA t81} Run #4: ActivityRecord{42930618 u0 com.example.fredye.myapplication/.lanuchmode.ActivityD t81} Run #3: ActivityRecord{430e61d8 u0 com.example.fredye.myapplication/.lanuchmode.ActivityC t81} Run #2: ActivityRecord{42ee02c0 u0 com.example.fredye.myapplication/.lanuchmode.ActivityB t81} Run #1: ActivityRecord{427133c8 u0 com.example.fredye.myapplication/.lanuchmode.ActivityA t81}

如果再由A启动B, 此时有意思的事便发生了。同样,我们打印出当前Activity和栈的情况:

Running activities (most recent first): TaskRecord{423f84e0 #81 A=com.example.fredye.myapplication U=0 sz=2} Run #2: ActivityRecord{42ee02c0 u0 com.example.fredye.myapplication/.lanuchmode.ActivityB t81} Run #1: ActivityRecord{427133c8 u0 com.example.fredye.myapplication/.lanuchmode.ActivityA t81}

会发现栈中只有两个activity。我们在ActivityB上指定了launchmode是singleTask,于是在第二次启动ActivityB时,发现当前task里面已经有ActivityB了,便将ActivityB移动栈顶,同时销毁ActivityC和ActivityD。在这个过程中ActivityB的onCreate方法是不会执行的。只会执行其onNewIntent方法。[注意此时所有的Activity都还是在一个task中]

问题来了

1. UI 问题

之前我们的App中每次在Activity跳转的时候都会去finish当前的Activity,确保应用中只存在一个Activity。因此有部份UI的初始化操作是直接放到onCreate方法中做的。singleTask的引入,导致了Activity的onCreate方法不一定执行,因此UI初始化的问题便出现了。

2. 数据不一致问题

如果项目中存在这种代码,那就要小心了

  public class ActivityA extends Activity {
      private String data;
      protected void onCreate(Bundle saveInstanceState) {
          data = (MyApplication)getApplication().getData();
      }
  }

对于属性data, 如果在应用中的其它地方对它赋值,会导致在ActivityA中使用的data不是最新的。同样,Root Cause是因为onCreate方法不会每次都执行。

如果碰到采用intent传值也要小心。比如在A activity中有这么一段代码:

Intent intent = new Intent(AActivity.this, BActivity.class);
intent.putExtra("date", new Date().toLocaleString());
startActivity(intent);

由A activity去启动BActivity时用 intent传了一个参数,如果BActivity被设置成了singleTask,由上面的分析,我们知道BActivity中的onCreate方法不一定会执行,因此我们没有在onCreate方法中取intent中的数据,而是从onResume方法里面去取。看起来好像没有问题,但是后来发现,当我们取值的时候,我们发现数据一直都没有更新。此时我们需要进行另外一个操作,重写BActivity中的onNewIntent方法,代码如下:

    //如果当前Activity的启动模式是singleTask, 重定onNewIntent方法可以保证接收到的其它Activity传过来的值是最新的。
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }

3. 用户行为的问题

在上面例子中,由于ActivityB设置了launchmode为singleTask, 从而导致了ActivityC和ActivityD被销毁的问题。此时,如果用户按Back键,会发现回退时屏幕出现的顺序和启动的顺序没有匹配上,上面例子中是不能返回到ActivityC屏和ActivityD屏。

关于1, 2 两个问题,当了解了singleTask的引入导致activity的生命周期回调方法的没有按我们预期的执行这个问题,解决起来也就好弄了。关于第3点,用户行为的问题,这个就暂时没有找到比较好的方法去弄了。

官方对singleTask的定义

The system creates a new task and instantiates the activity at the root of the new task. However, if an instance of the activity already exists in a separate task, the system routes the intent to the existing instance through a call to its onNewIntent() method, rather than creating a new instance. Only one instance of the activity can exist at a time. Although the activity starts in a new task, the Back button still returns the user to the previous activity.

实际上我们在使用时发现,对于设置了"singleTask"启动模式的Activity,它在启动的时候,会先检测系统中属性值affinity等于它的属性值taskAffinity的task是否存在;如果存在这样的task,它就会在这个task中启动,否则就开启一个新的task。因此,如果我们想要设置了"singleTask"启动模式的Activity在新的task中启动,就要为它设置一个独立的taskAffinity属性值,taskAffinity默认情况下是应用的包名。下面的代码中,应用程序的包名是com.fred.testactivity,我们将BActivity的启动模式设置成singleTask, 同时设置其taskAffinity属性为com.fred.testactivity.BActivity 。当BActivity启动时,会发现多了一个task 。代码如下:

<activity
    android:name=".BActivity"
    android:label="@string/title_activity_b"
    android:launchMode="singleTask"
    android:taskAffinity="com.fred.testactivity.BActivity">
</activity>

补充:android中的task和stack

首先我们需要明白任何一个Android app是由多个Activity组成的. 每一个Activity可以启动自己app应用中的activity或者其它app应用中的Activity.每一个Activity可以声明它所要处理的意图。当声明处理同一个意图的Activity有多个时,系统就会弹出一个窗口让用户进行选择。

一个task便是一系列Activity的集合。当用户在APP A中发出了一个发送邮件的意图,系统打开了邮件客户端,邮件发送完后,又回到了APP A, 给用户的感觉发送邮件就是在APP A中完成的一样。这个便是由于发送邮件的Activity, 和APP A中的activity在同一个Task中。Task中的activity是交给一个stack来管理的,stack中activity顺序是按照它们启动的先后顺序。

桌面是大多数task启动的位置,当用户点击了一个app 的 icon时,这个app 的task便由后台转到前台。如果当前的app 没有对应的task, 便创建一个新的task,同时启动它的”main” activity,同时将其放入stack中。如果当前的activity启动另外一个activity, 新的activity便会放到栈顶,之前的activity会被stop, 系统会保留它的状态。当用户点击Back键时,当前activty会出栈(会被destory, 它的onDestory方法会被调用),它的前一个activity会被Resume。如果用户不断的按Back键,该Task中的Activity将会一个接一个的出栈,当这个栈中的activity都出栈了,这个task也就不存在了。

Google的提醒

我们可以通过设置launchmode和affinity来管理Task, 但Google给了我们一个提醒 --大多数应用不要改变Activity的启动模式, 原文如下

Caution: Most applications should not interrupt the default behavior for activities and tasks. If you determine that it's necessary for your activity to modify the default behaviors, use caution and be sure to test the usability of the activity during launch and when navigating back to it from other activities and tasks with the Back button. Be sure to test for navigation behaviors that might conflict with the user's expected behavior.

jackycaojiaqi commented 7 years ago

学到了

xiao-mian-yang commented 7 years ago

学到了

yljnet commented 6 years ago

学习到

luckycloves commented 6 years ago

get it