热门搜索 :
考研考公
您的当前位置:首页正文

Activity重建过程中复用数据的四种方式

来源:东饰资讯网
Paste_Image.png

作为一名Android开发者,你一定经常在思考如下场景:如果在一个Activity中我们发送一个请求,然后请求返回之前,由于某种原因(比如:旋转屏幕或者更改语言),导致Activity重新创建,此时如何优雅地复用之前的请求,同时也不导致Activity泄漏?

对于Android中这个典型的场景,我列举了如下比较常见的解决方案,当然每个方案都有其优缺点,文末会推荐其中两种作者认为较好的实现方式。

模拟场景

网络请求

一个简单的模拟网络请求Task,每隔200ms更新一下进度,直到操作完成。

public class NetWorkTask extends Thread {
    private volatile ProgressUpdateLinster progressUpdateLinster;
    private Handler handler = new Handler(Looper.getMainLooper());

    public NetWorkTask(ProgressUpdateLinster progressUpdateLinster) {
        this.progressUpdateLinster = progressUpdateLinster;
    }

    private int progress = 0;
    @Override
    public void run() {
        while (progress <= 100) {
            if(progressUpdateLinster != null) {
                handler.post(new Runnable() {
                 @Override
                    public void run() {
                        progressUpdateLinster.updateProgress(progress);
                    }
                });
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                return;
            }
            progress += 2;
        }
    }

    public interface ProgressUpdateLinster {
        void updateProgress(int progress);
    }

    public void cacel() {
        interrupt();
    }
    public void setProgressUpdateLinster(ProgressUpdateLinster progressUpdateLinster) {   
       this.progressUpdateLinster = progressUpdateLinster;
    }
}

简单的布局,包括一个进度条和一个展示进度的文案

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <ProgressBar
        android:layout_width="200dp"
        android:layout_height="20dp"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:id="@+id/progressbar"
        android:max="100"
        />

    <TextView
     android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/progressbar"
        android:layout_marginLeft="10dp"
        android:textColor="#ff0051"
        android:id="@+id/tv_progroess"
        />
</RelativeLayout>

一个非常简单的Activity

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private ProgressBar progressBar;
    private TextView textView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar = (ProgressBar) findViewById(R.id.progressbar);
        textView = (TextView) findViewById(R.id.tv_progroess);

        new NetWorkTask(new NetWorkTask.ProgressUpdateLinster() {
            @Override
            public void updateProgress(int progress) {
               progressBar.setProgress(progress);
               textView.setText(progress+"%");
               Log.d(TAG,MainActivity.this.toString());
            }
        }).start();
}

在修改手机的语言环境时,Activity会被重建,存在两个问题:

  1. 重建之前旧的Activity会被Thread持有,(活的Thread是一个GC Root,是不能回收的),导致Activity内存泄漏。
  2. 网络请求会被发送两次

我们可以通过改一下手机语言,导致Activity重建看一下效果:

default02.gif
可以看到Activity重建之后,NetWorkTask会重新执行(进度从0开始),而且旧的Activity并不会马上销毁(可以看log)。注意Activity的标签,在重建之后自动由“Label”变成了“标签”,我们对标签配置了中文和英文两种语言。

为了解决上面两个问题,我们有如下方案:

一 不让Activity重建

既然系统会让Activity进行重建,那我们是不是可以拦截这些变化,不让Activity重建呢?自然而然就想到了配置Activity的android:configChanges,比如在我们这个案例里我们配置android:configChanges="locale|layoutDirection",然后把语言环境从US切换到天朝,效果如下。

config02.gif

注意两个细节:1、由于Activity并没有重建,所以Activity的标签栏在语言环境改变之后并没有自动如愿地改成我们想要的“标签”。我们需要在onConfigurationChanged中手动更新设置一下Activity的标签字符串。2、我们拦截了两种设备配置变化,locale|layoutDirection,为什么要拦截layoutDirection呢?因为语言环境的改变也会触发layoutDirection条件变化...此外,比如我们常用的屏幕翻转也会触发屏幕大小配置变化,如果我们漏掉其中任何一个属性配置,那么Activity还是会被重建。

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    getSupportActionBar().setTitle(R.string.lab_str);
}

优点

简单,只需要在manifest文件中增加一些属性即可

缺点

系统在设备配置变化时,默认需要重建的原因是可以为新的属性适配相应的资源,在我们的列子中,只是简单的适配了一下ActionBar的title,但是如果我们当前页面需要适配各种布局,像素大小就悲剧了,这些适配都需要我们自己手动在onConfigurationChanged里面进行;同时,Activity重建的相关配置属性都需要在manifest中配置,配置越多,onConfigurationChanged中的处理逻辑就越复杂。

二 onRetainNonConfigurationInstance

在Activity由于设备配置导致重建,系统会调用onRetainNonConfigurationInstance方法,我们可以返回任何类型的对象进行保存,在Activity被重建之后,我们可以调用getLastNonConfigurationInstance来获取保持的对象。我们修改Activity的代码如下,注意,在FragmentActivity中把上面两个方法包装成了onRetainCustomNonConfigurationInstance和getLastCustomNonConfigurationInstance,如果你是直接继承自系统Activity,使用上面所说的方法。

private ProgressBar progressBar;
private TextView textView;
private static final String TAG = "MainActivity";

