技术大杂烩2022

个人技术经验点滴积累


  • 首页

  • 归档

  • 标签

  • 关于

Android图片加载问题分析

发表于 2016-10-31

下图是一个客户端图片加载模块常见的处理流程。本文以UniversalImageLoader为例分析了这一流程,然后分析了Fresco的优势和问题,最终推荐大家使用Glide。

图片加载逻辑

从UniversalImageLoader分析图片加载中需要处理的问题

网络

主要用于下载网络图片,在UIL中是将图片地址变为InputStream。UIL支持多种类型来源的图片显示,包括:

  • 网络
  • 文件
  • Uri资源(如果是视频,会找到缩略图显示)
  • assets
  • drawable

缓存

  • UIL使用两级缓存:磁盘缓存图片文件、内存缓存Bitmap
  • UIL已经实现了多种缓存策略,但一般都使用LRU缓存
  • UIL可以指定图片缓存的路径,和缓存文件名的生成规则
  • 需要自己确定缓存的大小,确定内存缓存的大小尤其重要
    • 通过ActivityManager#getMemoryClass获得单个应用的最大内存,最多划分1/4的最大内存,否则容易导致OOM。???Runtime.getRuntime().maxMemory()
    • 图片较多,大图较多的应用,需要使用较大的缓存,提高缓存的命中率
    • 内存也不宜太小,最少应该能缓存2~3个屏幕大小的Bitmap

解码

  • 要按需解码,否则会造成内存的浪费,主要通过options.inJustDecodeBounds进行预解析
  • ImageAware可以帮助控制解码图片的大小,ImageViewAware就是对ImageView的一个封装
  • 注意照片方向Exif,避免图片错误旋转

显示

实现接口BitmapDisplayer可以自定义显示效果,已实现的包括:带描边的圆形、渐入、圆角矩形(不能四个角分别指定)等。

某些设计可能会出现两个角圆角、另外两个直角的特殊裁剪模式。自己实现这类Displayer时,不要生成一个新的Bitmap,定义一个Drawable会更高效。因为生成新的Bitmap会引起内存分配和回收,从而使GC更加频繁,而Drawable只是在绘制时会使用很少的计算资源。可以参考源码中RoundedBitmapDisplayer。

多线程

图片的下载和解码都需要再后台线程中处理,而且为了提高效率,一般都使用多个线程分别进行解码和网络请求。

共有三个Executor,分别用于

  • 分发任务
  • 处理已缓存图片
  • 处理未缓存图片

已缓存的图片主要占用计算资源,未缓存的图片则主要占用网络资源,所以不应该在一个Executor中竞争。可以分别指定Executor的线程数量,UIL默认为3个。

监听

UIL向外提供了两类监听:ImageLoadingListener和ImageLoadingProgressListener

PauseOnScrollListener主要用于,在列表滚动时暂停图片加载,但在现在的RecyclerView中无法使用。需要自己使用ImageLoader#pause和ImageLoader#resume

了解了UIL是如何处理图片加载的问题之后,其他的第三方库也都是大同小异,下面再介绍下Fresco和Glide。

Fresco的优势和问题

Fresco在解决图片加载问题上的思路和其他框架有很大的不同。它最大的问题有三个:

  • 不能直接使用ImageView
  • 源码很复杂,使用时写的代码也很复杂
  • 需要指定宽高

相比UIL,它的优点主要包括:

  • 5.0以下的系统上,使用ASHMEM,不会占用Java堆内容
  • 多一级未解码图片的内存缓存,减少文件IO(很多Android机器使用1年之后变慢,很大的原因就是IO变慢了,所以这一优化的效果还是很显著的)
  • 支持多图请求,可以在大图显示之前展现缩略图
  • 支持Gif动画和渐进式JPEG(图片还未下载完成的时候就可以先显示一部分)

缓存和网络:Image Pipeline

在5.0系统以下,Image Pipeline 使用 pinned purgeables 将Bitmap数据避开Java堆内存,存在ashmem中。这要求图片不使用时,要显式地释放内存。SimpleDraweeView自动处理了这个释放过程,所以没有特殊情况,尽量使用SimpleDraweeView。

Fresco的缓存虽然有很多优势,但有一个问题:要直接从缓存中获取图片很麻烦。
不仅需要定义一个订阅者,还需要处理回收,甚至还需要找到一个合适的executor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DataSource<CloseableReference<T>> dataSource = ...;
DataSubscriber<CloseableReference<T>> dataSubscriber =
new BaseDataSubscriber<CloseableReference<T>>() {
@Override
protected void onNewResultImpl(
DataSource<CloseableReference<T>> dataSource) {
if (!dataSource.isFinished()) {
return;
}
CloseableReference<T> ref = dataSource.getResult();
if (ref != null) {
try {
T result = ref.get();
...
} finally {
CloseableReference.closeSafely(ref);
}
}
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<T>> dataSource) {
Throwable t = dataSource.getFailureCause();
// handle failure
}
};
dataSource.subscribe(dataSubscriber, executor);

显示

修改图片的显示效果需要使用DraweeHolder,它不仅能控制图片的显示,还可以处理View的Touch事件。默认支持圆角和圆形。

另一个控制图片显示的方法是在Postprocessor中修改Bitmap,但效率很低。

监听

  • ControllerListener:监听图片显示的过程
  • RequestListener:监听图片获取的过程

Glide是个不错的选择

优点

  • 支持本地Video
  • 分别控制每次请求的优先级
  • 支持缩略图
  • 可直接更新AppWidget和Notification中的图片
  • 自定义图片转换效果,还可使用GPU转换(使用GPU处理图片变换,并保持到缓存文件中,在Demo中可以看到GPU变换的10种特效)
  • 定义加载动画
  • 很方便的使用图片裁剪服务

可参考Glide相关文章了解怎么通过自定义的GlideModule优化加载的图片。本文也提供给了参考代码,在代码中引入七牛裁图服务,同时也可体验10种GPU变化的特效。

关于Transformation和BitmapImageViewTarget的使用

  • Transformation#transform:在缓存前对图片进行处理,处理之后的图片才会进行缓存。处理图片的过程中会产生额外的内存消耗,处理后的图片会占据独立的缓存空间。但第二次使用的时候,不再需要处理,直接从缓存中读取。
  • BitmapImageViewTarget#setResource:控制图片的显示逻辑,每次显示的时候都会处理。因此在setResource中应该定义特殊的Drawable来控制显示效果,而不应该对Bitmap进行处理。(Bitmap的频繁生成和回收会导致gc;Drawable是在绘制的时候,通过Paint设置特殊的绘制效果,不会产生新的Bitmap)

参考文章

  • 汉化版Glide使用指南
  • 引入Fresco

使用Mobsy进行MVP实战

发表于 2016-10-26

使用Mobsy进行MVP实战

MVP介绍

  • M:Model,需要显示的数据,以及获取和保存数据的相关逻辑
  • V:View,显示数据的页面或空间,并接受用户的交互
  • P:Presenter,处于M和V中间,是对产品交互的抽象。决定M由哪个V显示,V的动作会引起哪些数据的变化。

如下为典型的MVP工作流程

mvp workflow

需要注意的点:

  1. Presenter不应该直接处理View的事件
  2. View只应向Presenter传递消息,并接受Presenter的命令
  3. Activity和Fragment是View的一部分,一般可用于处理用户事件
  4. Presenter和Model应该是纯Java代码,而且可以独立的运行单元测试

Mosby介绍

gradle依赖

1
2
3
4
5
6
7
8
dependencies {
//...
compile 'com.hannesdorfmann.mosby:mvp:2.0.1'
compile 'com.hannesdorfmann.mosby:viewstate:2.0.1'
//...
}

MvpView和MvpPresenter

  • MvpView是个空接口。在实际使用时,会扩展这个接口来定义一系列的View的方法
  • MvpView会依附或脱离于MvpPresenter。库中定义好的一些MvpView使用代理模式实现了依附和脱离的逻辑。
  • MvpPresenter通过软引用访问View,从而避免内存泄漏
1
2
3
4
5
6
7
8
9
public interface MvpView { }
public interface MvpPresenter<V extends MvpView> {
public void attachView(V view);
public void detachView(boolean retainInstance);
}

通过MvpLceFragment学习使用MVP

LCE就是Loading-Content-Error,代表了一个典型的移动互联网应用的页面。

  1. 显示LoadingView,并在后台获取数据
  2. 如果获取成功,显示获取的到数据
  3. 如果失败,显示一个错误的提示View

先看看MvpLceView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @param <M> The type of the data displayed in this view
*/
public interface MvpLceView<M> extends MvpView {
/**
* Display a loading view while loading data in background.
* <b>The loading view must have the id = R.id.loadingView</b>
*
* @param pullToRefresh true, if pull-to-refresh has been invoked loading.
*/
public void showLoading(boolean pullToRefresh);
/**
* Show the content view.
*
* <b>The content view must have the id = R.id.contentView</b>
*/
public void showContent();
/**
* Show the error view.
* <b>The error view must be a TextView with the id = R.id.errorView</b>
*
* @param e The Throwable that has caused this error
* @param pullToRefresh true, if the exception was thrown during pull-to-refresh, otherwise
* false.
*/
public void showError(Throwable e, boolean pullToRefresh);
/**
* The data that should be displayed with {@link #showContent()}
*/
public void setData(M data);
}

实现MvpLceView的控件或页面一定要包含至少3个View,他们的id分别为R.id.loadingView,R.id.contentView 和 R.id.errorView,因此我们使用如下的xml为Fragment布局。

库工程中的MvpLceFragment和MvpLceActivity已经实现了MvpLceView的三个方法
showLoading,showContent和showError,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!-- Loading View -->
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
/>
<!-- Content View -->
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</android.support.v4.widget.SwipeRefreshLayout>
<!-- Error view -->
<TextView
android:id="@+id/errorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</FrameLayout>

