背景
壁纸模块中需要实现预览效果,支持拖动查看全图,滑动到图片边界拖动后自动切换到下一页,最终采用开源控件photoView,因为项目加载图片库采用里facebook fresco 开源库,所以对photoview进行了改造。代码如下。
public class PhotoView extends ImageView implements IPhotoView {
private PhotoViewAttacher mAttacher;
private ScaleType mPendingScaleType;
private Context myContext;
private CloseableReference<CloseableImage> imageReference = null;
public DraweeHolder<GenericDraweeHierarchy> mDraweeHolder;
private CloseableReference copyCloseableImageReference;
private ImageLoadLintener imageLoadLintener;
public void setImageLoadLintener(ImageLoadLintener imageLoadLintener) {
this.imageLoadLintener = imageLoadLintener;
}
public PhotoView(Context context) {
this(context, null);
}
public PhotoView(Context context, AttributeSet attr) {
this(context, attr, 0);
}
public PhotoView(Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
super.setScaleType(ScaleType.MATRIX);
init();
GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources()).build();
// .setFadeDuration(3000)
// .setFailureImage(getResources().getDrawable(R.drawable.error),
// ScalingUtils.ScaleType.CENTER_CROP)
// .set //一些通用设置可以在这里进行设置,此处设置一个空的hierarchy为了加载图片
// .setPlaceholderImage(getResources().getDrawable(R.color.color_normal_gray_9),
// ScalingUtils.ScaleType.CENTER_CROP)
// .setBackground(getResources().getDrawable(R.color.color_normal_gray_9))
mDraweeHolder = DraweeHolder.create(hierarchy, context);
}
protected void init() {
if (null == mAttacher || null == mAttacher.getImageView()) {
mAttacher = new PhotoViewAttacher(this);
}
if (null != mPendingScaleType) {
setScaleType(mPendingScaleType);
mPendingScaleType = null;
}
}
@Override
protected boolean verifyDrawable(Drawable dr) {
if (dr == mDraweeHolder.getHierarchy().getTopLevelDrawable()) {
return true;
}
return false;
}
@Override
public void onStartTemporaryDetach() {
super.onStartTemporaryDetach();
mDraweeHolder.onDetach();
}
@Override
protected void onAttachedToWindow() {
init();
super.onAttachedToWindow();
mDraweeHolder.onAttach();
}
@Override
public void onFinishTemporaryDetach() {
super.onFinishTemporaryDetach();
mDraweeHolder.onAttach();
}
protected void onDetachedFromWindow() {
mAttacher.cleanup();
mDraweeHolder.onDetach();
if(copyCloseableImageReference!=null){
copyCloseableImageReference.close();
copyCloseableImageReference=null;
}
super.onDetachedFromWindow();
}
public void setImageUrl(String url, final int view_width_dp, int view_height_dp) {
ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)).setAutoRotateEnabled(true)
.setResizeOptions(new ResizeOptions(view_width_dp, view_height_dp)).build();
ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest,
this);
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setOldController(mDraweeHolder.getController()).setImageRequest(imageRequest)
.setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String s, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
//Log.e("photeview", " ########## onFinalImageSet " + s);
try {
imageReference = dataSource.getResult();
if (imageReference != null) {
copyCloseableImageReference = imageReference.clone();
CloseableBitmap closeableBitmap = (CloseableBitmap)copyCloseableImageReference.get();
if (closeableBitmap != null && closeableBitmap instanceof CloseableStaticBitmap) {
CloseableStaticBitmap closeableStaticBitmap = (CloseableStaticBitmap) closeableBitmap;
Bitmap bitmap = closeableStaticBitmap.getUnderlyingBitmap();
if (bitmap != null&!bitmap.isRecycled()) {
setImageBitmap(bitmap);
if (null != imageLoadLintener) {
imageLoadLintener.onFinalImageSet(s);
}//Log.e("photeview", " ########## bitmap.getWidth= " + bitmap.getWidth());
}else{
if (null != imageLoadLintener) {
imageLoadLintener.onFailure(s);
}
}
}
}
}finally {
dataSource.close();
CloseableReference.closeSafely(imageReference);
}
}
@Override
public void onSubmit(String id, Object callerContext) {
if (null != imageLoadLintener) {
imageLoadLintener.onSubmit(id);
}
//Log.e("photeview", " ########## onSubmit " + id);
}
@Override
public void onRelease(String id) {
//Log.e("photeview", " ########## onRelease " + id);
if (null != imageLoadLintener) {
imageLoadLintener.onRelease(id);
}
}
@Override
public void onFailure(String id, Throwable throwable) {
if (null != imageLoadLintener) {
imageLoadLintener.onFailure(id);
}
//Log.e("photeview", " ########## onFailure " + id);
}
@Override
public void onIntermediateImageSet(String id, ImageInfo imageInfo) {
//Log.e("photeview", " ########## onIntermediateImageSet " + id);
}
}).setTapToRetryEnabled(true).build();
mDraweeHolder.setController(controller);
}
/**
* @deprecated use {@link #setRotationTo(float)}
*/
@Override
public void setPhotoViewRotation(float rotationDegree) {
mAttacher.setRotationTo(rotationDegree);
}
@Override
public void setRotationTo(float rotationDegree) {
mAttacher.setRotationTo(rotationDegree);
}
@Override
public void setRotationBy(float rotationDegree) {
mAttacher.setRotationBy(rotationDegree);
}
@Override
public boolean canZoom() {
return mAttacher.canZoom();
}
@Override
public RectF getDisplayRect() {
return mAttacher.getDisplayRect();
}
@Override
public Matrix getDisplayMatrix() {
return mAttacher.getDisplayMatrix();
}
@Override
public boolean setDisplayMatrix(Matrix finalRectangle) {
return mAttacher.setDisplayMatrix(finalRectangle);
}
@Override
@Deprecated
public float getMinScale() {
return getMinimumScale();
}
@Override
public float getMinimumScale() {
return mAttacher.getMinimumScale();
}
@Override
@Deprecated
public float getMidScale() {
return getMediumScale();
}
@Override
public float getMediumScale() {
return mAttacher.getMediumScale();
}
@Override
@Deprecated
public float getMaxScale() {
return getMaximumScale();
}
@Override
public float getMaximumScale() {
return mAttacher.getMaximumScale();
}
@Override
public float getScale() {
return mAttacher.getScale();
}
@Override
public ScaleType getScaleType() {
return mAttacher.getScaleType();
}
@Override
public void setAllowParentInterceptOnEdge(boolean allow) {
mAttacher.setAllowParentInterceptOnEdge(allow);
}
@Override
@Deprecated
public void setMinScale(float minScale) {
setMinimumScale(minScale);
}
@Override
public void setMinimumScale(float minimumScale) {
mAttacher.setMinimumScale(minimumScale);
}
@Override
@Deprecated
public void setMidScale(float midScale) {
setMediumScale(midScale);
}
@Override
public void setMediumScale(float mediumScale) {
mAttacher.setMediumScale(mediumScale);
}
@Override
@Deprecated
public void setMaxScale(float maxScale) {
setMaximumScale(maxScale);
}
@Override
public void setMaximumScale(float maximumScale) {
mAttacher.setMaximumScale(maximumScale);
}
@Override
public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
mAttacher.setScaleLevels(minimumScale, mediumScale, maximumScale);
}
@Override
// setImageBitmap calls through to this method
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
if (null != mAttacher) {
mAttacher.update();
}
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
if (null != mAttacher) {
mAttacher.update();
}
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
if (null != mAttacher) {
mAttacher.update();
}
}
@Override
public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
mAttacher.setOnMatrixChangeListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener l) {
mAttacher.setOnLongClickListener(l);
}
@Override
public void setOnPhotoTapListener(OnPhotoTapListener listener) {
mAttacher.setOnPhotoTapListener(listener);
}
@Override
public OnPhotoTapListener getOnPhotoTapListener() {
return mAttacher.getOnPhotoTapListener();
}
@Override
public void setOnViewTapListener(OnViewTapListener listener) {
mAttacher.setOnViewTapListener(listener);
}
@Override
public OnViewTapListener getOnViewTapListener() {
return mAttacher.getOnViewTapListener();
}
@Override
public void setScale(float scale) {
mAttacher.setScale(scale);
}
@Override
public void setScale(float scale, boolean animate) {
mAttacher.setScale(scale, animate);
}
@Override
public void setScale(float scale, float focalX, float focalY, boolean animate) {
mAttacher.setScale(scale, focalX, focalY, animate);
}
@Override
protected void onDraw(Canvas canvas) {
try {
super.onDraw(canvas);
}catch (Exception e){
Log.e("PhotoView", " bitmap is recylce...");
if (null != imageLoadLintener) {
imageLoadLintener.onRelease("");
}
//WallPaperUtil.ToastUtils.showToast(ACContext.getContext(),getResources().getString(R.string.wallpaper_preview_failed));
}
}
@Override
public void setScaleType(ScaleType scaleType) {
if (null != mAttacher) {
mAttacher.setScaleType(scaleType);
} else {
mPendingScaleType = scaleType;
}
}
@Override
public void setZoomable(boolean zoomable) {
mAttacher.setZoomable(zoomable);
}
@Override
public Bitmap getVisibleRectangleBitmap() {
return mAttacher.getVisibleRectangleBitmap();
}
@Override
public void setZoomTransitionDuration(int milliseconds) {
mAttacher.setZoomTransitionDuration(milliseconds);
}
@Override
public IPhotoView getIPhotoViewImplementation() {
return mAttacher;
}
@Override
public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
mAttacher.setOnDoubleTapListener(newOnDoubleTapListener);
}
@Override
public void setOnScaleChangeListener(PhotoViewAttacher.OnScaleChangeListener onScaleChangeListener) {
mAttacher.setOnScaleChangeListener(onScaleChangeListener);
}
public void cleanup(){
if(mAttacher!=null){
mAttacher.cleanup();
}
if(copyCloseableImageReference!=null){
copyCloseableImageReference.close();
copyCloseableImageReference=null;
}
}
}
至此完成需求,后期测试跑monkey,发现此界面存在内容泄漏,通过mat 对hprof文件分享,发现确实存在内存泄漏。
主要以下几处(mac markdown在线编辑器上传图失败,这里只能描述下引用链了)
1.Actvity -> Gesturedetector
2.Activty —> photoView - fresco
3.Activity -> PhotoView
4.Activity -> AudioManage
解决问题
Actvity -> Gesturedetector
一开始是以为是photoview 中 onDetachedFromWindow() 中
mAttacher.cleanup();未调用成功所致,后来改为photoview销毁前,手动调用,反复测试后发现然并卵,然后开始转换查找问题方向,去代码中查看GestureDetector 初始化及构造方法,最后发现他是在 PhotoViewAttacher 这么构造出来的
mGestureDetector = new GestureDetector(imageView.getContext(),
new GestureDetector.SimpleOnGestureListener() {
// forward long click listener
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
});
最后改为
mGestureDetector = new GestureDetector(imageView.getContext().getApplicationContext(),
new GestureDetector.SimpleOnGestureListener() {
// forward long click listener
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
});
ps: 在android某些版本上前一种并不会造成泄漏,但是安全起见最好是用后一种。
2.Activty —> photoView - fresco
photoview显示图片我们是通过 从frsco中 imagePipeline 直接拷贝一份图片内存缓存,通过如下方式
final DataSource `< CloseableReference < CloseableImage
>> dataSource = imagePipeline.fetchDecodedImage(imageRequest,
this);
这个过程中把photoview作为 object 传给了frsco,造成了内存泄漏。跟着源码发现 这个object 只是作为一个标记,frsco拿它并无大用,直接改以下这张
final DataSource `< CloseableReference < CloseableImage
>> dataSource = imagePipeline.fetchDecodedImage(imageRequest,
null);
问题解决。
3.Activity -> PhotoView
这个是排查后发现是个低级错误,photoView 中某个listener进行了set,但是在activity销毁的时候,并没有针对每个photoview 进行注销listener。
4.Activity -> AudioManage
这个莫名奇妙,完全没有用到这个类,或者相关的import,最后查看引用链,发现是context 引起的泄漏,然后排查所有用到的content,最后现在这么一句,在设置壁纸时,初始化了WallpaperManager
WallpaperManager wallpaperManager = WallpaperManager.getInstance(mContext);
除了这里其他并没有引用了,所以改为这样
WallpaperManager wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
跑monkey测试发现,没有泄漏了,问题解决了。
此次monkey 测试还发现其他界面的一些内存泄漏,这里坐下总结
总结
容易引起内存泄漏的有以下一些
1. 匿名内部类
匿名内部类中做异步网络请求时,通过传入当前Activity,使用弱引用的方 式。如:
WeakReference `< WallPaperSpecialActivity` > refActivity;
public WallpaperSpecialAsyncTask(WallPaperSpecialActivity activity, String pType, int OffSet) {
this.refActivity = new WeakReference (activity);
this.OffSet = OffSet;
this.pType = pType;
}
2. 未注销的引用
3. context
4. AsyncTask
针对这些时一定要慎之又慎。