Android-内存优化-首页内存占用优化
平时开发大家都会遇到OOM问题,今天介绍下我最近如何优化首页内存占用问题的。
描述
- 应用OutOfMemory Crash很多。
- 首页4个Tab都展示过后,占用内存达到110MB左右(小米4)。
- 首页4个Tab里都是图片。
- 首页展示的框架是ListView多ItemType方式实现的。
- 显示图片的View都是使用NetworkImageView。
- 我们使用图片库是Glide。
分析
- 首页内存占用主要是Bitmap,并且内存一直占用着,给二三级页面申请内存带来压力(GC),减少了首页占用的内存,手机OOM问题就会好很多。
- 要想内存占用小,必须减少与Bitmap的引用。
- 是否可以把其他未显示的Tab内存回收或断开与Bitmap的引用。
- ListView的多ItemType时,未显示的部分,其实还是在内存中的,所以其中的ImageView还是引用着Bitmap的,Bitmap内存空间是无法释放的。
- 手机一个屏幕能显示的内容有限,每个Tab中内容无法全部显示,是否可以在合适的时机(首页Tab切换或onStop被触发的时候)把ListView中未显示ItemType中ImageView与Bitmap引用断开。
如何回收掉ListView中未显示的Item占用的内存???
验证
- 大家都知道ListView会把未显示的View添加到RecycleBin中,等待下次的重用。child.dispatchFinishTemporaryDetach()通知View被重新加入到ListView中展示给用户。
// ListView.java
View obtainView(int position, boolean[] isScrap) {
...
// 从Recycler中获取View,用于重用
final View scrapView = mRecycler.getScrapView(position);
// 大家经常见到的adapter.getView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;
//告诉Child你重新被添加到ListView,再次进入ListView显示区域中,再次被用户看到。
child.dispatchFinishTemporaryDetach();
}
}
...
return child;
}
- ListView滑动时调用trackMotionScroll函数,处理滑出去的View。
// ListView.java, listView滑动的时候调用的函数
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
if (down) {
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
if (child.isAccessibilityFocused()) {
child.clearAccessibilityFocus();
}
//把滑出ListView区域的View添加到RecycleBin中
mRecycler.addScrapView(child, position);
}
}
}
} else {
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
if (child.isAccessibilityFocused()) {
child.clearAccessibilityFocus();
}
//把滑出ListView区域的View添加到RecycleBin中
mRecycler.addScrapView(child, position);
}
}
}
}
return false;
}
- RecycleBin把滑出ListView区域的View添加到数组中,带后续重用。scrap.dispatchStartTemporaryDetach()通知View滑出ListView的显示区域,用户看不到了。
// ListView.java, RecycleBin中addScrapView方法,把View添加到缓存数组中。
void addScrapView(View scrap, int position) {
...
// 通知View你已经滑出ListView的区域了
scrap.dispatchStartTemporaryDetach();
...
// Don't scrap views that have transient state.
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
...
} else {
...
// 回调通知scrap从ListView区域中滑出去了。
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
- View.dispatchFinishTemporaryDetach和View.dispatchStartTemporaryDetach正好满足我们的需求,让我们再看看这两个函数。
/**
* @hide
*/
public void dispatchStartTemporaryDetach() {
onStartTemporaryDetach();
}
/**
* This is called when a container is going to temporarily detach a child, with
* {@link ViewGroup#detachViewFromParent(View) ViewGroup.detachViewFromParent}.
* It will either be followed by {@link #onFinishTemporaryDetach()} or
* {@link #onDetachedFromWindow()} when the container is done.
*/
public void onStartTemporaryDetach() { // View消失时回调
removeUnsetPressCallback();
mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT;
}
/**
* @hide
*/
public void dispatchFinishTemporaryDetach() {
onFinishTemporaryDetach();
}
/**
* Called after {@link #onStartTemporaryDetach} when the container is done
* changing the view.
*/
public void onFinishTemporaryDetach() { // View再次显示时回调
}
View的onStartTemporaryDetach和onFinishTemporaryDetach方法完全满足我们的需求,而且不需要去改动业务代码。如何实现呢?大家接着看…
解决
既然我们知道ListView中的Item消失与显示的回调,那么我们就可以干一个很简单事情了。大体思路如下:
- 我们的应用中图片展示都是使用的NetworkImageView。
- NetworkImageView实现onStartTemporaryDetach和onFinishTemporaryDetach方法。
- 收集未显示的NetworkImageView。
- 在合适时机把未显示的NetworkImageView与Bitmap引用断开。
代码实现如下:
- NetworkImageView实现
public class NetworkImageView extends ImageView {
// list view reuse view
@Override
public void onFinishTemporaryDetach() {
super.onFinishTemporaryDetach();
GlideRecycledHelper.getInstance().detachView(this);
}
// list view item no display
@Override
public void onStartTemporaryDetach() {
super.onStartTemporaryDetach();
GlideRecycledHelper.getInstance().attachView(this);
}
// add view
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
GlideRecycledHelper.getInstance().detachView(this);
}
// remove view
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
GlideRecycledHelper.getInstance().detachView(this);
}
}
- GlideRecycledHelper实现收集未显示的NetworkImageView。
public class GlideRecycledHelper {
private LinkedList<WeakReference<View>> mRecycledViews = new LinkedList<>();
private static GlideRecycledHelper sInstance = new GlideRecycledHelper();
/**
* GlideRecycledHelper
*
* @return GlideRecycledHelper instance
*/
public static GlideRecycledHelper getInstance() {
return sInstance;
}
private GlideRecycledHelper() {
}
/**
* attachView
*
* @param view view
*/
public void attachView(View view) {
if (view == null) {
return;
}
if (checkThread()) {
return;
}
if (!findView(view)) {
mRecycledViews.add(new WeakReference<View>(view)); // 把未显示的View添加到队列中
}
}
/**
* detachView
*
* @param view view
*/
public void detachView(View view) {
if (view == null) {
return;
}
if (checkThread()) {
return;
}
removeView(view); // 从队列中删除再显示的View
}
private boolean findView(View view) {
Iterator<WeakReference<View>> iterator = mRecycledViews.iterator();
while (iterator.hasNext()) {
WeakReference<View> weakView = iterator.next();
View v = weakView.get();
if (v == null) {
iterator.remove();
} else if (v == view) {
return true;
}
}
return false;
}
private void removeView(View view) {
Iterator<WeakReference<View>> iterator = mRecycledViews.iterator();
while (iterator.hasNext()) {
WeakReference<View> weakView = iterator.next();
View v = weakView.get();
if (v == null) {
iterator.remove();
} else if (v == view) {
iterator.remove();
}
}
}
/**
* clear recycled view memory
*/
public void clearMemory() {
if (checkThread()) {
return;
}
try {
Iterator<WeakReference<View>> iterator = mRecycledViews.iterator();
while (iterator.hasNext()) {
WeakReference<View> weakView = iterator.next();
View v = weakView.get();
if (v == null) {
iterator.remove();
} else {
clear(v);
}
}
} catch (StackOverflowError error) {
}
}
/**
* clear view reference to glide request
*
* @param view view
*/
public static void clear(View view) {
if (checkThread()) {
return;
}
try {
clearInternal(view);
} catch (StackOverflowError e) {
}
}
// 断开NetworkImageView与Bitmap之间的引用
private static void clearInternal(View view) {
if (view == null) {
return;
}
if (view instanceof NetworkImageView) {
try {
Glide.clear(view);
} catch (Exception e) {
e.printStackTrace();
}
((ImageView) view).setImageDrawable(null);
return;
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
clearInternal(viewGroup.getChildAt(i));
}
}
}
private static boolean checkThread() {
if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
return true;
}
return false;
}
}
总结
有人可能会问,为什么不直接使用ListView的setRecyclerListener
/**
* Sets the recycler listener to be notified whenever a View is set aside in
* the recycler for later reuse. This listener can be used to free resources
* associated to the View.
*
* @param listener The recycler listener to be notified of views set aside
* in the recycler.
*
* @see android.widget.AbsListView.RecycleBin
* @see android.widget.AbsListView.RecyclerListener
*/
public void setRecyclerListener(RecyclerListener listener) {
mRecycler.mRecyclerListener = listener;
}
首页如果ListView中ItemView未显示时立马就断开与View的引用,这样在上下滑动ListView的时候,ItemView会闪现下默认图,个人觉得这样的用户体验不好,老板看到应该也会觉得体验不好。不过二三级页面可以做成这种方式。
个人总结
- 多看android源码才是王道。
- 优化无止境,这才刚开始,后续会接着介绍关于内存方面的优化。
- 后续会介绍Fresco来优化内存。