这个页面中将显示一个从网络获取的国家列表,先看看Presenter的代码。这里通过CountriesAsyncLoader获取国家列表,并通过setData和showContent让View显示这些国家信息。当然还获取前显示Loading,获取失败后显示Error。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CountriesPresenter extends MvpBasePresenter<CountriesView> {
@Override
public void loadCountries(final boolean pullToRefresh) {
getView().showLoading(pullToRefresh);
CountriesAsyncLoader countriesLoader = new CountriesAsyncLoader(
new CountriesAsyncLoader.CountriesLoaderListener() {
@Override public void onSuccess(List<Country> countries) {
if (isViewAttached()) {
getView().setData(countries);
getView().showContent();
}
}
@Override public void onError(Exception e) {
if (isViewAttached()) {
getView().showError(e, pullToRefresh);
}
}
});
countriesLoader.execute();
}
}

最后是MvpLceFragment,注意其中的createPresenter是所有的MvpView都需要实现的方法,用于创建和MvpView关联的Presenter,另一个setData两个方法是MvpLceFragment中没有实现的方法,因为只有实现的时候才知道最终的Model,已经如何显示这个Model。

另一个要注意的是MvpLceFragment的四个范型,依次是:显示内容的AndroidView,需要显示的内容Model,MvpView,MvpPresenter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class CountriesFragment
extends MvpLceFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter>
implements CountriesView, SwipeRefreshLayout.OnRefreshListener {
@Bind(R.id.recyclerView) RecyclerView recyclerView;
CountriesAdapter adapter;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.countries_list, container, false);
}
@Override public void onViewCreated(View view, @Nullable Bundle savedInstance) {
super.onViewCreated(view, savedInstance);
// Setup contentView == SwipeRefreshView
contentView.setOnRefreshListener(this);
// Setup recycler view
adapter = new CountriesAdapter(getActivity());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
loadData(false);
}
public void loadData(boolean pullToRefresh) {
presenter.loadCountries(pullToRefresh);
}
@Override protected CountriesPresenter createPresenter() {
return new SimpleCountriesPresenter();
}
@Override public void setData(List<Country> data) {
adapter.setCountries(data);
adapter.notifyDataSetChanged();
}
@Override public void onRefresh() {
loadData(true);
}
}

实战——封装一个LceListView

这部分代码可参考代码,只用关注mvplist包下的相关代码即可。

这个页面的View结构和之前的MvpLceFrgment类似,并通过修改Adapter给RecycleView的末尾增加了一个LoadMoreView。将这类业务的下拉刷新,上拉加载更多,以及错误处理都抽象出来。实现列表时,剩下的逻辑主要包括

  1. Model:获取的数据,以及从数据中获取每个列表项展示需要的数据列表
  2. Presenter:刷新和加载更多时,分别调用Model的获取数据方法
  3. View:根据数据决定ViewHolder的类型,以及ViewHolder的实现

如何使用,可以参考mvplist.sample包的内容

基类LecListView的方案

LecListView包含了一个LoadMoreView,LoadMoreView也符合Mvp架构。

我们先看看LoadMoreView的实现。

LoadMoreView

  • 没有Model:这里的数据只包括一个状态,因此没有对应的Model类
  • LoadMoreView
    • 提供一个setState(int state)方法,供Presenter更新状态
    • 加载更多失败的情况下,点击View会请求Presenter更改状态到Loading
  • LoadMorePresenter
    • 通过setLoadMoreState(int state)改变View的状况
    • 通过接口LoadMoreListener#onLoadMore通知LceListViewPresenter加载更多数据

LecListView

  • IListModel:在LceListView中,Model应实现IListModel,从而提供一个在RecycleView中显示的数据列表。
```Java
public interface IListModel<M> {
    List<M> getData();
}
```    
  • LceListView
    • 实现了MvpLceView的五个方法,和在列表底部添加数据的addData方法
    • 监听RecyclerView的滚动,通知Presenter改变加载更多的状态
    • 使用LceListAdapter为RecyclerView底部增加了LoadMoreView
  • LceListPresenter实现ILceListPresenter
    • 在refreshData时,通知LceListView显示Loading
    • 包含一个LoadMorePresenter来实现ILoadMorePresenter的接口
    • 在LoadMoreView附着在窗口时,调用LceListPresenter#setLoadMorePresenter

参考文章

  • Mosby MVP
  • Android—-MVC、MVP、MVVM、区别介绍

一次搞定Process和Task

发表于 2016-10-18

一次搞定Process和Task

关于进程-Process

影响process的属性

控制组件运行进程的有两个个属性:android:process和android:multiprocess

关于android:process

  • 可分别指定Application和四大组件运行的进程名
  • 不指定时,使用包名作为进程名
  • 如果进程名是以冒号开头的,则这个进程是应用的私有进程
  • 如果进程名是以字符开头的,且符合包名规范,则这个进程是全局的

只有provider和activity定义了android:multiprocess,但不要在activity中使用。

如果android:multiprocess为true,则每个访问provider的应用都会自己创建一个ContentProvider实例

  • 优势:避免跨进程通信,提高数据访问效率
  • 弊端:多个实例导致系统内存消耗变大,且难以处理多个进程之间的数据同步问题

