Android | 毫分缕析!说说图片加载的整个过程

简介: Android | 毫分缕析!说说图片加载的整个过程

前言


  • 最近我负责了一些相册相关的需求,在完成业务的同时,也希望对图片加载的过程有更深入的认识;
  • 在这篇文章里,我将从源码上探讨 图片加载 的过程,文章中引用的核心源码我已经进行了简化与梳理,相信能极大减低你的学习成本。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


相关文章



目录


image.png

1. 图片解码选项


public static class Options {
public Bitmap inBitmap;
public boolean inMutable;
public boolean inJustDecodeBounds;
public boolean inPremultiplied;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity; // 用不到
public boolean inScaled;
public int inSampleSize;
public int outWidth;
public int outHeight;
public Bitmap.Config outConfig;
public ColorSpace outColorSpace;
}
复制代码
public Options() {
    inScaled = true;
    inPremultiplied = true;
}
复制代码



2. 图片资源加载过程概述


现在,我们来看加载图片资源的入口方法:

BitmapFactory.java


入口方法 1
public static Bitmap decodeResource(Resources res, int id) {
    return decodeResource(res, id, null);
}
入口方法 2(已简化)
public static Bitmap decodeResource(Resources res, int id, Options opts) {
    1、匹配资源 id,打开 InputStream
    final TypedValue value = new TypedValue();
    InputStream is = res.openRawResource(id, value);
    2、解码资源,返回 Bitmap
    return decodeResourceStream(res, value, is, null, opts);
}
复制代码


可以看到,两个入口方法的区别在于是否传入Options,在 第 5 节 我会讲到 通过配置Options来自定义解码,目前我们先当opts == null

简化后的decodeResource(...)非常清晰,无非是分为两个步骤:


  • 1、匹配资源,打开 InputStream
  • 2、解码资源,返回 Bitmap

image.png


3. 步骤一:匹配资源,打开 InputStream


这个步骤主要完成 从资源 id(一个 int 值)定位到具体某一个文件夹下的资源:

ResourcesImpl.java


-> 1、匹配资源 id,打开 InputStream
最终调用到(已简化):
InputStream openRawResource(@RawRes int id, TypedValue value) {
    1.1 匹配资源
    getValue(id, value, true);
    1.2 打开输入流
    return mAssets.openNonAsset(value.assetCookie, value.string.toString(),
                    AssetManager.ACCESS_STREAMING);
}
-> 1.1 
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) {
    1.1.1 查找资源 id,并将相关信息存储在 outValue
    boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
    if (!found) {
        1.1.2 资源未找到,抛出异常
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
    }
}
复制代码

AssetManager.java

-> 1.1.1
boolean getResourceValue(@AnyRes int resId, int densityDpi, TypedValue outValue, boolean resolveRefs) {
    final int cookie = nativeGetResourceValue(mObject, resId, (short) densityDpi, outValue, resolveRefs);
    if (cookie <= 0) {
        return false;
    }
    return true;
}
复制代码

AssetManager.cpp

static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
                                   jshort density, jobject typed_value,
                                   jboolean resolve_references) {
    匹配过程略,见下图...
    return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value);
}
static jint CopyValue(JNIEnv* env, ...jobject out_typed_value) {
    ...
    if (config != nullptr) {
        关键信息:将文件夹对应的 density 保存在 TypeValue 中
        env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, config->density);
    }
    return static_cast<jint>(ApkAssetsCookieToJavaCookie(cookie));
}
复制代码


以上代码已经非常简化了,主要关注以下几点:


  • 在 1.1.1 分支,查找资源 id,并将相关信息存储在 outValue,其中比较关键的信息是:将文件夹对应的 densityDpi 保存在 TypeValue 中(这个值在下一节会用到);
  • 匹配过程比较冗长,直接看示意图:

image.png

引用自 blog.csdn.net/xuaho0907/a… —— 爱吃冰淇淋的羊驼


4. 步骤二:解码资源,返回 Bitmap


在上一步匹配资源中,我们已经获得 InputStream & TypedValue(带有文件夹对应的 densityDpi),这一步我们将对图片资源进行解码,decodeResourceStream()代码如下:


BitmapFactory.java