NetWorkTask netWorkTask = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    progressBar = (ProgressBar) findViewById(R.id.progressbar);
    textView = (TextView) findViewById(R.id.tv_progroess);

    if(getLastCustomNonConfigurationInstance() != null
            && getLastCustomNonConfigurationInstance() instanceof NetWorkTask) {
         = (NetWorkTask) getLastCustomNonConfigurationInstance();
        
    }else {
         = new NetWorkTask();
        netWorkTask.setProgressUpdateLinster(linster);
        netWorkTask.start();
    }

}

private NetWorkTask.ProgressUpdateLinster linster = new NetWorkTask.ProgressUpdateLinster() {
    @Override
    public void updateProgress(int progress) {
        progressBar.setProgress(progress);
        textView.setText(progress+"%");
        Log.d(TAG,MainActivity.this.toString());
    }
};

@Override
public Object onRetainCustomNonConfigurationInstance() {
    return netWorkTask;
}

效果如下,界面进行了重建,同时network也只被执行了一次,同时由于我们重新set了linster,network所在的Thread不再持有原来Activity的引用,所以不会导致Activity泄漏。

retainsave01.gif

优点

简单。。。

缺点

  1. 只能处理由于设备配置变化导致的Activity重建的情况
  2. Activity需要参与状态保存和恢复

三 Retain Fragment

在Android3.0之后,官方建议我们使用Retain Fragment来处理这种场景。增加一个work Fragment:

public class WorkFragment extends Fragment {


NetWorkTask netWorkTask = null;

/**
 * 重建之后这里的Context会自动替换成新的Activity
 * @param context
 */
@Override
public void onAttach(Context context) {
    super.onAttach(context);
    //第一次启动的时候,这里network还没有初始化
    //Activity重建之后,更新回调
    if(netWorkTask != null) {
        netWorkTask.setProgressUpdateLinster((NetWorkTask.ProgressUpdateLinster) context);
    }
}

@Override
public void onDetach() {
    super.onDetach();
    netWorkTask.setProgressUpdateLinster(null);
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //设置为retain instance Fragment
    setRetainInstance(true);
    netWorkTask = new NetWorkTask();
    netWorkTask.setProgressUpdateLinster((NetWorkTask.ProgressUpdateLinster) getActivity());
    netWorkTask.start();
}
}   

修改Activity代码:

public class MainActivity extends AppCompatActivity implements NetWorkTask.ProgressUpdateLinster {

private ProgressBar progressBar;
private TextView textView;
private static final String TAG = "MainActivity";
private static final String TAG_TASK_FRAGMENT = "work";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    progressBar = (ProgressBar) findViewById(R.id.progressbar);
    textView = (TextView) findViewById(R.id.tv_progroess);
    
    //如果已经有了work fragment,那就不需要再新建了
    if(getSupportFragmentManager().findFragmentByTag(TAG_TASK_FRAGMENT) == null) {
        getSupportFragmentManager().beginTransaction().add(new 
    }
}

@Override
public void updateProgress(int progress) {
    progressBar.setProgress(progress);
    textView.setText(progress+"%");
}
}

Retain Fragment的生命周期可以跨越Activity的重建周期,相比较第二种方式,使用Work Fragment可以将各种状态保存和恢复模块到Fragment中,代码组织更加合理,同时它的应用场景不仅仅再针对于由于设备配置变化导致的重建。
以上三种方式都是基于Android自身API实现,其中第三种是官方推荐的方式。

四 EventBus

如果我们使用了EventBus之类的第三方库,我们可以这样做:

  1. 新建一个事件类型

     public class ProgressUpdateEvent {
       public final int progress;
       public ProgressUpdateEvent(int progress) {
         this.progress = progress;
       }
    }
    

2.修改Activity代码

public class MainActivity extends AppCompatActivity  {

private ProgressBar progressBar;
private TextView textView;
private static final String TAG = "MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    progressBar = (ProgressBar) findViewById(R.id.progressbar);
    textView = (TextView) findViewById(R.id.tv_progroess);
    //将当前Activity注册到EventBus
    EventBus.getDefault().register(this);
    //如果是重建的Activity不需要再次新建NetworkTask
    if(savedInstanceState == null) {
        new NetWorkTask(l).start();
    }
}

/**
 * 接受到事件
 * @param event
 */
public void onEventMainThread(ProgressUpdateEvent event) {
    progressBar.setProgress(event.progress);
    textView.setText(event.progress+"%");
}

/**
 * 注意,这里是一个静态内部类,不会持有任何外部类的引用
 */
static NetWorkTask.ProgressUpdateLinster l = new NetWorkTask.ProgressUpdateLinster() {
    @Override
    public void updateProgress(int progress) {
        EventBus.getDefault().post(new ProgressUpdateEvent(progress));
    }
};


@Override
protected void onDestroy() {
    super.onDestroy();
    //注销订阅
    EventBus.getDefault().unregister(this);
}
}

总结

Activity在重建时,有可能导致请求重新发送和Activity内存泄漏,针对这两个问题,本文提出了四种方式来处理这种场景,其中第一种和第二种只能处理由于设备配置变化导致的Activity重建,第三种是官方推荐的一种方式,第四种使用第三方库Eventbus来处理,建议大家尽量使用第三种和第四种处理方式。

Top