如果需要两个不同APK中的组件运行于同一个进程,需要以下三个条件:

  • 两个应用程序使用相同的android:sharedUserId
  • 两个应用使用相同的keystore进行签名
  • 为组件设置相同的android:process

不同Process带来的影响

跨进程通信

由于不同的进程使用不同的内存空间,所以不同进程之间的通信本质上只依赖以下四种方式

  • Binder:包括Messenger、AIDL,只能传递基本数据类型或实现了Parcelable的对象,本质是数据的序列化和反序列化
  • Intent:跨进程的组件调用,本质也是数据的序列化和反序列化
  • ASHMEM:匿名共享内存,需要依赖Binder传递共享内存文件描述,使用系统签名的APK才能直接使用
  • 其他通用方式:共享文件、网络接口等

ContentProvider的实现就是使用ASHMEM

  • ContentProvider保存的数据,都是以私有文件存储的,其他进程无法访问
  • 其他程序创建CursorWindow时,同时创建了一块匿名共享内存,并实现了Parcelable
  • 通过Binder将CursorWindow和共享内存文件描述传递给ContentProvider
  • ContentProvider创建自己的共享内存文件描述,并指向共享内存

进程优先级

内存不足时,系统可能杀掉进程,这时进程中的Application和应用组件也会随之销毁。系统如何选择停止的进程,就涉及到进程优先级了。

  1. 前台进程 Activte process
    • 前台响应用户事件的Activity以及与之绑定的Service
    • startForeground的Service
    • 正在执行onStart,onCreate,OnDestroy的Service
    • 正在执行onReceive的BroadcastReceiver
  2. 可见进程 Visible Process
    • onPause但未onStop的Activity
    • 绑定到可见Activity的Service
  3. 服务进程 Service process
    • 普通Service
  4. 背景进程 Background process
    • onStop的Activity
  5. 空进程 Empty process
    • 不包含任何组件的进程

Process的实际使用

  1. 子进程可以分担主进程的内存压力
  2. 在主进程Crash时,子进程中的功能不受影响
  3. 子进程的Application#onCreate中可以跳过一些不必要的初始化

关于任务-Task

关于Task,有太多的参数与之相关,而且他们相互影响,感觉无法穷尽。下面仅从实际的使用出发,明确和Task相关的一些原则。

Task和startActivityForResult

使用startActivityForResult的必要条件是被启动的Activity和原Activity要在同一个Task中。因此,使用startActivityForResult时,会强制将新启动的Activity放在原来的Task中,不论activiy的xml属性和Intent#Flag_XXX。

只有一个例外FLAG_ACTIVITY_NEW_TASK:如果使用这个标签,原Activity会立刻收到onActivityResult,并执行和startActivity相同的逻辑。在后面的分析中可以看到,仅凭这个FLAG是无法正真启动新Task的。

多Task有关的参数

可以下载多Task相关的演示代码,修改MultiTaskActivity启动方法的Intent和Manifest中的属性,就可演示这小结的大部分例子。

launchMode

  1. standard和singleTop:可以包含多个Activity实例
  2. singleInstance和singleTask:只有一个Activity实例
  3. singleInstance是一个Task中唯一的Activity
  4. singleTask不一定是Task root(google文档有问题,但使用中建议作为Task root使用)

taskAffinity

  1. 仅有singleInstance启动新Task不依赖taskAffinity
  2. singleTask的Activity只有其taskAffinity和原Activity不一样时才会启动新Task
  3. standard和singleTop启动新Task,不仅要求新的taskAffinity,而且需要FLAG_ACTIVITY_NEW_TASK
  4. 如果有后台Task和要启动的Activity具有相同的taskAffinity,则不会启动新的Task,而是将后台Task切换到前台,并根据其他属性和标签重新安排后台Task中的Activity和新Activity
  5. taskAffinity的值应该是以‘.’开头的字符串

其他多Task属性和标签

只有standard和singleTop类型的Activity才可能出现多个实例。因此,只有这两类Activity才可能出现多个实例,并处于不同的Task中。下面的讨论,也仅限于这两类。

  • FLAG_ACTIVITY_NEW_DOCUMENT
    • 如果已经有Activity符合Intent,则切换到该Activity
    • 否则启动新的Activity
  • FLAG_ACTIVITY_MULTIPLE_TASK
    • 配合FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_NEW_DOCUMENT使用,在启动时不再寻找匹配的Activity,而是直接启动新任务。
  • documentLaunchMode:与前面两个标签相互限制。
    • always:每次启动时都会启动新的Task。等同于FLAG_ACTIVITY_NEW_DOCUMENT和FLAG_ACTIVITY_MULTIPLE_TASK
    • intoExisting:如果有符合Intent的Activity存在,则不再启动新Task,否则启动新Task。等同于FLAG_ACTIVITY_NEW_DOCUMENT,且不带FLAG_ACTIVITY_MULTIPLE_TASK
    • none:默认值。由其他标签和属性决定是否启动新Task。
    • never:使FLAG_ACTIVITY_NEW_DOCUMENT和FLAG_ACTIVITY_MULTIPLE_TASK失效。

