来源:得物技术
目录
一、背景
二、图片拉伸优化
1. 本地图拉伸
2. .9图拉伸
3. 网络图FIT_CENTER拉伸
4. 网络图CENTER_CROP拉伸
三、本地相册加载优化
四、动图缓存、闪烁优化
五、未来展望
六、总结
一
背景
图片的加载体验对于交易、社区为核心的社交电商类应用来说可以说是生命线,好的图片加载体验决定了用户分享欲、购买欲。
二
图片拉伸优化




本地图拉伸

private void updatePaint() { if (mLastBitmap == null || mLastBitmap.get() != mBitmap) { mLastBitmap = new WeakReference<>(mBitmap); mPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); mIsShaderTransformDirty = true; } if (mIsShaderTransformDirty) { mPaint.getShader().setLocalMatrix(mTransform); mIsShaderTransformDirty = false; } mPaint.setFilterBitmap(getPaintFilterBitmap());}



//设置path的核心代码if (mIsCircle) {mPath.addCircle(mRootBounds.centerX(),mRootBounds.centerY(),Math.min(mRootBounds.width(), mRootBounds.height()) / 2,Path.Direction.CW);} else if (mScaleDownInsideBorders) {if (mInsideBorderRadii == null) {mInsideBorderRadii = new float[8];}for (int i = 0; i < mBorderRadii.length; i++) {mInsideBorderRadii[i] = mCornerRadii[i] - mBorderWidth;}mPath.addRoundRect(mRootBounds, mInsideBorderRadii, Path.Direction.CW);} else {mPath.addRoundRect(mRootBounds, mCornerRadii, Path.Direction.CW);}//设置RootBounds的核心代码if (mTransformCallback != null) {mTransformCallback.getTransform(mParentTransform);mTransformCallback.getRootBounds(mRootBounds);} else {mParentTransform.reset();mRootBounds.set(getBounds());}


targetDensity = 手机设备 √(水平像素数^2 + 垂直像素数^2)/ 屏幕尺寸(以英寸为单位) = 560
sdensity = 本地图存放的drawable文件夹对应的density,xxhdpi = 480
android DisplayMetrics源码
mdpi --> DisplayMetrics.DENSITY_MEDIUM --> 160
hdpi --> DisplayMetrics.DENSITY_HIGH --> 240
xhdpi --> DisplayMetrics.DENSITY_XHIGH --> 320
xxhdpi --> DisplayMetrics.DENSITY_XXHIGH --> 480
xxxhdpi --> DisplayMetrics.DENSITY_XXXHIGH --> 640

.9图拉伸

