
本文只是簡(jiǎn)要分析安卓端自帶壓縮與加載方案
1,高效加載加載大圖
展示高分辨率圖片的時(shí)候,最好先將圖片進(jìn)行壓縮。
BitmapFactory這個(gè)類(lèi)提供了多個(gè)解析方法(decodeByteArray, decodeFile, decodeResource等)用于創(chuàng)建Bitmap對(duì)象,我們應(yīng)該根據(jù)圖片的來(lái)源選擇合適的方法。比如SD卡中的圖片可以使用decodeFile方法,網(wǎng)絡(luò)上的圖片可以使用decodeStream方法,資源文件中的圖片可以使用decodeResource方法。
色彩模式->色彩模式是數(shù)字世界中表示顏色的一種算法,在Bitmap里用Config來(lái)表示。
ARGB_8888:每個(gè)像素占四個(gè)字節(jié),A、R、G、B 分量各占8位,是 Android 的默認(rèn)設(shè)置;
RGB_565:每個(gè)像素占兩個(gè)字節(jié),R分量占5位,G分量占6位,B分量占5位;
ARGB_4444:每個(gè)像素占兩個(gè)字節(jié),A、R、G、B分量各占4位,成像效果比較差;
Alpha_8: 只保存透明度,共8位,1字節(jié);
安卓自帶壓縮方案流程
1.比例大小壓縮
BitmapFactory每一種解析方法都提供了一個(gè)可選的BitmapFactory.Options參數(shù),將這個(gè)參數(shù)的inJustDecodeBounds屬性設(shè)置為true就可以讓解析方法禁止為bitmap分配內(nèi)存,返回值也不再是一個(gè)Bitmap對(duì)象,而是null。雖然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType屬性都會(huì)被賦值。這個(gè)技巧讓我們可以在加載圖片之前就獲取到圖片的長(zhǎng)寬值和MIME類(lèi)型,從而根據(jù)情況對(duì)圖片進(jìn)行壓縮。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
決定是把整張圖片加載到內(nèi)存中還是加載一個(gè)壓縮版的圖片到內(nèi)存中。以下幾個(gè)因素是我們需要考慮的:
預(yù)估一下加載整張圖片所需占用的內(nèi)存。
為了加載這一張圖片你所愿意提供多少內(nèi)存。
用于展示這張圖片的控件的實(shí)際大小。
當(dāng)前設(shè)備的屏幕尺寸和分辨率。
通過(guò)設(shè)置BitmapFactory.Options中inSampleSize的值就可以實(shí)現(xiàn)。比如我們有一張20481536像素的圖片,將inSampleSize的值設(shè)置為4,就可以把這張圖片壓縮成512384像素。原本加載這張圖片需要占用13M的內(nèi)存,壓縮后就只需要占用0.75M了(假設(shè)圖片是ARGB_8888類(lèi)型,即每個(gè)像素點(diǎn)占用4個(gè)字節(jié))。下面的方法可以根據(jù)傳入的寬和高,計(jì)算出合適的inSampleSize值:
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源圖片的高度和寬度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 計(jì)算出實(shí)際寬高和目標(biāo)寬高的比率
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
// 一定都會(huì)大于等于目標(biāo)的寬和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
BitmapFactory.Options的inJustDecodeBounds屬性設(shè)置為true,解析一次圖片。然后將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合適的inSampleSize值了。之后再解析一次圖片,使用新獲取到的inSampleSize值,并把inJustDecodeBounds設(shè)置為false,就可以得到壓縮后的圖片了。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析將inJustDecodeBounds設(shè)置為true,來(lái)獲取圖片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 調(diào)用上面定義的方法計(jì)算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用獲取到的inSampleSize值再次解析圖片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
下面的代碼非常簡(jiǎn)單地將任意一張圖片壓縮成100*100的縮略圖,并在ImageView上展示。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
2.質(zhì)量壓縮
所用方法與類(lèi)與比例壓縮基本相同
質(zhì)量壓縮:根據(jù)傳遞進(jìn)去的質(zhì)量大小,采用系統(tǒng)自帶的壓縮算法,將圖片壓縮成JPEG格式
private Bitmap compressImage(Bitmap image) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//質(zhì)量壓縮方法,這里100表示不壓縮,把壓縮后的數(shù)據(jù)存放到baos中
int options = 100;
while ( baos.toByteArray().length / 1024>100) { //循環(huán)判斷如果壓縮后圖片是否大于100kb,大于繼續(xù)壓縮
baos.reset();//重置baos即清空baos
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//這里壓縮options%,把壓縮后的數(shù)據(jù)存放到baos中
options -= 10;//每次都減少10
}
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把壓縮后的數(shù)據(jù)baos存放到ByteArrayInputStream中
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream數(shù)據(jù)生成圖片
return bitmap;
}
使用圖片壓縮庫(kù)進(jìn)行壓縮
JNI使用Jpeg庫(kù)
Android和IOS 中圖片處理使用了一個(gè)叫做skia的開(kāi)源圖形處理引擎。他位于android源碼的/external/skia 目錄。我們平時(shí)在java層使用一個(gè)圖片處理的函數(shù)實(shí)際上底層就是調(diào)用了這個(gè)開(kāi)源引擎中的相關(guān)的函數(shù)。
android在進(jìn)行jpeg壓縮編碼的時(shí)候,考慮到了效率問(wèn)題使用了定長(zhǎng)編碼方式進(jìn)行編碼(因?yàn)楫?dāng)時(shí)的手機(jī)性能都比較低),而IOS使用了變長(zhǎng)編碼的算法——哈夫曼算法。而且IOS對(duì)skia引擎也做了優(yōu)化。所有我們看到同樣的圖片在ios上壓縮會(huì)好一點(diǎn)。
1、下載開(kāi)源的libjpeg,進(jìn)行移植、編譯得到libjpeg.so
2、使用jni編寫(xiě)一個(gè)函數(shù)用來(lái)圖片壓縮
3、在函數(shù)中添加一個(gè)開(kāi)關(guān)選項(xiàng),可以讓我們選擇是否使用哈夫曼算法。
4、打包,搞成sdk供我們以后使用。
具體可查看Android使用libjpeg實(shí)現(xiàn)圖片壓縮
Luban魯班壓縮
可能是最接近微信朋友圈的圖片壓縮算法
接近微信朋友圈壓縮后的效果,具體看以下對(duì)比!
| 內(nèi)容 | 原圖 | Luban | |
|---|---|---|---|
| 截屏 720P | 720*1280,390k | 720*1280,87k | 720*1280,56k |
| 截屏 1080P | 1080*1920,2.21M | 1080*1920,104k | 1080*1920,112k |
| 拍照 13M(4:3) | 3096*4128,3.12M | 1548*2064,141k | 1548*2064,147k |
| 拍照 9.6M(16:9) | 4128*2322,4.64M | 1032*581,97k | 1032*581,74k |
| 滾動(dòng)截屏 | 1080*6433,1.56M | 1080*6433,351k | 1080*6433,482k |
github上算法邏輯的介紹拷貝過(guò)來(lái)了:
- 判斷圖片比例值,是否處于以下區(qū)間內(nèi);
- [1, 0.5625) 即圖片處于 [1:1 ~ 9:16) 比例范圍內(nèi)
- [0.5625, 0.5) 即圖片處于 [9:16 ~ 1:2) 比例范圍內(nèi)
- [0.5, 0) 即圖片處于 [1:2 ~ 1:∞) 比例范圍內(nèi)
- 判斷圖片最長(zhǎng)邊是否過(guò)邊界值;
- [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)
- 計(jì)算壓縮圖片實(shí)際邊長(zhǎng)值,以第2步計(jì)算結(jié)果為準(zhǔn),超過(guò)某個(gè)邊界值則:width / pow(2, n-1),height/pow(2, n-1)
- 計(jì)算壓縮圖片的實(shí)際文件大小,以第2、3步結(jié)果為準(zhǔn),圖片比例越大則文件越大。
size = (newW * newH) / (width * height) * m;
- [1, 0.5625) 則 width & height 對(duì)應(yīng) 1664,4990,1280 * n(n≥3),m 對(duì)應(yīng) 150,300,300;
- [0.5625, 0.5) 則 width = 1440,height = 2560, m = 200;
- [0.5, 0) 則 width = 1280,height = 1280 / scale,m = 500;注:scale為比例值
- 判斷第4步的size是否過(guò)小
- [1, 0.5625) 則最小 size 對(duì)應(yīng) 60,60,100
- [0.5625, 0.5) 則最小 size 都為 100
- [0.5, 0) 則最小 size 都為 100
- 將前面求到的值壓縮圖片 width, height, size 傳入壓縮流程,壓縮圖片直到滿足以上數(shù)值
使用圖片緩存技術(shù)
為了能夠選擇一個(gè)合適的緩存大小給LruCache, 有以下多個(gè)因素應(yīng)該放入考慮范圍內(nèi),例如:
- 你的設(shè)備可以為每個(gè)應(yīng)用程序分配多大的內(nèi)存?
- 設(shè)備屏幕上一次最多能顯示多少?gòu)垐D片?有多少圖片需要進(jìn)行預(yù)加載,因?yàn)橛锌赡芎芸煲矔?huì)顯示在屏幕上?
- 你的設(shè)備的屏幕大小和分辨率分別是多少?一個(gè)超高分辨率的設(shè)備(例如 Galaxy Nexus) 比起一個(gè)較低分辨率的設(shè)備(例如 Nexus S),在持有相同數(shù)量圖片的時(shí)候,需要更大的緩存空間。
- 圖片的尺寸和大小,還有每張圖片會(huì)占據(jù)多少內(nèi)存空間。
- 圖片被訪問(wèn)的頻率有多高?會(huì)不會(huì)有一些圖片的訪問(wèn)頻率比其它圖片要高?如果有的話,你也許應(yīng)該讓一些圖片常駐在內(nèi)存當(dāng)中,或者使用多個(gè)LruCache 對(duì)象來(lái)區(qū)分不同組的圖片。
- 你能維持好數(shù)量和質(zhì)量之間的平衡嗎?有些時(shí)候,存儲(chǔ)多個(gè)低像素的圖片,而在后臺(tái)去開(kāi)線程加載高像素的圖片會(huì)更加的有效。
下面是一個(gè)使用 LruCache 來(lái)緩存圖片的例子:
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 獲取到可用內(nèi)存的最大值,使用內(nèi)存超出這個(gè)值會(huì)引起OutOfMemory異常。
// LruCache通過(guò)構(gòu)造函數(shù)傳入緩存值,以KB為單位。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用內(nèi)存值的1/8作為緩存的大小。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重寫(xiě)此方法來(lái)衡量每張圖片的大小,默認(rèn)返回圖片數(shù)量。
return bitmap.getByteCount() / 1024;
}
};
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
在這個(gè)例子當(dāng)中,使用了系統(tǒng)分配給應(yīng)用程序的八分之一內(nèi)存來(lái)作為緩存大小。在中高配置的手機(jī)當(dāng)中,這大概會(huì)有4兆(32/8)的緩存空間。一個(gè)全屏幕的 GridView 使用4張 800x480分辨率的圖片來(lái)填充,則大概會(huì)占用1.5兆的空間(8004804)。因此,這個(gè)緩存大小可以存儲(chǔ)2.5頁(yè)的圖片。
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
}
BitmapWorkerTask 還要把新加載的圖片的鍵值對(duì)放到緩存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
// 在后臺(tái)加載圖片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}
2,加載加載長(zhǎng)圖,不壓縮
那么對(duì)于這種需求,該如何做呢?
首先不壓縮,按照原圖尺寸加載,那么屏幕肯定是不夠大的,并且考慮到內(nèi)存的情況,不可能一次性整圖加載到內(nèi)存中,所以肯定是局部加載,那么就需要用到一個(gè)類(lèi):
BitmapRegionDecoder
其次,既然屏幕顯示不完,那么最起碼要添加一個(gè)上下左右拖動(dòng)的手勢(shì),讓用戶可以拖動(dòng)查看。
BitmapRegionDecoder
BitmapRegionDecoder主要用于顯示圖片的某一塊矩形區(qū)域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
顯示指定的區(qū)域
bitmapRegionDecoder.decodeRegion(rect, options);
參數(shù)一很明顯是一個(gè)rect,參數(shù)二是BitmapFactory.Options,你可以控制圖片的inSampleSize,inPreferredConfig等。
下面列出核心代碼塊
try
{
InputStream inputStream = getAssets().open("tangyan.jpg");
//獲得圖片的寬、高
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, tmpOptions);
int width = tmpOptions.outWidth;
int height = tmpOptions.outHeight;
//設(shè)置顯示圖片的中心區(qū)域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);
} catch (IOException e)
{
e.printStackTrace();
}
上述代碼,就是使用BitmapRegionDecoder去加載assets中的圖片,調(diào)用bitmapRegionDecoder.decodeRegion解析圖片的中間矩形區(qū)域,返回bitmap,最終顯示在ImageView上。
自定義顯示大圖控件
根據(jù)上面的分析呢,我們這個(gè)自定義控件思路就非常清晰了:
- 提供一個(gè)設(shè)置圖片的入口
- 重寫(xiě)onTouchEvent,在里面根據(jù)用戶移動(dòng)的手勢(shì),去更新顯示區(qū)域的參數(shù)
- 每次更新區(qū)域參數(shù)后,調(diào)用invalidate,onDraw里面去regionDecoder.decodeRegion拿到bitmap,去draw
自定義View及示例代碼
文章參考: