qmsggg_BlogCollect
qmsggg_BlogCollect copied to clipboard
Android应用开发实践篇之媒体开发
Android媒体开发:Bitmap实践指南
文章目录
- 一 Bitmap内存管理
- 二 Bitmap质量压缩
- 2.1 实现方法
- 2.2 实现原理
- 三 Bitmap尺寸压缩
- 3.1 邻近采样
- 3.2 双线性采样
本篇文章用来介绍Android平台的图像压缩方案以及图像编解码的通识性理解,事实上Android平台对图像的处理最终都交由底层实现,篇幅有限,我们这里不会去过多的分析底层的细节实现细节,但是 我们会提一下底层的实现方案概览,给向进一步扩展的同学提供一些思路。
在介绍Bitmap之前,我们先要了解一下和压缩相关的图像的基本知识,这也可以帮助我们理解Bitmap.java里定义的一些变量的含义。
像素密度
像素密度指的是每英寸像素数目,在Bitmap里用mDensity/mTargetDensity,mDensity默认是设备屏幕的像素密度,mTargetDensity是图片的目标像素密度,在加载图片时就是 drawable 目录的像素密度。
色彩模式
色彩模式是数字世界中表示颜色的一种算法,在Bitmap里用Config来表示。
- ARGB_8888:每个像素占四个字节,A、R、G、B 分量各占8位,是 Android 的默认设置;
- RGB_565:每个像素占两个字节,R分量占5位,G分量占6位,B分量占5位;
- ARGB_4444:每个像素占两个字节,A、R、G、B分量各占4位,成像效果比较差;
- Alpha_8: 只保存透明度,共8位,1字节;
一 Bitmap内存管理
Bitmap是我们应用里使用内存的大户,很多OOM都是由于不当的图像使用造成内存过多占用而造成的,Bitmap在Android虚拟机内存存储的结构图如下所示:

从上图可以看出:
- Andrroid 3.0 以前:Bitmap存储在Native Heap中,不收GC管理,需要手动调用Bitmap的recycle()方法。
- Andrroid 3.0 以前:Bitmap存储在Java Heap中,收GC管理,无需手动调用Bitmap的recycle()方法。
如何计算Bitmap占用内存的大小呢?🤔
Bitamp 占用内存大小 = 宽度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存
👉 注:这里inDensity表示目标图片的dpi(放在哪个资源文件夹下),inTargetDensity表示目标屏幕的dpi,所以你可以发现inDensity和inTargetDensity会对Bitmap的宽高 进行拉伸,进而改变Bitmap占用内存的大小。
在Bitmap里有两个获取内存占用大小的方法。
- getByteCount():API12 加入,代表存储 Bitmap 的像素需要的最少内存。
- getAllocationByteCount():API19 加入,代表在内存中为 Bitmap 分配的内存大小,代替了 getByteCount() 方法。
在不复用 Bitmap 时,getByteCount() 和 getAllocationByteCount 返回的结果是一样的。在通过复用 Bitmap 来解码图片时,那么 getByteCount() 表示新解码图片占用内存的大 小,getAllocationByteCount() 表示被复用 Bitmap真实占用的内存大小(即 mBuffer 的长度)。
除了以上这些概念,我们再提一下Bitmap.java里的一些成员变量,这些变量大家在可能也经常遇到,要理解清楚。
- private byte[] mBuffer:图像数组,用来存储图像,这个Java层的数组实际上是在C++层创建的,下面会说明这个问题。
- private final boolean mIsMutable:图像是否是可变的,这么说有点抽象,它就像String与StringBuffer的关系一样,String是不可修改的,StringBuffer是可以修改的。
- private boolean mRecycled:图像是否已经被回收,图像的回收也是在C++层完成的。
从上面的分析可以看出,不管是在哪个Android版本是虚拟机进程所在内存大小16M这一点是没有改变的,我们要有节制的去使用内存。
可以从以下几个方面来考虑:
- 缓存图片。
- 复用图片。
- UC黑科技 - 偷用Native内存
- 图片压缩。
缓存图片
可以使用LruCache来缓存图片。
Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
mReusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable.
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later.
mReusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}
复用图片
使用BitmapFactory.Option的inBitmap标志位来复用图片。
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// If we're running on Honeycomb or newer, try to use inBitmap.
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
rivate static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
// inBitmap only works with mutable bitmaps, so force the decoder to
// return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap;
}
}
}
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
synchronized (mReusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator
= mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again.
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
}
return bitmap;
}
static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
// the new bitmap is smaller than the reusable bitmap candidate
// allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
/**
* A helper function to return the byte usage per pixel of a bitmap based on its configuration.
*/
static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}
UC黑科技 - 偷用Native内存
👉 注:由于Bitmap解码是由底层Skia库来完成的,这么做可能会有兼容性问题,但这个方法对于需要大量使用图像的App可以考虑这个方法,当然 你需要定义自己的Skia库解决兼容性问题。
public Bitmap decodeFile (String filePath){
Bitmap bitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
try {
BitmapFactory.Options.class.getField("inNativeAlloc").setBoolean(options, true);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (mFilePath != null) {
bitmap = BitmapFactory.decodeFile(mFilePath, options);
return bitmap;
}
}
接下来我们来重点分析图像压缩相关知识,知识保证图像低内存占用的重要手段。
Android平台压缩图像的手段通常有两种:
- 质量压缩
- 尺寸压缩
二 质量压缩
2.1 实现方法
质量压缩的关键在于Bitmap.compress()函数,该函数不会改变图像的大小,但是可以降低图像的质量,从而降低存储大小,进而达到压缩的目的。
compress(CompressFormat format, int quality, OutputStream stream)
它有三个参数
- CompressFormat format:压缩格式,它有JPEG、PNG、WEBP三种选择,JPEG是有损压缩,PNG是无损压缩,压缩后的图像大小不会变化(也就是没有压缩效果),WEBP是Google推出的 图像格式,它相比JPEG会节省30%左右的空间,处于兼容性和节省空间的综合考虑,我们一般会选择JPEG。
- int quality:0~100可选,数值越大,质量越高,图像越大。
- OutputStream stream:压缩后图像的输出流。
我们来写个例子验证一下。
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
, "timo_compress_quality_100.jpg");
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timo);
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(file));
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bitmap.recycle();
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
try {
if(bos != null){
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
quality = 100
1823x1076 1.16m

quality = 50
1823x1076 124.52k

quality = 0
1823x1076 35.80k

可以看到随着quality的降低,图像质量发生了明显的变化,但是图像的尺寸没有发生变化。
2.2 实现原理
Android图片的编码是由Skia库来完成的。
Skia是一个开源的二维图形库,提供各种常用的API,并可在多种软硬件平台上运行。谷歌Chrome浏览器、Chrome OS、安卓、火狐浏览器、火狐操作 系统以及其它许多产品都使用它作为图形引擎。
Skia在external/skia包中,我们虽然在平时的开发中没有直接用到Skia,但它对我们太重要了,它 是Android系统的重要组成部分,很多重要操作例如图像编解码,Canvas绘制在底层都是通过Skia来完成的。它同样被广泛用于Google的其他产品中。
Skia在src/allList包下定义了各种格式图片的编解码器。
kImageEncoder.cpp
- SkJpegEncoder.cpp:JPEG解码器
- SkPngEncoder.cpp:PNG解码器
- SkWebpEncoder.cpp:WEBP解码器
Skia本身提供了基本的画图和编解码功能,它同时还挂载了其他第三方编解码库,例如:libpng.so、libjpeg.so、libgif.so、所以我们上面想要编码成jpeg图像最终是由libjpeg来完成的。 上面也提到,我们做图像压缩,一般选择的JPEG,我们重点来看看JPEG的编解码。
libjpeg是一个完全用C语言编写的处理JPEG图像数据格式的自由库。它包含一个JPEG编解码器的算法实现,以及用于处理JPEG数据的多种实用程序。
Android并非采用原生的libjpeg,而是做了一些修改,具体说来:
- 修改了内存管理的方式
- 增加了把压缩数据输出到输出流的支持
libjpeg源码在external/jpeg包下,接下来我们具体看看JPEG压缩的实现。
我们再来从上到下看看整个源码的实现流程。
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
boolean result = nativeCompress(mNativePtr, format.nativeInt,
quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return result;
}
可以看到它在内部调用的是一个native方法nativeCompress(),这是定义在Bitmap.java里的一个函数,它的实现在Bitmap.cpp里
它最终调用的是Bitmap.cpp里的Bitmap_compress()函数,我们来看看它的实现。
static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
int format, int quality,
jobject jstream, jbyteArray jstorage) {
SkImageEncoder::Type fm;
//根据编码类型选择SkImageEncoder
switch (format) {
case kJPEG_JavaEncodeFormat:
fm = SkImageEncoder::kJPEG_Type;
break;
case kPNG_JavaEncodeFormat:
fm = SkImageEncoder::kPNG_Type;
break;
case kWEBP_JavaEncodeFormat:
fm = SkImageEncoder::kWEBP_Type;
break;
default:
return false;
}
//判断当前bitmap指针是否为空
bool success = false;
if (NULL != bitmap) {
SkAutoLockPixels alp(*bitmap);
if (NULL == bitmap->getPixels()) {
return false;
}
//创建SkWStream,用于将压缩数据输出到输出流
SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
if (NULL == strm) {
return false;
}
//根据编码类型,创建对应的编码器,对bitmap指针指向的图像数据进行压缩并输出到输出流
SkImageEncoder* encoder = SkImageEncoder::Create(fm);
if (NULL != encoder) {
//调用encodeStream进行编码
success = encoder->encodeStream(strm, *bitmap, quality);
delete encoder;
}
delete strm;
}
return success;
}
可以看到该函数根据编码格式选择SkImageEncoder,从而创建对应的图像编码器,最后 调用encodeStream(strm, *bitmap, quality)方法来完成编码。通
上面的代码创建了SkJpegEncoder,并最终调用了它里面的make()方法,如下所示:
std::unique_ptr<SkEncoder> SkJpegEncoder::Make(SkWStream* dst, const SkPixmap& src,
const Options& options) {
if (!SkPixmapIsValid(src, options.fBlendBehavior)) {
return nullptr;
}
std::unique_ptr<SkJpegEncoderMgr> encoderMgr = SkJpegEncoderMgr::Make(dst);
if (setjmp(encoderMgr->jmpBuf())) {
return nullptr;
}
if (!encoderMgr->setParams(src.info(), options)) {
return nullptr;
}
//设置压缩质量
jpeg_set_quality(encoderMgr->cinfo(), options.fQuality, TRUE);
//开始压缩
jpeg_start_compress(encoderMgr->cinfo(), TRUE);
sk_sp<SkData> icc = icc_from_color_space(src.info());
if (icc) {
// Create a contiguous block of memory with the icc signature followed by the profile.
sk_sp<SkData> markerData =
SkData::MakeUninitialized(kICCMarkerHeaderSize + icc->size());
uint8_t* ptr = (uint8_t*) markerData->writable_data();
memcpy(ptr, kICCSig, sizeof(kICCSig));
ptr += sizeof(kICCSig);
*ptr++ = 1; // This is the first marker.
*ptr++ = 1; // Out of one total markers.
memcpy(ptr, icc->data(), icc->size());
jpeg_write_marker(encoderMgr->cinfo(), kICCMarker, markerData->bytes(), markerData->size());
}
return std::unique_ptr<SkJpegEncoder>(new SkJpegEncoder(std::move(encoderMgr), src));
}
上面就是整个图像压缩的流程。
一般情况下,Android自带的libjpeg就可以满足日常的开发需求,如果业务对高质量和低存储的需求比较大,可以考虑一下以下两个库:
- libjpeg-turbo:增强版libjpeg,它是一种JPEG图像编解码器,它使用SIMD指令(MMX,SSE2,NEON,AltiVec)来加速x86,x86-64,ARM和 PowerPC系统上的基准JPEG压缩和解压缩。 在这样的系统上,libjpeg-turbo的速度通常是libjpeg的2-6倍,其他的都是相等的。 在其他类型的系统上,依靠其高度优化的Huffman编码例程,libjpeg-turbo仍然 可以胜过libjpeg。 在许多情况下,libjpeg-turbo的性能与专有的高速JPEG编解码器相媲美。
- mozilla/mozjpeg:基于libjpeg-turbo.实现,保证不降低图像质量且兼容主流编解码器的情况下进行jpeg压缩。
三 尺寸压缩
尺寸压缩本质上就是一个重新采样的过程,放大图像称为上采样,缩小图像称为下采样,Android提供了两种图像采样方法,邻近采样和双线性采样。
3.1 邻近采样
邻近采样采用邻近点插值算法,用一个像素点代替邻近的像素点,
它的实现代码大家也非常熟悉。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
+ "/timo_BitmapFactory_1.png";
ImageUtils.save(bitmap, savePath, Bitmap.CompressFormat.PNG);
inSampleSize = 1

inSampleSize = 32

可以看到这种方式的关键在于inSampleSize的选择,它决定了压缩后图像的大小。
inSampleSize代表了压缩后的图像一个像素点代表了原来的几个像素点,例如inSampleSize为4,则压缩后的图像的宽高是原来的1/4,像素点数是原来的1/16,inSampleSize 一般会选择2的指数,如果不是2的指数,内部计算的时候也会像2的指数靠近。
关于inSampleSize的计算,Luban提供了很好的思路,作者也给出了算法思路。
算法思路
1. 判断图像比例值,是否处于以下区间内;
- [1, 0.5625) 即图像处于 [1:1 ~ 9:16) 比例范围内
- [0.5625, 0.5) 即图像处于 [9:16 ~ 1:2) 比例范围内
- [0.5, 0) 即图像处于 [1:2 ~ 1:∞) 比例范围内
2. 判断图像最长边是否过边界值;
- [1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
- [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
- [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)
3. 计算压缩图像实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)
4. 计算压缩图像的实际文件大小,以第2、3步结果为准,图像比例越大则文件越大。
size = (newW * newH) / (width * height) * m;
- [1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
- [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
- [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值
5. 判断第4步的size是否过小
- [1, 0.5625) 则最小 size 对应 60,60,100
- [0.5625, 0.5) 则最小 size 都为 100
- [0.5, 0) 则最小 size 都为 100
6. 将前面求到的值压缩图像 width, height, size 传入压缩流程,压缩图像直到满足以上数值
具体实现
private int computeSize() {
int mSampleSize;
mSourceWidth = mSourceWidth % 2 == 1 ? mSourceWidth + 1 : mSourceWidth;
mSourceHeight = mSourceHeight % 2 == 1 ? mSourceHeight + 1 : mSourceHeight;
mSourceWidth = mSourceWidth > mSourceHeight ? mSourceHeight : mSourceWidth;
mSourceHeight = mSourceWidth > mSourceHeight ? mSourceWidth : mSourceHeight;
double scale = ((double) mSourceWidth / mSourceHeight);
if (scale <= 1 && scale > 0.5625) {
if (mSourceHeight < 1664) {
mSampleSize = 1;
} else if (mSourceHeight >= 1664 && mSourceHeight < 4990) {
mSampleSize = 2;
} else if (mSourceHeight >= 4990 && mSourceHeight < 10240) {
mSampleSize = 4;
} else {
mSampleSize = mSourceHeight / 1280 == 0 ? 1 : mSourceHeight / 1280;
}
} else if (scale <= 0.5625 && scale > 0.5) {
mSampleSize = mSourceHeight / 1280 == 0 ? 1 : mSourceHeight / 1280;
} else {
mSampleSize = (int) Math.ceil(mSourceHeight / (1280.0 / scale));
}
return mSampleSize;
}
核心思想就是通过对原图宽高的比较计算出合适的采样值。
同样的我们也来看看这种方式的底层实现原理,BitmapFactory里有很多decode方法,它们最终调用的是native方法。
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
Rect padding, Options opts);
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,
int length, Options opts);
这些native方法在BitmapFactory.cpp里实现,这些方法最终调用的是doDecode()方法
static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false,
bool applyScale = false, float scale = 1.0f) {
int sampleSize = 1;
//图像解码模式,这里是像素点模式
SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode;
//参数初始化
SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;
bool doDither = true;
bool isMutable = false;
bool willScale = applyScale && scale != 1.0f;
bool isPurgeable = !willScale &&
(forcePurgeable || (allowPurgeable && optionsPurgeable(env, options)));
bool preferQualityOverSpeed = false;
//javaBitmap对象
jobject javaBitmap = NULL;
//对options里的参数进行初始化
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (optionsJustBounds(env, options)) {
mode = SkImageDecoder::kDecodeBounds_Mode;
}
// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);
jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
preferQualityOverSpeed = env->GetBooleanField(options,
gOptions_preferQualityOverSpeedFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
}
if (willScale && javaBitmap != NULL) {
return nullObjectReturn("Cannot pre-scale a reused bitmap");
}
//创建图像解码器,并设置从Java层传递过来的参数,例如sampleSize、doDither等
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
if (decoder == NULL) {
return nullObjectReturn("SkImageDecoder::Factory returned null");
}
decoder->setSampleSize(sampleSize);
decoder->setDitherImage(doDither);
decoder->setPreferQualityOverSpeed(preferQualityOverSpeed);
NinePatchPeeker peeker(decoder);
//Java的像素分配器
JavaPixelAllocator javaAllocator(env);
SkBitmap* bitmap;
if (javaBitmap == NULL) {
bitmap = new SkBitmap;
} else {
if (sampleSize != 1) {
return nullObjectReturn("SkImageDecoder: Cannot reuse bitmap with sampleSize != 1");
}
bitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID);
// config of supplied bitmap overrules config set in options
prefConfig = bitmap->getConfig();
}
SkAutoTDelete<SkImageDecoder> add(decoder);
SkAutoTDelete<SkBitmap> adb(bitmap, javaBitmap == NULL);
decoder->setPeeker(&peeker);
if (!isPurgeable) {
decoder->setAllocator(&javaAllocator);
}
AutoDecoderCancel adc(options, decoder);
// To fix the race condition in case "requestCancelDecode"
// happens earlier than AutoDecoderCancel object is added
// to the gAutoDecoderCancelMutex linked list.
if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) {
return nullObjectReturn("gOptions_mCancelID");
}
SkImageDecoder::Mode decodeMode = mode;
if (isPurgeable) {
decodeMode = SkImageDecoder::kDecodeBounds_Mode;
}
//解码
SkBitmap* decoded;
if (willScale) {
decoded = new SkBitmap;
} else {
decoded = bitmap;
}
SkAutoTDelete<SkBitmap> adb2(willScale ? decoded : NULL);
if (!decoder->decode(stream, decoded, prefConfig, decodeMode, javaBitmap != NULL)) {
return nullObjectReturn("decoder->decode returned false");
}
//缩放操作
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
// 更新选项参数
if (options != NULL) {
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID,
getMimeTypeString(env, decoder->getFormat()));
}
//处于justBounds模式,不再创建Bitmap对象,直接返回,这个很熟悉吧,对应了
//options.inJustDecodeBounds = true,直解析大小,不实际加载图像
if (mode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}
jbyteArray ninePatchChunk = NULL;
if (peeker.fPatchIsValid) {
if (willScale) {
scaleNinePatchChunk(peeker.fPatch, scale);
}
size_t ninePatchArraySize = peeker.fPatch->serializedSize();
ninePatchChunk = env->NewByteArray(ninePatchArraySize);
if (ninePatchChunk == NULL) {
return nullObjectReturn("ninePatchChunk == null");
}
jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
if (array == NULL) {
return nullObjectReturn("primitive array == null");
}
peeker.fPatch->serialize(array);
env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
}
// detach bitmap from its autodeleter, since we want to own it now
adb.detach();
//处理缩放
if (willScale) {
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
//处理图像的边距
if (padding) {
if (peeker.fPatchIsValid) {
GraphicsJNI::set_jrect(env, padding,
peeker.fPatch->paddingLeft, peeker.fPatch->paddingTop,
peeker.fPatch->paddingRight, peeker.fPatch->paddingBottom);
} else {
GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
}
}
SkPixelRef* pr;
if (isPurgeable) {
pr = installPixelRef(bitmap, stream, sampleSize, doDither);
} else {
// if we get here, we're in kDecodePixels_Mode and will therefore
// already have a pixelref installed.
pr = bitmap->pixelRef();
}
if (!isMutable) {
// promise we will never change our pixels (great for sharing and pictures)
pr->setImmutable();
}
if (javaBitmap != NULL) {
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}
// 创建Bitmap对象并返回
return GraphicsJNI::createBitmap(env, bitmap, javaAllocator.getStorageObj(),
isMutable, ninePatchChunk);
}
我们发现在最后调用了createBitmap()方法来创建Bitmap对象,这个方法在Graphics.cpp里定义的,我们来看看它是如何创建Bitmap的。
jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buffer,
bool isMutable, jbyteArray ninepatch, int density)
{
SkASSERT(bitmap);
SkASSERT(bitmap->pixelRef());
//调用Java方法,创建一个对象
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
static_cast<jint>(reinterpret_cast<uintptr_t>(bitmap)),
buffer, isMutable, ninepatch, density);
hasException(env); // For the side effect of logging.
//返回Bitmap对象
return obj;
}
可以看到最终C++层调用JNI方法创建了Java层的Bitmap对象,至此,整个BitmapFactory的解码流程我们就分析完了。
3.2 双线性采样
双线性采样采用双线性插值算法,相比邻近采样简单粗暴的选择一个像素点代替其他像素点,双线性采样参考源像素相应位置周围2x2个点的值,根据相对位置取对应的权重,经过计算得到目标图像。
它的实现方式也很简单
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
Bitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true);
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/timo_BitmapFactory_1.png";
ImageUtils.save(bitmap, savePath, Bitmap.CompressFormat.PNG);
这种方式的关键在于Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)方法。
这个方法有七个参数:
- Bitmap source:源图像
- int x:目标图像第一个像素的x坐标
- int y:目标图像第一个像素的y坐标
- int width:目标图像的宽度(像素点个数)
- int height:目标图像的高度(像素点个数)
- Matrix m:变换矩阵
- boolean filter:是否开启过滤
我们来看看它的实现。
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height,
Matrix m, boolean filter) {
//参数校验
...
int neww = width;
int newh = height;
Canvas canvas = new Canvas();
Bitmap bitmap;
Paint paint;
Rect srcR = new Rect(x, y, x + width, y + height);
RectF dstR = new RectF(0, 0, width, height);
//选择图像的编码格式,和源图像保持一致
Config newConfig = Config.ARGB_8888;
final Config config = source.getConfig();
// GIF files generate null configs, assume ARGB_8888
if (config != null) {
switch (config) {
case RGB_565:
newConfig = Config.RGB_565;
break;
case ALPHA_8:
newConfig = Config.ALPHA_8;
break;
//noinspection deprecation
case ARGB_4444:
case ARGB_8888:
default:
newConfig = Config.ARGB_8888;
break;
}
}
if (m == null || m.isIdentity()) {
bitmap = createBitmap(neww, newh, newConfig, source.hasAlpha());
paint = null; // not needed
} else {
final boolean transformed = !m.rectStaysRect();
//通过Matrix变换获取新的图像宽高
RectF deviceR = new RectF();
m.mapRect(deviceR, dstR);
neww = Math.round(deviceR.width());
newh = Math.round(deviceR.height());
//传入图像参数到底层,创建爱女Bitmap对象
bitmap = createBitmap(neww, newh, transformed ? Config.ARGB_8888 : newConfig,
transformed || source.hasAlpha());
canvas.translate(-deviceR.left, -deviceR.top);
canvas.concat(m);
paint = new Paint();
paint.setFilterBitmap(filter);
if (transformed) {
paint.setAntiAlias(true);
}
}
// The new bitmap was created from a known bitmap source so assume that
// they use the same density
bitmap.mDensity = source.mDensity;
bitmap.setHasAlpha(source.hasAlpha());
bitmap.setPremultiplied(source.mRequestPremultiplied);
canvas.setBitmap(bitmap);
canvas.drawBitmap(source, srcR, dstR, paint);
canvas.setBitmap(null);
return bitmap;
}
可以看到这个方法又调用了它的同名方法createBitmap(neww, newh, transformed ? Config.ARGB_8888 : newConfig,transformed || source.hasAlpha()) 该方法当然也是借由底层的native方法实现Bitmap的创建。
private static native Bitmap nativeCreate(int[] colors, int offset,
int stride, int width, int height,
int nativeConfig, boolean mutable);
这个方法对应着Bitmap.cpp里的Bitmap_creator()方法。
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
int offset, int stride, int width, int height,
SkBitmap::Config config, jboolean isMutable) {
if (NULL != jColors) {
size_t n = env->GetArrayLength(jColors);
if (n < SkAbs32(stride) * (size_t)height) {
doThrowAIOOBE(env);
return NULL;
}
}
//SkBitmap对象
SkBitmap bitmap;
//设置图像配置信息
bitmap.setConfig(config, width, height);
//创建图像数组,这里对应着Bitmap.java里的mBuffers
jbyteArray buff = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
if (NULL == buff) {
return NULL;
}
if (jColors != NULL) {
GraphicsJNI::SetPixels(env, jColors, offset, stride,
0, 0, width, height, bitmap);
}
//创建Bitmap对象,并返回
return GraphicsJNI::createBitmap(env, new SkBitmap(bitmap), buff, isMutable, NULL);
}
可以看到上面调用allocateJavaPixelRef()方法来创建图像数组,该方法在Graphics.cpp里定义的。
jbyteArray GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
SkColorTable* ctable) {
Sk64 size64 = bitmap->getSize64();
if (size64.isNeg() || !size64.is32()) {
jniThrowException(env, "java/lang/IllegalArgumentException",
"bitmap size exceeds 32bits");
return NULL;
}
size_t size = size64.get32();
//调用Java层的方法创建一个Java数组
jbyteArray arrayObj = env->NewByteArray(size);
if (arrayObj) {
// TODO: make this work without jniGetNonMovableArrayElements
//获取数组地址
jbyte* addr = jniGetNonMovableArrayElements(&env->functions, arrayObj);
if (addr) {
SkPixelRef* pr = new AndroidPixelRef(env, (void*) addr, size, arrayObj, ctable);
bitmap->setPixelRef(pr)->unref();
// since we're already allocated, we lockPixels right away
// HeapAllocator behaves this way too
bitmap->lockPixels();
}
}
return arrayObj;
}
创建完成图像数组后,就接着调用createBitmap()创建Java层的Bitmap对象,这个我们在上面已经说过,自此Bitmap.createBitmap()方法的实现流程我们也分析完了。
以上便是Android原生支持的两种采样方式,如果这些并不能满足你的业务需求,可以考虑以下两种方式。
- 双立方/双三次采样:双立方/双三次采样使用的是双立方/双三次插值算法。邻近点插值算法的目标像素值由源图上单个像素决定,双线性內插值算法由源像素某点周围 2x2 个像素点按一定权重获得,而双立 方/双三次插值算法更进一步参考了源像素某点周围 4x4 个像素。这个算法在 Android 中并没有原生支持,如果需要使用,可以通过手动编写算法或者引用第三方算法库,这个算法在 ffmpeg 中已经给到了支持, 具体的实现在 libswscale/swscale.c 文件中:FFmpeg Scaler Documentation。
- Lanczos 采样:Lanczos 采样和 Lanczos 过滤是 Lanczos 算法的两种常见应用,它可以用作低通滤波器或者用于平滑地在采样之间插入数字信号,Lanczos 采样一般用来增加数字信号的采样率,或者间隔 采样来降低采样率。
好了,以上就是关于Android平台处理图像压缩的全部内容,下一篇文章我们来分析视频压缩的实现方案。另外phoenix项目完整的实现了图片与视频的压缩,其中图片的压缩就是用的上文提到的 Luban的算法实现,大家在做项目的时候可以做个参考。
Android媒体开发:Camera实践指南
文章目录
- 一 Camera实践指南
- 1.1 打开相机
- 1.2 关闭相机
- 1.3 开启预览
- 1.4 关闭预览
- 1.5 拍照
- 1.6 开始视频录制
- 1.7 结束视频录制
- 二 Camera2实践指南
- 2.1 打开相机
- 2.2 关闭相机
- 2.3 开启预览
- 2.4 关闭预览
- 2.5 拍照
- 2.6 开始视频录制
- 2.7 结束视频录制
Android Camera 相关API也是Android生态碎片化最为严重的一块,首先Android本身就有两套API,Android 5.0以下的Camera和Android 5.0以上的Camera2,而且 更为严重的时,各家手机厂商都Camera2的支持程度也各不相同,这就导致我们在相机开发中要花费很大精力来处理兼容性问题。
相机开发的一般流程是什么样的?🤔
- 检测并访问相机资源 检查手机是否存在相机资源,如果存在则请求访问相机资源。
- 创建预览界面,创建继承自SurfaceView并实现SurfaceHolder接口的拍摄预览类。有了拍摄预览类,即可创建一个布局文件,将预览画面与设计好的用户界面控件融合在一起,实时显示相机的预览图像。
- 设置拍照监听器,给用户界面控件绑定监听器,使其能响应用户操作, 开始拍照过程。
- 拍照并保存文件,将拍摄获得的图像转换成位图文件,最终输出保存成各种常用格式的图片。
- 释放相机资源,相机是一个共享资源,当相机使用完毕后,必须正确地将其释放,以免其它程序访问使用时发生冲突。
相机开发一般需要注意哪些问题?🤔
- 版本兼容性问题,Android 5.0以下的Camera和Android 5.0以上使用Camera2,Android 4.0以下的SurfaceView和Android 4.0以上的TextureView,Android 6.0以上要做相机等运行时权限兼容。
- 设备兼容性问题,Camera/Camera2里的各种特性在有些手机厂商的设备实现方式和支持程度是不一样的,这个需要做兼容性测试,一点点踩坑。
- 各种场景下的生命周期变化问题,最常见的是后台场景和锁屏场景,这两种场景下的相机资源的申请与释放,Surface的创建与销毁会带来一些问题,这个我们 后面会仔细分析。
关于Camera/Camear2
既然要解决这种兼容性问题,就要两套并用,那是不是根据版本来选择:Android 5.0 以下用Camera,Android 5.0以上用Camera2呢?🤔
事实上,这样是不可取的。前面说过不同手机厂商对Camera2的支持程度各不相同,即便是Android 5.0 以上的手机,也存在对Camera2支持非常差的情况,这个时候就要降级使用Camera,如何判断对Camera的支持 程度我们下面会说。
关于SurfaceView/TextureView
- SurfaceView是一个有自己Surface的View。界面渲染可以放在单独线程而不是主线程中。它更像是一个Window,自身不能做变形和动画。
- TextureView同样也有自己的Surface。但是它只能在拥有硬件加速层层的Window中绘制,它更像是一个普通View,可以做变形和动画。
更多关于SurfaceView与TextureView区别的内容可以参考这篇文章Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView.
那么如何针对版本进行方案的选择呢?🤔
官方的开源库cameraview给出了方案:

既然要两套并用,就要定义统一的接口,针对不同场景提供不同的实现,使用的时候也是根据不同的场景来创建不同的实例。
我们不难发现,这个接口一般需要定义以下功能:
- 打开相机
- 关闭相机
- 开启预览
- 关闭预览
- 拍照
- 开始视频录制
- 结束视频录制
定义好了接口,我们就有了思路,针对相机的具体特性实现相应的方案,那么另一个问题就出来了,相机在日常开发中一般作为一个SDK的形式存在供各个业务方调用,那么如何设计 出一个功能与UI相分离,高度可定制的相机SDK呢?🤔
答案就是利用Fragment,将各种点击事件(点击拍照、点击切换摄像头、点击切换闪光模式等)对应的功能封装在Fragment里,业务方在用的时候可以在Fragment之上蒙一层 UI(当然我们也需要提供默认的实现),这样就可以让功能和UI相分离,集成起来也非常的简便。
相机SDK框架图如下所示:

- CameraActivity:相机界面,主要用来实现UI的定制,实际功能(点击事件)交由CameraFragment完成。
- CameraFragment:向CameraActivity提供功能接口,完成CameraActivity里的点击事件,例如:拍照、录像等。
- CameraLifecycle:处理相机随着Activity生命周期变化的情况,内部持有CameraManager,处理相机初始化和释放,预览的创建与销毁等问题。
- CameraManager:相机的实际管理者,调用相机API来操作相机,进行拍照和录像等操作。
- Camera/Camera2:相机API。
phoenix项目最新版本已经实现了这套方案,效果图如下所示:
理解了整体的架构,我们接着就来分析针对这套架构,Camera/Camera2分别该如何实现。
一 Camera实践指南
Camera API中主要涉及以下几个关键类:
- Camera:操作和管理相机资源,支持相机资源切换,设置预览和拍摄尺寸,设置光圈、曝光等相关参数。
- SurfaceView:用于绘制相机预览图像,提供实时预览的图像。
- SurfaceHolder:用于控制Surface的一个抽象接口,它可以控制Surface的尺寸、格式与像素等,并可以监视Surface的变化。
- SurfaceHolder.Callback:用于监听Surface状态变化的接口。
SurfaceView和普通的View相比有什么区别呢?🤔
普通View都是共享一个Surface的,所有的绘制也都在UI线程中进行,因为UI线程还要处理其他逻辑,因此对View的更新速度和绘制帧率无法保证。这显然不适合相机实时 预览这种情况,因而SurfaceView持有一个单独的Surface,它负责管理这个Surface的格式、尺寸以及显示位置,它的Surface绘制也在单独的线程中进行,因而拥有更高 的绘制效率和帧率。
SurfaceHolder.Callback接口里定义了三个函数:
- surfaceCreated(SurfaceHolder holder); 当Surface第一次创建的时候调用,可以在这个方法里调用camera.open()、camera.setPreviewDisplay()来实现打开相机以及连接Camera与Surface 等操作。
- surfaceChanged(SurfaceHolder holder, int format, int width, int height); 当Surface的size、format等发生变化的时候调用,可以在这个方法里调用camera.startPreview()开启预览。
- surfaceDestroyed(SurfaceHolder holder); 当Surface被销毁的时候调用,可以在这个方法里调用camera.stopPreview(),camera.release()等方法来实现结束预览以及释放
1.1 打开相机
打开相机之前我们需要先获取系统相机的相关信息。
//有多少个摄像头
numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; ++i) {
final Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(i, cameraInfo);
//后置摄像头
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
faceBackCameraId = i;
faceBackCameraOrientation = cameraInfo.orientation;
}
//前置摄像头
else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
faceFrontCameraId = i;
faceFrontCameraOrientation = cameraInfo.orientation;
}
}
知道了相机相关信息,就可以通过相机ID打开相机了。
camera = Camera.open(cameraId);
另外,打开相机以后你会获得一个Camera对象,从这个对象里可以获取和设置相机的各种参数信息。
//获取相机参数
camera.getParameters();
//设置相机参数
camera.getParameters();
常见的参数有以下几种。
闪光灯配置参数,可以通过Parameters.getFlashMode()接口获取。
- Camera.Parameters.FLASH_MODE_AUTO 自动模式,当光线较暗时自动打开闪光灯;
- Camera.Parameters.FLASH_MODE_OFF 关闭闪光灯;
- Camera.Parameters.FLASH_MODE_ON 拍照时闪光灯;
- Camera.Parameters.FLASH_MODE_RED_EYE 闪光灯参数,防红眼模式。
对焦模式配置参数,可以通过Parameters.getFocusMode()接口获取。
- Camera.Parameters.FOCUS_MODE_AUTO 自动对焦模式,摄影小白专用模式;
- Camera.Parameters.FOCUS_MODE_FIXED 固定焦距模式,拍摄老司机模式;
- Camera.Parameters.FOCUS_MODE_EDOF 景深模式,文艺女青年最喜欢的模式;
- Camera.Parameters.FOCUS_MODE_INFINITY 远景模式,拍风景大场面的模式;
- Camera.Parameters.FOCUS_MODE_MACRO 微焦模式,拍摄小花小草小蚂蚁专用模式;
场景模式配置参数,可以通过Parameters.getSceneMode()接口获取。
- Camera.Parameters.SCENE_MODE_BARCODE 扫描条码场景,NextQRCode项目会判断并设置为这个场景;
- Camera.Parameters.SCENE_MODE_ACTION 动作场景,就是抓拍跑得飞快的运动员、汽车等场景用的;
- Camera.Parameters.SCENE_MODE_AUTO 自动选择场景;
- Camera.Parameters.SCENE_MODE_HDR 高动态对比度场景,通常用于拍摄晚霞等明暗分明的照片;
- Camera.Parameters.SCENE_MODE_NIGHT 夜间场景;
1.2 关闭相机
关闭相机很简单,只需要把相机释放掉就可以了。
camera.release();
1.3 开启预览
Camera的预览时通过SurfaceView的SurfaceHolder进行的,先通过,具体说来:
private void startPreview(SurfaceHolder surfaceHolder) {
try {
final Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(currentCameraId, cameraInfo);
int cameraRotationOffset = cameraInfo.orientation;
//获取相机参数
final Camera.Parameters parameters = camera.getParameters();
//设置对焦模式
setAutoFocus(camera, parameters);
//设置闪光模式
setFlashMode(mCameraConfigProvider.getFlashMode());
if (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_PHOTO
|| mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_UNSPECIFIED)
turnPhotoCameraFeaturesOn(camera, parameters);
else if (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_PHOTO)
turnVideoCameraFeaturesOn(camera, parameters);
final int rotation = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break; // Natural orientation
case Surface.ROTATION_90:
degrees = 90;
break; // Landscape left
case Surface.ROTATION_180:
degrees = 180;
break;// Upside down
case Surface.ROTATION_270:
degrees = 270;
break;// Landscape right
}
//根据前置与后置摄像头的不同,设置预览方向,否则会发生预览图像倒过来的情况。
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
displayRotation = (cameraRotationOffset + degrees) % 360;
displayRotation = (360 - displayRotation) % 360; // compensate
} else {
displayRotation = (cameraRotationOffset - degrees + 360) % 360;
}
this.camera.setDisplayOrientation(displayRotation);
if (Build.VERSION.SDK_INT > 13
&& (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_VIDEO
|| mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_UNSPECIFIED)) {
// parameters.setRecordingHint(true);
}
if (Build.VERSION.SDK_INT > 14
&& parameters.isVideoStabilizationSupported()
&& (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_VIDEO
|| mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_UNSPECIFIED)) {
parameters.setVideoStabilization(true);
}
//设置预览大小
parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
parameters.setPictureSize(photoSize.getWidth(), photoSize.getHeight());
//设置相机参数
camera.setParameters(parameters);
//设置surfaceHolder
camera.setPreviewDisplay(surfaceHolder);
//开启预览
camera.startPreview();
} catch (IOException error) {
Log.d(TAG, "Error setting camera preview: " + error.getMessage());
} catch (Exception ignore) {
Log.d(TAG, "Error starting camera preview: " + ignore.getMessage());
}
}
1.4 关闭预览
关闭预览很简单,直接调用camera.stopPreview()即可。
camera.stopPreview();
1.5 拍照
拍照时通过调用Camera的takePicture()方法来完成的,
takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback postview, PictureCallback jpeg)
该方法有三个参数:
- ShutterCallback shutter:在拍照的瞬间被回调,这里通常可以播放"咔嚓"这样的拍照音效。
- PictureCallback raw:返回未经压缩的图像数据。
- PictureCallback postview:返回postview类型的图像数据
- PictureCallback jpeg:返回经过JPEG压缩的图像数据。
我们一般用的就是最后一个,实现最后一个PictureCallback即可。
camera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] bytes, Camera camera) {
//存储返回的图像数据
final File pictureFile = outputPath;
if (pictureFile == null) {
Log.d(TAG, "Error creating media file, check storage permissions.");
return;
}
try {
FileOutputStream fileOutputStream = new FileOutputStream(pictureFile);
fileOutputStream.write(bytes);
fileOutputStream.close();
} catch (FileNotFoundException error) {
Log.e(TAG, "File not found: " + error.getMessage());
} catch (IOException error) {
Log.e(TAG, "Error accessing file: " + error.getMessage());
} catch (Throwable error) {
Log.e(TAG, "Error saving file: " + error.getMessage());
}
}
});
拍照完成后如果还要继续拍照则调用camera.startPreview()继续开启预览,否则关闭预览,释放相机资源。
1.6 开始视频录制
视频的录制时通过MediaRecorder来完成的。
if (prepareVideoRecorder()) {
mediaRecorder.start();
isVideoRecording = true;
uiHandler.post(new Runnable() {
@Override
public void run() {
videoListener.onVideoRecordStarted(videoSize);
}
});
}
MediaRecorder主要用来录制音频和视频,在使用之前要进行初始化和相关参数的设置,如下所示:
protected boolean preparemediaRecorder() {
mediaRecorder = new MediaRecorder();
try {
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
//输出格式
mediaRecorder.setOutputFormat(camcorderProfile.fileFormat);
//视频帧率
mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate);
//视频大小
mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight());
//视频比特率
mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate);
//视频编码器
mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec);
//音频编码率
mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate);
//音频声道
mediaRecorder.setAudioChannels(camcorderProfile.audioChannels);
//音频采样率
mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate);
//音频编码器
mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec);
File outputFile = outputPath;
String outputFilePath = outputFile.toString();
//输出路径
mediaRecorder.setOutputFile(outputFilePath);
//设置视频输出的最大尺寸
if (mCameraConfigProvider.getVideoFileSize() > 0) {
mediaRecorder.setMaxFileSize(mCameraConfigProvider.getVideoFileSize());
mediaRecorder.setOnInfoListener(this);
}
//设置视频输出的最大时长
if (mCameraConfigProvider.getVideoDuration() > 0) {
mediaRecorder.setMaxDuration(mCameraConfigProvider.getVideoDuration());
mediaRecorder.setOnInfoListener(this);
}
mediaRecorder.setOrientationHint(getVideoOrientation(mCameraConfigProvider.getSensorPosition()));
//准备
mediaRecorder.prepare();
return true;
} catch (IllegalStateException error) {
Log.e(TAG, "IllegalStateException preparing MediaRecorder: " + error.getMessage());
} catch (IOException error) {
Log.e(TAG, "IOException preparing MediaRecorder: " + error.getMessage());
} catch (Throwable error) {
Log.e(TAG, "Error during preparing MediaRecorder: " + error.getMessage());
}
releasemediaRecorder();
return false;
}
值得一提的是,日常的业务中经常对拍摄视频的时长或者大小有要求,这个可以通过mediaRecorder.setOnInfoListener()来处理,OnInfoListener会监听正在录制的视频,然后我们 可以在它的回调方法里处理。
@Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
if (MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED == what) {
//到达最大时长
} else if (MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED == what) {
//到达最大尺寸
}
}
更多关于MediaRecorder的介绍可以参考MediaRecorder官方文档。
1.7 结束视频录制
结束视频录制也很简单,只需要调用mediaRecorder.stop()方法即可。
mediaRecorder.stop();
此外,如果不再使用相机,也要注意释放相机资源。
以上便是Camera的全部内容,还是比较简单的,下面我们接着来讲Camera2的相关内容,注意体会两者的区别。
二 Camera2实践指南
Camera2 API中主要涉及以下几个关键类:
- CameraManager:摄像头管理器,用于打开和关闭系统摄像头
- CameraCharacteristics:描述摄像头的各种特性,我们可以通过CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法来获取。
- CameraDevice:描述系统摄像头,类似于早期的Camera。
- CameraCaptureSession:Session类,当需要拍照、预览等功能时,需要先创建该类的实例,然后通过该实例里的方法进行控制(例如:拍照 capture())。
- CaptureRequest:描述了一次操作请求,拍照、预览等操作都需要先传入CaptureRequest参数,具体的参数控制也是通过CameraRequest的成员变量来设置。
- CaptureResult:描述拍照完成后的结果。
Camera2拍照流程如下所示:

开发者通过创建CaptureRequest向摄像头发起Capture请求,这些请求会排成一个队列供摄像头处理,摄像头将结果包装在CaptureMetadata中返回给开发者。整个流程建立在一个CameraCaptureSession的会话中。
2.1 打开相机
打开相机之前,我们首先要获取CameraManager,然后获取相机列表,进而获取各个摄像头(主要是前置摄像头和后置摄像头)的参数。
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
try {
final String[] ids = mCameraManager.getCameraIdList();
numberOfCameras = ids.length;
for (String id : ids) {
final CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
final int orientation = characteristics.get(CameraCharacteristics.LENS_FACING);
if (orientation == CameraCharacteristics.LENS_FACING_FRONT) {
faceFrontCameraId = id;
faceFrontCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
frontCameraCharacteristics = characteristics;
} else {
faceBackCameraId = id;
faceBackCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
backCameraCharacteristics = characteristics;
}
}
} catch (Exception e) {
Log.e(TAG, "Error during camera initialize");
}
Camera2与Camera一样也有cameraId的概念,我们通过mCameraManager.getCameraIdList()来获取cameraId列表,然后通过mCameraManager.getCameraCharacteristics(id) 获取每个id对应摄像头的参数。
关于CameraCharacteristics里面的参数,主要用到的有以下几个:
- LENS_FACING:前置摄像头(LENS_FACING_FRONT)还是后置摄像头(LENS_FACING_BACK)。
- SENSOR_ORIENTATION:摄像头拍照方向。
- FLASH_INFO_AVAILABLE:是否支持闪光灯。
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL:获取当前设备支持的相机特性。
注:事实上,在各个厂商的的Android设备上,Camera2的各种特性并不都是可用的,需要通过characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)方法 来根据返回值来获取支持的级别,具体说来:
- INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬件支持,允许手动控制全高清的摄像、支持连拍模式以及其他新特性。
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支持,这个需要单独查询。
- INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有设备都会支持,也就是和过时的Camera API支持的特性是一致的。
利用这个INFO_SUPPORTED_HARDWARE_LEVEL参数,我们可以来判断是使用Camera还是使用Camera2,具体方法如下:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean hasCamera2(Context mContext) {
if (mContext == null) return false;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false;
try {
CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
String[] idList = manager.getCameraIdList();
boolean notFull = true;
if (idList.length == 0) {
notFull = false;
} else {
for (final String str : idList) {
if (str == null || str.trim().isEmpty()) {
notFull = false;
break;
}
final CameraCharacteristics characteristics = manager.getCameraCharacteristics(str);
final int supportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
notFull = false;
break;
}
}
}
return notFull;
} catch (Throwable ignore) {
return false;
}
}
更多ameraCharacteristics参数,可以参见CameraCharacteristics官方文档。
打开相机主要调用的是mCameraManager.openCamera(currentCameraId, stateCallback, backgroundHandler)方法,如你所见,它有三个参数:
- String cameraId:摄像头的唯一ID。
- CameraDevice.StateCallback callback:摄像头打开的相关回调。
- Handler handler:StateCallback需要调用的Handler,我们一般可以用当前线程的Handler。
mCameraManager.openCamera(currentCameraId, stateCallback, backgroundHandler);
上面我们提到了CameraDevice.StateCallback,它是摄像头打开的一个回调,定义了打开,关闭以及出错等各种回调方法,我们可以在 这些回调方法里做对应的操作。
private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
//获取CameraDevice
mcameraDevice = cameraDevice;
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
//关闭CameraDevice
cameraDevice.close();
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
//关闭CameraDevice
cameraDevice.close();
}
};
2.2 关闭相机
通过上面的描述,关闭就很简单了。
//关闭CameraDevice
cameraDevice.close();
2.3 开启预览
Camera2都是通过创建请求会话的方式进行调用的,具体说来:
- 调用mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)方法创建CaptureRequest,调用
- mCameraDevice.createCaptureSession()方法创建CaptureSession。
CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType)
createCaptureRequest()方法里参数templateType代表了请求类型,请求类型一共分为六种,分别为:
- TEMPLATE_PREVIEW:创建预览的请求
- TEMPLATE_STILL_CAPTURE:创建一个适合于静态图像捕获的请求,图像质量优先于帧速率。
- TEMPLATE_RECORD:创建视频录制的请求
- TEMPLATE_VIDEO_SNAPSHOT:创建视视频录制时截屏的请求
- TEMPLATE_ZERO_SHUTTER_LAG:创建一个适用于零快门延迟的请求。在不影响预览帧率的情况下最大化图像质量。
- TEMPLATE_MANUAL:创建一个基本捕获请求,这种请求中所有的自动控制都是禁用的(自动曝光,自动白平衡、自动焦点)。
createCaptureSession(@NonNull List<Surface> outputs, @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
createCaptureSession()方法一共包含三个参数:
- List<Surface> outputs:我们需要输出到的Surface列表。
- CameraCaptureSession.StateCallback callback:会话状态相关回调。
- Handler handler:callback可以有多个(来自不同线程),这个handler用来区别那个callback应该被回调,一般写当前线程的Handler即可。
关于CameraCaptureSession.StateCallback里的回调方法:
- onConfigured(@NonNull CameraCaptureSession session); 摄像头完成配置,可以处理Capture请求了。
- onConfigureFailed(@NonNull CameraCaptureSession session); 摄像头配置失败
- onReady(@NonNull CameraCaptureSession session); 摄像头处于就绪状态,当前没有请求需要处理。
- onActive(@NonNull CameraCaptureSession session); 摄像头正在处理请求。
- onClosed(@NonNull CameraCaptureSession session); 会话被关闭
- onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface); Surface准备就绪
理解了这些东西,创建预览请求就十分简单了。
previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(workingSurface);
//注意这里除了预览的Surface,我们还添加了imageReader.getSurface()它就是负责拍照完成后用来获取数据的
mCameraDevice.createCaptureSession(Arrays.asList(workingSurface, imageReader.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
cameraCaptureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler);
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Log.d(TAG, "Fail while starting preview: ");
}
}, null);
可以发现,在onConfigured()里调用了cameraCaptureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler),这样我们就可以 持续的进行预览了。
注:上面我们说了添加了imageReader.getSurface()它就是负责拍照完成后用来获取数据,具体操作就是为ImageReader设置一个OnImageAvailableListener,然后在它的onImageAvailable() 方法里获取。
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//当图片可得到的时候获取图片并保存
mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
}
};
2.4 关闭预览
关闭预览就是关闭当前预览的会话,结合上面开启预览的内容,具体实现如下:
if (captureSession != null) {
captureSession.close();
try {
captureSession.abortCaptures();
} catch (Exception ignore) {
} finally {
captureSession = null;
}
}
2.5 拍照
拍照具体来说分为三步:
- 对焦
try {
//相机对焦
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);
//修改状态
previewState = STATE_WAITING_LOCK;
//发送对焦请求
captureSession.capture(previewRequestBuilder.build(), captureCallback, backgroundHandler);
} catch (Exception ignore) {
}
我们定义了一个CameraCaptureSession.CaptureCallback来处理对焦请求返回的结果。
private CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult) {
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
//等待对焦
final Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) {
//对焦失败,直接拍照
captureStillPicture();
} else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState
|| CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState
|| CaptureResult.CONTROL_AF_STATE_INACTIVE == afState
|| CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN == afState) {
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
previewState = STATE_PICTURE_TAKEN;
//对焦完成,进行拍照
captureStillPicture();
} else {
runPreCaptureSequence();
}
}
}
};
- 拍照
我们定义了一个captureStillPicture()来进行拍照。
private void captureStillPicture() {
try {
if (null == mCameraDevice) {
return;
}
//构建用来拍照的CaptureRequest
final CaptureRequest.Builder captureBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(imageReader.getSurface());
//使用相同的AR和AF模式作为预览
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//设置方向
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getPhotoOrientation(mCameraConfigProvider.getSensorPosition()));
//创建会话
CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
Log.d(TAG, "onCaptureCompleted: ");
}
};
//停止连续取景
captureSession.stopRepeating();
//捕获照片
captureSession.capture(captureBuilder.build(), CaptureCallback, null);
} catch (CameraAccessException e) {
Log.e(TAG, "Error during capturing picture");
}
}
- 取消对焦
拍完照片后,我们还要解锁相机焦点,让相机恢复到预览状态。
try {
//重置自动对焦
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
captureSession.capture(previewRequestBuilder.build(), captureCallback, backgroundHandler);
//相机恢复正常的预览状态
previewState = STATE_PREVIEW;
//打开连续取景模式
captureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler);
} catch (Exception e) {
Log.e(TAG, "Error during focus unlocking");
}
2.6 开始视频录制
//先关闭预览,因为需要添加一个预览输出的Surface,也就是mediaRecorder.getSurface()
closePreviewSession();
//初始化MediaRecorder,设置相关参数
if (preparemediaRecorder()) {
final SurfaceTexture texture = Camera2Manager.this.texture;
texture.setDefaultBufferSize(videoSize.getWidth(), videoSize.getHeight());
try {
//构建视频录制aptureRequest
previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
final List<Surface> surfaces = new ArrayList<>();
//设置预览Surface
final Surface previewSurface = workingSurface;
surfaces.add(previewSurface);
previewRequestBuilder.addTarget(previewSurface);
//设置预览输出Surface
workingSurface = mediaRecorder.getSurface();
surfaces.add(workingSurface);
previewRequestBuilder.addTarget(workingSurface);
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
captureSession = cameraCaptureSession;
previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
try {
//持续发送Capture请求,实现实时预览。
captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
} catch (Exception e) {
}
try {
//开始录像
mediaRecorder.start();
} catch (Exception ignore) {
Log.e(TAG, "mediaRecorder.start(): ", ignore);
}
isVideoRecording = true;
uiHandler.post(new Runnable() {
@Override
public void run() {
cameraVideoListener.onVideoRecordStarted(videoSize);
}
});
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Log.d(TAG, "onConfigureFailed");
}
}, backgroundHandler);
} catch (Exception e) {
Log.e(TAG, "startVideoRecord: ", e);
}
}
关于MediaRecorder上面讲Camera的时候我们就已经说过,这里不再赘述。
以上便是视频录制的全部内容,就是简单的API使用,还是比较简单的。
2.7 结束视频录制
结束视频录制主要也是关闭会话以及释放一些资源,具体说来:
- 关闭预览会话
- 停止mediaRecorder
- 释放mediaRecorder
//关闭预览会话
if (captureSession != null) {
captureSession.close();
try {
captureSession.abortCaptures();
} catch (Exception ignore) {
} finally {
captureSession = null;
}
}
//停止mediaRecorder
if (mediaRecorder != null) {
try {
mediaRecorder.stop();
} catch (Exception ignore) {
}
}
//释放mediaRecorder
try {
if (mediaRecorder != null) {
mediaRecorder.reset();
mediaRecorder.release();
}
} catch (Exception ignore) {
} finally {
mediaRecorder = null;
}
以上便是Camera/Camera2实践的相关内容,更多关于图像、视频处理的内容可以参见phoenix项目。