-> 2、解码资源,返回 Bitmap
public static Bitmap decodeResourceStream(Resources res, TypedValue value, 
InputStream is, Rect pad, Options opts) {
    if (opts == null) {
        opts = new Options();
    }
    2.1 如果未设置 inDensity,则设置为文件夹对应的 densityDpi
    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            关键点
            opts.inDensity = density;
        }
    }
    2.2 如果未设置 inTargetDensity,则设置为设备的 densityDpi
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    2.3 执行解码
    return decodeStream(is, pad, opts);
}
-> 2.3 执行解码(已简化)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    2.3.1 AssetManager 输入流(例如:/asset、/raw、/drawable)
    if (is instanceof AssetManager.AssetInputStream) {
        final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
        return nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts), Options.nativeColorSpace(opts));
    } else {
        2.3.2 其他输入流(例如 FileInputStream)
        return decodeStreamInternal(is, outPadding, opts);
    }
}
-> 2.3.2
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    一块可复用的中间内存
    byte [] tempStorage = null;
    if (opts != null) tempStorage = opts.inTempStorage;
    if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
    return nativeDecodeStream(is, tempStorage, outPadding, opts,
        Options.nativeInBitmap(opts), Options.nativeColorSpace(opts));
}
复制代码


可以看到,在执行decodeStream()之前有两个比较重要的步骤:

  • 2.1 如果未设置 inDensity,则设置为 文件夹对应的 densityDpi
  • 2.2 如果未设置 inTargetDensity,则设置为 设备的 densityDpi


到了【2.3 执行解码】,根据是否为 AssetInputStream,调用不同的 native 方法:


  • 2.3.1 AssetManager 输入流(例如:/asset、/raw、/drawable),调用nativeDecodeAsset()
  • 2.3.2 其他输入流(例如 FileInputStream),调用nativeDecodeStream()

BitmapFactory.cpp


-> 2.3.1 AssetManager 输入流
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jlong native_asset,
        jobject padding, jobject options) {
    Asset* asset = reinterpret_cast<Asset*>(native_asset);
    执行解码
    return doDecode(env, skstd::make_unique<AssetStreamAdaptor>(asset), padding, options);
}
-> 2.3.2 其他输入流
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {
    jobject bitmap = NULL;
    std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
    if (stream.get()) {
        std::unique_ptr<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded()));
        执行解码
        bitmap = doDecode(env, std::move(bufferedStream), padding, options);
    }
    return bitmap;
}
复制代码


最终都走到doDecode(),这段代码是图片解码的核心逻辑:


已简化
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject padding, jobject options) {
    1、获取 java 层 Options 对象 In 字段值
    int sampleSize;      对应于 Options#inSampleSize(默认 1)
    if (sampleSize <= 0) {
        sampleSize = 1;
    }
    bool onlyDecodeSize; 对应于 Options#inJustDecodeBounds(默认 false)
    bool isHardware;     对应于 Options#inPreferredConfig(默认 ARGB_8888)
    bool isMutable;      对应于 Options#inMutable(默认 false)
    jobject javaBitmap;  对应于 Options#inBitmap(默认 null)
    boolean inScale;     对应于 Options#inScaled(默认 true)
    int density;         对应于 Options#inDensity
    int targetDensity;   对应于 Options#inTargetDensity
    2、设置 java 层 Options 对象 out 字段初始值
    Options#outWidth = -1
    Options#outHeight = -1
    Options#outMimeType = 0
    Options#outConfig = 0
    Options#outColorSpace = 0
    3、获得 inDensity / inTargetDensity
    float scale = 1.0f;
    if (density != 0 && targetDensity != 0 && density != screenDensity) {
        scale = (float) targetDensity / density;
    }
    mutable 和 hardware 是冲突的
    if (isMutable && isHardware) {
        doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
        return nullObjectReturn("Cannot create mutable hardware bitmap");
    }
    4、根据 sampleSize 确定采样后的尺寸(size)
    SkISize size = codec->getSampledDimensions(sampleSize);
    int scaledWidth = size.width();
    int scaledHeight = size.height();
    5、确定 java 层 Options 对象 out 字段最终值
    Options#outWidth = scaledWidth 
    Options#outHeight = scaledHeight 
    Options#outMimeType = (例如 "image/png")
    Options#outConfig = (例如 ARGB_8888)
    Options#outColorSpace =(例如 RGB)
    6、返回点一:inJustDecodeBounds = true,只获取采样后的尺寸(无缩放)
    if (onlyDecodeSize) {
        return nullptr;
    }
    7、确定最终缩放到的目标尺寸(先采样,再缩放)
    bool willScale = false;
    if (scale != 1.0f) {
        willScale = true;
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    8、存在 java 层的 Options#inBitmap,做一些准备工作
    android::Bitmap* reuseBitmap = nullptr;
    if (javaBitmap != NULL) {
        reuseBitmap = &bitmap::toBitmap(env, javaBitmap);
    }
    RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
    9、采样解码得到 SkBitmap(注意:只使用了采用后尺寸)
    const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace);
    SkBitmap decodingBitmap;
    decodingBitmap.setInfo(bitmapInfo);
    decodingBitmap.tryAllocPixels(decodeAllocator)
    10、执行缩放
    SkBitmap outputBitmap;
    if (willScale) {
        10.1 根据是否有可回收的 inBitmap,确定不同的分配器
        SkBitmap::Allocator* outputAllocator;
        if (javaBitmap != nullptr) {
            outputAllocator = &recyclingAllocator;
        } else {
            outputAllocator = &defaultAllocator;
        }
        10.2 复制(注意:使用了缩放后尺寸)
        SkColorType scaledColorType = decodingBitmap.colorType();
        outputBitmap.setInfo(bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
        outputBitmap.tryAllocPixels(outputAllocator)
        10.3 利用 Canvas 进行缩放
        const float scaleX = scaledWidth / float(decodingBitmap.width());
        const float scaleY = scaledHeight / float(decodingBitmap.height());
        SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
        canvas.scale(scaleX, scaleY);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else {
        outputBitmap.swap(decodingBitmap);
    }
    11、返回点二:返回 java 层的 Options#inBitmap
    if (javaBitmap != nullptr) {
        return javaBitmap 
    }
    12、返回点三:硬件位图(from Android 8)
    if (isHardware) {
        sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap);
        return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,
                ninePatchChunk, ninePatchInsets, -1);
    }
    13、返回点四:一般方式
    return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
复制代码


提示: 整段源码非常长,阅读的过程是比较痛苦。好处是最终通过源码也发现了不少错误 / 片面的认识,也是收获颇丰。贴心的我当然都帮你整理好了,如果能帮上忙,请务必点赞加关注 ,这真的对我非常重要。

目录
相关文章
|
4天前
|
存储 缓存 算法
构建安卓应用的高效图片加载策略
【4月更文挑战第16天】在移动设备上优化用户体验的关键之一是快速而高效的图片加载。对于Android平台而言,由于设备多样性和网络环境的不稳定性,设计一个既能提升速度又能减少资源消耗的图片加载策略尤为重要。本文将深入探讨在Android应用中实现图片加载的几种技术手段,包括图片格式选择、内存缓存、磁盘缓存以及使用第三方库等,旨在为开发者提供一套综合性的解决方案,以实现在不同设备和网络环境下的高效图片加载。
|
4天前
|
API Android开发
[Android]图片加载库Glide
[Android]图片加载库Glide
60 0
|
9月前
|
JSON Java Android开发
Android 中使用Volley进行网络请求和图片加载详解
Android 中使用Volley进行网络请求和图片加载详解
219 0
Android 中使用Volley进行网络请求和图片加载详解
|
11月前
|
Android开发
Android图片加载开源库对比
Android图片加载开源库对比
72 0
Android组件化开发(三)--图片加载组件封装
今天我们来封装一个`图片加载库`:`lib_image_loader`
|
传感器 消息中间件 JavaScript
安卓开发过程中的RatingBar、Handler以及GPS在大型项目中的使用【Android】
安卓开发过程中的RatingBar、Handler以及GPS在大型项目中的使用【Android】
166 0
|
XML 存储 网络协议
【Android】使用Android开发应用过程中遇到ViewGroup的简单效以及aw和assets文件夹下的文件(Http协议的底层工作)
【Android】使用Android开发应用过程中遇到ViewGroup的简单效以及aw和assets文件夹下的文件(Http协议的底层工作)
131 0
【Android】使用Android开发应用过程中遇到ViewGroup的简单效以及aw和assets文件夹下的文件(Http协议的底层工作)
|
Java Linux Android开发
转 - Android下一次OOM调试过程
线程数超限,即proc/pid/status中记录的线程数(threads项)突破/proc/sys/kernel/threads-max中规定的最大线程数。
98 0
|
XML Java Android开发
【Android】构建安卓项目过程中的一些细节问题全记录
前言 距离安卓项目结束已经过去了好几天,之后很长一段时间我应该都不会再写和安卓有关的项目了。今天偶然翻到之前写的笔记,想了想还是决定整理出来,希望对后来要完成课设的学弟学妹们有帮助。
108 0
|
Oracle IDE Java
最详细的Android开发环境配置经验分享(包含配置过程中可能出现的问题及解决办法。繁琐的配置步骤是否是你头疼呢,详细配置步骤你值得拥有!)
最详细的Android开发环境配置经验分享(包含配置过程中可能出现的问题及解决办法。繁琐的配置步骤是否是你头疼呢,详细配置步骤你值得拥有!)
280 0
最详细的Android开发环境配置经验分享(包含配置过程中可能出现的问题及解决办法。繁琐的配置步骤是否是你头疼呢,详细配置步骤你值得拥有!)