@JvmStatic fun loadNinePatchBackground(image: ImageView, url: String) { //获取.9图的cdn下载文件 loadImageAsFile(url, { //通过BitmapFactory进行解码 获取bitmap val bitmap = BitmapFactory.decodeFile(it.path) ?: return@loadImageAsFile val chunk = bitmap.ninePatchChunk image.resources?.apply { //将bitmap数据封装到NinePatchDrawable中 val defaultDrawable = if (NinePatch.isNinePatchChunk(chunk)) { NinePatchDrawable(this, bitmap, chunk, Rect(), null) } else { BitmapDrawable(this, bitmap) } //设置核心图片背景 image.background = defaultDrawable } })}





问题.9图仍旧为xxhdpi的标准图片BitmapDensity为480。
网络图取决于手机实际分辨率情况,问题设备上为640。



对于问题图片来说,inDensity为480,而inTargetDensity则为 DisplayMetrics.DENSITY_DEVICE_STABLE的值。
* density 对应本地图的匹配density,默认为0,不进行inDensity配置。对于部分可变分辨率手机,可能会存在.9拉伸的问题,需要指定输入输出density。* 参数说明:* mdpi--> DisplayMetrics.DENSITY_MEDIUM* hdpi --> DisplayMetrics.DENSITY_HIGH* xhdpi -->DisplayMetrics.DENSITY_XHIGH* xxhdpi -->DisplayMetrics.DENSITY_XXHIGH* xxxhdpi -->DisplayMetrics.DENSITY_XXXHIGHfun loadNinePatchBackgroundForView(view: View, url: String, density: Int = 0) {loadImageAsFile(url, {var options: BitmapFactory.Options? = nulloptions = BitmapFactory.Options()options.inDensity = densityoptions.inTargetDensity = DisplayMetrics.DENSITY_DEVICE_STABLEBitmapFactory.decodeFile(it.path)val bitmap = BitmapFactory.decodeFile(it.path, options) ?: return@loadImageAsFileval chunk = bitmap.ninePatchChunkview.resources?.apply {val defaultDrawable = if (NinePatch.isNinePatchChunk(chunk)) {val rect = if (DuImageGlobalConfig.enableNewNinePatchLoad) {Rect(chunk[12].toInt(), chunk[20].toInt(), chunk[16].toInt(), chunk[24].toInt())}NinePatchDrawable(this, bitmap, chunk, rect, null)} else {BitmapDrawable(this, bitmap)}view.background = defaultDrawable}})}
网络图FIT_CENTER拉伸





网络图CENTER_CROP拉伸






//重写测量宽高的实现public int getIntrinsicHeight() {Bitmap bitmap = this.mBitmap;if (bitmap != null) {return bitmap.getHeight();}return super.getIntrinsicHeight();}public int getIntrinsicWidth() {Bitmap bitmap = this.mBitmap;if (bitmap != null) {return bitmap.getWidth();}return super.getIntrinsicWidth();}
三
本地相册加载优化

缩略图获取


左图为android Q及以上,右图为android Q及以下


旋转参数兼容

//cursor 获取具体的pathNamefinal String pathname = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));if (pathname != null) {try {//通过ExifInterface获取图片的旋转角度ExifInterface exif = new ExifInterface(pathname);orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);rotationAngle = JfifUtil.getAutoRotateAngleFromOrientation(orientation);} catch (IOException ioe) {FLog.e(PRODUCER_NAME, ioe, "Unable to retrieve thumbnail rotation for %s", pathname);}}..... //此处省略部分代码实现//构造CloseableStaticBitmap时,传入旋转的角度与方向CloseableStaticBitmap closeableStaticBitmap =new CloseableStaticBitmap(loadThumbnail,SimpleBitmapReleaser.getInstance(),ImmutableQualityInfo.FULL_QUALITY,rotationAngle, orientation)
异常读取兜底


try {loadThumbnail = mContentResolver.loadThumbnail(imageRequest.getSourceUri(), new Size(imageRequest.getPreferredWidth(), imageRequest.getPreferredHeight()), cancellationSignal);} catch (IOException exception) {if (exception.getMessage() != null && exception.getMessage().contains("Failed to create thumbnail")) {if (imageRequest.getMimeType() != null && HEIF_MIME_TYPE.contains(imageRequest.getMimeType())) {String path = getRealPathFromUri(mContentResolver, imageRequest.getSourceUri());if (path != null) {File file = new File(path);FileInputStream fileInputStream = new FileInputStream(file);//读取头文件 做实际的判断String type = ImageFormatChecker.getInstance().determineImageFormat(fileInputStream).getFileExtension();//暂时只判断heif, avif格式的误判 暂时没做头文件 format检测if (type != null && !type.equals(HEIF_FORMAT_EXTENSION)) {loadThumbnail = createImageThumbnail(file, new Size(imageRequest.getPreferredWidth(), imageRequest.getPreferredHeight()), cancellationSignal);if (loadThumbnail != null) {//补充异常兜底事件抛到业务层producerContext.getProducerListener().onProducerEvent(producerContext, PRODUCER_NAME, THUMBNAIL_FALL_BACK_EVENT);}}}} else {//此处抛出异常 会被onProducerFailed 处理 上层能监控到throw exception;}}}
视频缩略图兼容 & 大图压缩
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { loadThumbnail = mContentResolver.loadThumbnail(imageRequest.getSourceUri(), new Size(imageRequest.getPreferredWidth(), imageRequest.getPreferredHeight()), null);} else { long videoId = -1; try { videoId = ContentUris.parseId(imageRequest.getSourceUri()); } catch (Exception e) { FLog.w(PRODUCER_NAME, imageRequest.getSourceUri().toString() + " find exception " + e.getLocalizedMessage()); } if (videoId > 0) { loadThumbnail = MediaStore.Video.Thumbnails.getThumbnail( mContentResolver, videoId, calculateKind(imageRequest), null ); }}if (imageRequest.isResizingThumbnail() && imageRequest.getResizeOptions() != null) {if (bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) return;ResizeOptions resizeOptions = imageRequest.getResizeOptions();int resizeWidth = resizeOptions.width;int resizeHeight = resizeOptions.height;if (resizeWidth <= 0 || resizeHeight <= 0 ) return;//计算缩略图的宽高比float radio = (float) bitmap.getWidth() / bitmap.getHeight();float tempWidth = resizeWidth;float tempHeight = resizeHeight;// 如果 resize的宽高比 大于 实际原图的话,那就需要对图片做调整if (tempWidth / tempHeight > radio) {resizeHeight = (int) (tempWidth / radio);} else {resizeWidth = (int) (tempHeight * radio);}bitmap = Bitmap.createScaledBitmap(bitmap, resizeWidth, resizeHeight, true);}
四
动图缓存、闪烁优化




private void prepareFrame(final int frameNumber) { // 先从缓存中取 BitmapFrameCache mBitmapFrameCache = mAnimationBackend.mBitmapFrameCache; CloseableReference<Bitmap> bitmapReference = mBitmapFrameCache.getCachedFrame(frameNumber); if (bitmapReference == null || !bitmapReference.isValid()) { // 缓存中没有,new Bitmap并保存 try { bitmapReference = mAnimationBackend.mPlatformBitmapFactory.createBitmap(mAnimationBackend.getIntrinsicWidth(), mAnimationBackend.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); } catch (OutOfMemoryError e) { //埋点上报逻辑 } // 新建出来的对象也不可用 丢弃当前帧 if (bitmapReference == null || !bitmapReference.isValid()) { return; } Bitmap bitmap = bitmapReference.get(); if (bitmap.isRecycled()) { return; } mAnimationBackend.mAnimatedImageCompositor.renderFrame(frameNumber, bitmap); // 动图不再写入内存缓存 mBitmapFrameCache.onFrameRendered(frameNumber, bitmapReference, BitmapAnimationBackend.FRAME_TYPE_CACHED); } mAnimationBackend.mCurrentFrame.put(frameNumber, bitmapReference);}动图cacheKey重写





public boolean equals(Object obj) {if (this == obj) {return true;}if (obj == null || getClass() != obj.getClass()) {return false;}AnimationFrameCacheKey that = (AnimationFrameCacheKey) obj;//equals 仅对比mAnimationUriStringreturn mAnimationUriString.equals(that.mAnimationUriString);}public int hashCode() {//hashCode 仅对比mAnimationUriString的hashCode 即 anim:// + 图片URI hashcodereturn mAnimationUriString.hashCode();}
private BitmapFrameCache createBitmapFrameCache(AnimatedImageResult animatedImageResult) { int cacheKeyHash; if (DuImageGlobalConfig.isEnableAnimatedCompareOnlyUri() && animatedImageResult.getSourceUri() != null && !animatedImageResult.getSourceUri().isEmpty()) { //动图cacheKeyHash 仅采用SourceUri cacheKeyHash = animatedImageResult.getSourceUri().hashCode(); } else { //原有逻辑 cacheKeyHash = animatedImageResult.hashCode(); } return new FrescoFrameCache(new AnimatedFrameCache(new AnimationFrameCacheKey(cacheKeyHash, DuImageGlobalConfig.isEnableNewAnimatedCache()), mBackingCache), DuImageGlobalConfig.isEnableNewAnimatedCache());}支持重用帧配置

// 先从内存缓存中取 BitmapFrameCache mBitmapFrameCache = mAnimationBackend.mBitmapFrameCache; CloseableReference<Bitmap> bitmapReference = mBitmapFrameCache.getCachedFrame(frameNumber); if (DuImageGlobalConfig.isEnableNewAnimatedCache() && (bitmapReference == null || !bitmapReference.isValid())) { //从无引用的缓存中在尝试取一次 bitmapReference = mBitmapFrameCache.getBitmapToReuseForFrame(frameNumber, mAnimationBackend.getIntrinsicWidth(), mAnimationBackend.getIntrinsicHeight()); //获取到后重新写入内存缓存 mBitmapFrameCache.onFramePrepared(frameNumber, bitmapReference, BitmapAnimationBackend.FRAME_TYPE_CACHED);}
优化前

优化后
绑定view加载Tag
//当前view的tag 与 加载url不相等if(view.tag != image.url){ view.loadwith(image.url).apply view.tag=image.url}五
未来展望
六
总结
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70027824/viewspace-3015201/ ,如需转载,请注明出处,否则将追究法律责任。