FLAG_ACTIVITY_NEW_DOCUMENT和documentLaunchMode都是5.0的新特性。通过FLAG_ACTIVITY_NEW_DOCUMENT启动一个新的Task,不需要指定taskAffinity。因此,最好不要将taskAffinity与FLAG_ACTIVITY_NEW_DOCUMENT混用。

在实测中发现,never无法使FLAG_ACTIVITY_NEW_DOCUMENT失效,只是使FLAG_ACTIVITY_MULTIPLE_TASK失效。

Task相关的其他参数

这部分的参数主要处理以下两个问题,演示代码不包括这部分的例子。

  • Task在最近任务中的表现
  • 一个新Activity进入已有的Task时,如何重新安排Task中的老Activity和新Activity

其它xml元素

  1. alllowTaskReparenting:默认false,只有standard和singleTop有效,表示Activity实例可以换到其他Task中。(通过应用图标启动程序时会生效,在程序内通过FLAG_ACTIVITY_NEW_TASK切换任务时,不生效)例子:浏览器。
  2. finishOnTaskLaunch:默认false,通过应用图标重新启动程序时,这个Activity会被销毁。
  3. clearTaskOnLaunch:用于Task root,默认false,启动时会清空Task上的其它Activity,只保留root。(通过应用图标启动程序时会生效,通过最近任务启动时不生效)
  4. alwaysRetainTaskState:用于Task root,默认false,在应用切换到后台30分钟后会被系统清理。如果为true则不会被系统清理
  5. noHistory:页面不可见时被自动销毁,不会保存在mHistory中

其他Intent.FLAG

  1. FLAG_ACTIVITY_CLEAR_TOP:启动的Activity存在,则不创建新实例,而是使用原有实例,并清空上面的其它Activity。在Activity是singleTask,或者Intent中有FLAG_ACTIVITY_SINGLE_TOP,这个Activity不会重新创建。
  2. FLAG_ACTIVITY_REORDER_TO_FRONT:Activity会重新排序,任何Activity都不会被销毁。
  3. FLAG_ACTIVITY_NO_HISTORY:和xml中的noHistory效果相同
  4. FLAG_ACTIVITY_TASK_ON_HOME:后退时直接回Home,而不会回到之前的Task

Task和Process的关系

之前文章的《怎么处理SaveState》的末尾也提到的Task和Process的关系。这里重复一遍。

我们先看下再ActivityManagerService中进程Process和Task的关系

Task&Process

  1. ActivityManagerService通过一个列表mHistory来管理所有ActivityRecord
  2. 相同TaskRecord中的ActivityRecord在列表中处于连续位置
  3. 同一个TaskRecord中的ActivityRecord可能处于不同的ProcessRecord中

由于以下两个因素,使得很难找到Task和进程之间关联的清晰线索。

  • 同一Task中的Activity可能属于不同进程
  • 进程中不仅有Activity,还有Service和BroadcastReceiver

先看Task中Activity销毁

  • 处理的问题:一个进程内部,前后台Task的资源协调
  • 触发时机:进程使用的内存接近上限时(根据机型不同,大约在64M~256M之间)
  • 会调用Activity#onDestroy,后台Task回到前台时会触发Activity#onRestoreInstanceState

再看进程被杀

  • 处理的问题:系统控制中,多进程之间的资源协调
  • 触发时机:整个系统使用的内存接近机器配置的内存上限时
  • 不会调用Activity#onDestroy,后台Task回到前台时不一定会触发Activity#onRestoreInstanceState,和Task的启动方式和启动时间有关。

我们以一个简化的例子讨论两者的关系。假设:

  • 单进程最大可使用内存为100M,进程使用内存超过90M时会触发后台Task销毁。
  • 系统总可用内存为200M,系统使用内存超过190M时会触发后台进程被杀。
  • 系统中运行着3个进程,他们在三个Task中的分布和内存使用如下
  • Task1处于前台运行
Momory Usage Process1 Process2 Process 3
Task1 60M 20M -
Task2 20M 20M -
Task3 - - 40M

如果T1 P1部分消耗的内存由60M上升到75M,由于P1的总内存消耗达到95M,所以会导致P1 T2中的Activity被销毁。

如果T1 P2部分消耗的内存由20M上升到50M,会导致系统总内存消耗达到190M。此时三个Process中,P1和P2和前台Task关联,优先级较高,所以系统会杀掉P3。

这个例子,只是对两者关系的一个简要说明。系统对进程的实际处理方式要复杂得多!

怎么处理SaveState

发表于 2016-10-13

怎么处理SaveState

前文链接:是时候使用SaveState了

要使前文介绍的5.0新机制生效,应用需要设计为多Task结构,而且要处理好页面的SaveState相关逻辑。

这里先讨论SaveState的相关逻辑,再介绍怎么设计应用的Task结构。

处理SaveState的四个方面

在之前演示代码的基础上,我们稍做改动:代码链接

  1. ActivityTwo包含一个文字列表
  2. 文字列表中每一项的前缀是由启动的Intent的Extra决定
  3. 文字列表中每一项的后缀是由创建页面的时间戳决定

如果我们不处理SaveState,则在恢复ActivityTwo时,列表中每一项的后缀会发生变化,如果处理SaveState,则能保证返回时页面和创建时一样。在代码中通过修改ActivityTwo#ENABLE_SAVE_STATE可以切换两种状态。

创建时的页面:

创建时的页面

不处理SaveState恢复后的页面

不处理SaveState恢复后的页面

处理SaveState恢复后的页面

处理SaveState恢复后的页面

SaveState的处理应该包括4个部分

  1. 保存数据
  2. 恢复数据
  3. 处理View
  4. 处理Fragment

第一:保存数据

这一步相对简单,只要把页面中的数据变量保存到outState中

1
2
3
4
5
6
7
8
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (ENABLE_SAVE_STATE) {
outState.putString(KEY_PREFIX, mPrefix);
outState.putString(KEY_SUFFIX, mSuffix);
}
}

实际项目中,可能由Intent中传入页面id,再通过网络接口获取页面详情。这时,也需要将网络返回的数据也保存在outState中。

第二:恢复数据

恢复数据时,需要考虑onCreate的正常处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_two);
mListView = (ListView) findViewById(R.id.lv);
if (ENABLE_SAVE_STATE && savedInstanceState != null) {
// 恢复流程
mPrefix = savedInstanceState.getString(KEY_PREFIX);
mSuffix = savedInstanceState.getString(KEY_SUFFIX);
mListView.setAdapter(new TwoAdapter(mPrefix, mSuffix));
} else {
// 初始化流程
mPrefix = getIntent().getStringExtra(KEY_PREFIX);
mSuffix = String.valueOf(SystemClock.uptimeMillis());
mListView.setAdapter(new TwoAdapter(mPrefix, mSuffix));
}
}

实际项目中,初始化网络请求应放初始化流程中,而类似mListView.setAdapter的View更新逻辑应该在网络请求的回调中处理。

第三:处理View

上面的例子中,除了ListView的内容,我们还应注意到ListView的位置。不管我们是否处理SaveState,ListView都会恢复到离开时的位置。这是因为ListView的基类AbsListView实现了Save和Restore,下面是节选的一小段源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// AbsListView
@Override
public Parcelable onSaveInstanceState() {
......
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
if (mPendingSync != null) {
// Just keep what we last restored.
ss.selectedId = mPendingSync.selectedId;
ss.firstId = mPendingSync.firstId;
ss.viewTop = mPendingSync.viewTop;
ss.position = mPendingSync.position;
ss.height = mPendingSync.height;
ss.filter = mPendingSync.filter;
ss.inActionMode = mPendingSync.inActionMode;
ss.checkedItemCount = mPendingSync.checkedItemCount;
ss.checkState = mPendingSync.checkState;
ss.checkIdState = mPendingSync.checkIdState;
return ss;
}
.......
}

除了AbsListView,还有很多View实现了Save和Restore的机制,包括ViewPager当前的位置,EditText和TextView中的文本等。

关于View的处理主要注意以下几点:

  • 系统以一个Map来保存View的状态,以id为key
  • 没有id的View是不会被保存状态的
  • 如果id重复,则View的状态会被覆盖
  • 自定义的View,要注意处理View的Save和Restore
  • 如果View的恢复有特殊处理逻辑,需要充分考虑View更新的时机,注意onCreate、onRestoreInstanceState和网络请求回调的时序问题。

第三:处理Fragment

关于Fragment的处理,和Activity的处理基本一致。只需要注意一个特殊的地方

很多项目都会只在Manifest中声明一个FragmentContainerActivity的模式,各个页面通过Fragment实现。然后启动FragmentContainerActivity,通过Extra传递要展现的Fragment类名和Argument。FragmentContainerActivity#onCreate中创建并添加Fragment。

由于Activity的恢复机制会自动重建Fragment,所以在恢复时不要再重复创建添加Fragment。当然也不要新创建,并通过replace替换老的,这样会使Fragment的恢复机制失效。

这部的例子演示,可使用如下步骤复现:

  1. 修改ActivityThree#ENABLE_SAVE_STATE进行切换
  2. 启动Three
  3. 点击Three的文字可返回到One
  4. 在One中消耗内存,直到logcat中出现ActivityThree#onDestroy
  5. 再次启动Three

合理区分Task

Android关于Task的定义十分复杂,而且很多特性在普通应用开发中根本用不到。而且在5.0之后,又引入了android:documentLaunchMode让它变得更加复杂了。

关于Task,还需要另一篇专题来讨论。这里只举两类多Task的例子。

使用singleInstance

给一些特定的页面设置singleInstance,可使他们处于单独的Task中。这类页面一般和其他页面没有很强的逻辑关系,同时又是消耗资源的大户。

适用场景:

  • 视频播放或录制页(视频的编码解码,视频上的绚丽弹幕和礼物等)
  • 应用介绍页(包含很多大图和动画)
  • 应用内用于现实外部网页的单WebView页
  • ViewPager实现的大图图集页

使用taskAffinity

给一组完成某一功能的Activity设置相同的taskAffinity,就可使用FLAG_ACTIVITY_NEW_TASK启动新Task。使用taskAffinity启动的新Task一般都包括多个Activity,而且和别的参数相互影响,请谨慎使用。

适用场景:

  • 注册、登陆、找回密码等页面(这类页面一般占用的资源并不多,不一定要设计在独立的Task中)
  • 自定义的选择相册,照相机,图片预览,图片裁剪等页面

关于多Task的补充

由于不同的Task之间不能通过StartActivityForResult传递结果,可能需要EventBus或其他机制在Task之间传递信息。

默认的每个Task都会出现在最近应用中。上述的这些情况中,都可使用android:excludeFromRecents避免这些Task在最近应用中出现

后面单独补充了一节进程被杀的介绍,因为它很容易和Task中Activity销毁混淆

Task中Activity销毁 vs 进程被杀

我们先看下再ActivityManagerService中进程Process和Task的关系

Task&Process

  1. ActivityManagerService通过一个列表mHistory来管理所有ActivityRecord
  2. 相同TaskRecord中的ActivityRecord在列表中处于连续位置
  3. 同一个TaskRecord中的ActivityRecord可能处于不同的ProcessRecord中

由于以下两个因素,使得很难找到Task和进程之间关联的清晰线索。

  • 同一Task中的Activity可能属于不同进程
  • 进程中不仅有Activity,还有Service和BroadcastReceiver

先看Task中Activity销毁

  • 处理的问题:一个进程内部,前后台Task的资源协调
  • 触发时机:进程使用的内存接近上限时(根据机型不同,大约在64M~256M之间)

再看进程被杀

  • 处理的问题:系统控制中,多进程之间的资源协调
  • 触发时机:整个系统使用的内存接近机器配置的内存上限时

我们以一个简化的例子讨论两者的关系。假设:

  • 单进程最大可使用内存为100M,进程使用内存超过90M时会触发后台Task销毁。
  • 系统总可用内存为200M,系统使用内存超过190M时会触发后台进程被杀。
  • 系统中运行着3个进程,他们在三个Task中的分布和内存使用如下
  • Task1处于前台运行
Momory Usage Process1 Process2 Process 3
Task1 60M 20M -
Task2 20M 20M -
Task3 - - 40M

如果T1 P1部分消耗的内存由60M上升到75M,由于P1的总内存消耗达到95M,所以会导致P1 T2中的Activity被销毁。

如果T1 P2部分消耗的内存由20M上升到50M,会导致系统总内存消耗达到190M。此时三个Process中,P1和P2和前台Task关联,优先级较高,所以系统会杀掉P3。

这个例子,只是对两者关系的一个简要说明。系统对进程的实际处理方式要复杂得多!

是时候使用SaveState了

发表于 2016-10-12

是时候使用SaveState了

Android系统在5.0时,对进程内的内存管理做了一个优化,但并没有明确的文档说明这个优化。

这个优化为解决Android应用的内存问题,提供了一个新的思路。但如果开发者习惯于单Task的应用开发,或者从来不考虑SaveState,那开发者可能根本无法体会这个新机制的好处。

本文首先从SaveState讲起,对于了解SaveState的同学,可以直接跳过

什么是SaveState

要了解什么是SaveState必须要先知道Activity的两个关键方法

  • onSaveInstanceState
  • onRestoreInstanceState

onSaveInstanceState时系统做了些什么

在Activity被回收之前,系统会调用onSaveInstanceState(Bundle outState)来保存View的状态,并到传入的outState对象中。

  1. 保存Window
  2. 保存Fragment
  3. 调用外部注册的回调方法
1
2
3
4
5
6
7
8
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
getApplication().dispatchActivitySaveInstanceState(this, outState);
}

onRestoreInstanceState时系统做了些什么

在Activity被重新创建时,会通过onCreate(Bundle savedInstanceState)和onRestoreInstanceState(Bundle savedInstanceState)传入保存的状态信息并恢复View的状态。

  1. onCreate重建Fragment
  2. onRestoreInstanceState恢复Window状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
if (mLastNonConfigurationInstances != null) {
mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
}
if (mActivityInfo.parentActivityName != null) {
if (mActionBar == null) {
mEnableDefaultActionBarUp = true;
} else {
mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
}
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
}
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
if (mVoiceInteractor != null) {
mVoiceInteractor.attachActivity(this);
}
mCalled = true;
}
1
2
3
4
5
6
7
8
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
mWindow.restoreHierarchyState(windowState);
}
}
}

Window在save和restore时对View的处理

  1. Save时,遍历View的树状结构调用 Parcelable onSaveInstanceState()
  2. 以View的id为key在Window的SparseArray<Parcelable>中保存这些 Parcelable
  3. Restore时,Window从savedInstanceState获取View的savedStates
  4. 遍历View的树状结构调用 onRestoreInstanceState(Parcelable state)
  5. View根据id获取自己的state并恢复

小结

  1. Save和Restore的机制主要是用于保存和恢复View的
  2. 没有id的View是不会被保存状态的
  3. 如果id重复,则View的状态会被覆盖
  4. 被保存的Fragment会在onCreate中被自动创建和添加到FragmentActivity中
  5. 被保存的View不会被自动创建,只是通过id获取savedInstance用于更新View

关于SaveState的详细介绍可以参考文章Android中SaveState原理分析

为什么开始使用SaveState

为什么很多人不重视SaveState

我们先了解下会用到Restore机制的地方

  1. FragmentStatePagerAdapter用于在ViewPager中使用可回收和重建的Fragment
  2. 应用Crash时,当前页面被销毁,前一个页面被Restore

    • 在4.0之前,系统不会自动重启应用
    • 在4.0之后,系统会自动重启,并通过Restore机制恢复Crash的页面。

FragmentStatePagerAdapter中考虑SaveState是必须的,所以大家都会被迫处理SaveState的问题。

大多数开发者不会考虑Crash重建的问题,所以SaveState很少被开发者重视。而认真考虑过Crash重建的开发者一定不会对SaveState陌生。

5.0的新机制

在5.0中,SaveState有了新的作用,稍加利用,它会帮你解决OutOfMemory。而根据Google的统计,到今年下半年,Android5.0及以上的系统占比将超过50%。

要触发这个新机制,你的应用必须是多Task结构的。关于Task,那又是一个很大的话题,下面我只用一个简单的例子看看这个新机制。

演示代码可以通过git仓库下载

这里看看关键的ActivivtyOne.java

  • ActivityOne是standard
  • ActivityTwo是singleInstance,所以他会在单独的新的Task中
  • AcitivyOne可以启动ActivityTwo
  • ActivityOne可以不断消耗内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class ActivityOne extends BaseActivity {
boolean forceOom = false;
List<Bitmap> memory = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_one);
}
public void launchTwo(View view) {
ActivityTwo.launch(this);
}
/**
* 第一次点击使内存接近进程能获取的内存上限,再次点击触发OOM
* @param view
*/
public void consumeMem(View view) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (!forceOom && isLowMemory()) {
forceOom = true;
break;
}
memory.add(Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}).start();
}
/**
* 判断已使用的内存是否接近了单进程的内存上限
*
* @return
*/
public boolean isLowMemory() {
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
long total = Runtime.getRuntime().totalMemory() / (1024l * 1024l);
int max = activityManager.getMemoryClass();
Log.w(getClass().getSimpleName(), total + "/" + max);
if (total > activityManager.getMemoryClass() * 0.85) return true;
return false;
}
public static void launch(Activity activity) {
activity.startActivity(new Intent(activity, ActivityOne.class));
}
}

操作步骤:

  1. ActivityOne启动ActivityTow
  2. ActivityTwo启动ActivityOne,从而切换到老的Task中,ActivityTwo不会被销毁
  3. ActivityOne不断消耗内存,直到接近进程使用内存的上限(Android系统对每个进程使用的最大内存有一个限制)

这时通过logcat你会看到5.0的不同:

  • 5.0之前:不会有什么事情发生。再次点击消耗内存,会OOM,整个进程被杀。
  • 5.0及之后:ActivityTwo#OnDestroy会被调用,这时再启动ActivityTwo,可以看到ActivityTwo#onRestoreInstanceState的调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
W/ActivityOne: 83571877-onCreate: TaskId-7551
W/ActivityTwo: 128141518-onCreate: TaskId-7552
W/ActivityOne: 83571877-onSaveInstanceState
W/ActivityOne: 219591424-onCreate: TaskId-7551
W/ActivityTwo: 128141518-onSaveInstanceState
W/ActivityOne: 23/192
W/ActivityOne: 42/192
W/ActivityOne: 88/192
W/ActivityOne: 111/192
W/ActivityOne: 157/192
以下是5.0系统上才会出现的
W/ActivityTwo: 128141518-onDestroy
W/ActivityOne: 176/192
W/ActivityTwo: 80252517-onCreate: TaskId-7552
W/ActivityTwo: 80252517-onRestoreInstanceState
W/ActivityTwo: 80252517-onNewIntent
W/ActivityOne: 219591424-onSaveInstanceState

因此我们可以得出结论:

5.0之后,Android进程在遇到内存瓶颈时,会通过主动销毁进程中的Acitivty来释放内存。这些被销毁的Activity都属于后台Task,当被销毁的Activity需要重新出现时,会触发Restore机制

当然,这个结论又会引起很多疑问。

  1. 为什么不销毁当前Task中的后台Activity?
  2. 如果后台Task中有多个Activity是一起销毁吗?如果后台Task中的多个Activity是属于不同的进程呢?
  3. ……

关于这些问题,需要分析源码才能找到答案,但我不希望这篇文章变成对Android源码的一次分析。我将在下篇文章,继续介绍怎么处理SaveState。

当然,即使没有SaveState在5.0上带来的好处,正确处理页面的SaveState也是保证Android应用程序健壮性的一个重要部分。

黄怡菲

黄怡菲

各种杂七杂八的技术

5 日志
1 标签
© 2016 黄怡菲
由 Hexo 强力驱动
主体备案号:京ICP备16052131号