RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

史上最全的圖片壓縮方法總結(jié)

新機(jī)器視覺 ? 來源:CSDN技術(shù)社區(qū) ? 作者:Mr.Louis ? 2022-07-30 14:46 ? 次閱讀

前言

最近在研究圖片壓縮原理,看了大量資料,從上層尺寸壓縮、質(zhì)量壓縮原理到下層的哈夫曼壓縮,走成華大道,然后去二仙橋,全看了個(gè)遍,今天就來總結(jié)總結(jié),做個(gè)技術(shù)分享,下面的內(nèi)容可能會(huì)顛覆你對圖片壓縮的認(rèn)知。

圖片基礎(chǔ)知識(shí)

首先帶著幾個(gè)疑問來看這一小節(jié):


1、位深和色深有什么區(qū)別,他們是一個(gè)東西嗎?


2、為什么Bitmap不能直接保存,Bitmap和PNG、JPG到底是什么關(guān)系?


3、圖片占用的內(nèi)存大小公式:圖片分辨率 * 每個(gè)像素點(diǎn)大小,這種說法正確嗎?

4、為什么有時(shí)候同一個(gè) app,app >內(nèi)的同個(gè)界面上的同張圖片,但在不同設(shè)備上所耗內(nèi)存卻不一樣?


5、同一張圖片,在界面上顯示的控件大小不同時(shí),它的內(nèi)存大小也會(huì)跟隨著改變嗎?

ARGB介紹


ARGB顏色模型:最常見的顏色模型,設(shè)備相關(guān),四種通道,取值均為[0,255],即轉(zhuǎn)化成二進(jìn)制位0000 0000 ~ 1111 1111。


A:Alpha (透明度) R:Red (紅) G:Green (綠) B:Blue (藍(lán))


Bitmap概念


Bitmap對象本質(zhì)是一張圖片的內(nèi)容在手機(jī)內(nèi)存中的表達(dá)形式。它將圖片的內(nèi)容看做是由存儲(chǔ)數(shù)據(jù)的有限個(gè)像素點(diǎn)組成;每個(gè)像素點(diǎn)存儲(chǔ)該像素點(diǎn)位置的ARGB值。每個(gè)像素點(diǎn)的ARGB值確定下來,這張圖片的內(nèi)容就相應(yīng)地確定下來了。

色彩模式


Bitmap.Config是Bitmap的一個(gè)枚舉內(nèi)部類,它表示的就是每個(gè)像素點(diǎn)對ARGB通道值的存儲(chǔ)方案。取值有以下四種:

ALPHA_8:每個(gè)像素占8位(1個(gè)字節(jié)),存儲(chǔ)透明度信息,沒有顏色信息。

RGB_565:沒有透明度,R=5,G=6,B=5,,那么一個(gè)像素點(diǎn)占5+6+5=16位(2字節(jié)),能表示2^16種顏色。

ARGB_4444:由4個(gè)4位組成,即A=4,R=4,G=4,B=4,那么一個(gè)像素點(diǎn)占4+4+4+4=16位 (2字節(jié)),能表示2^16種顏色。


ARGB_8888:由4個(gè)8位組成,即A=8,R=8,G=8,B=8,那么一個(gè)像素點(diǎn)占8+8+8+8=32位(4字節(jié)),能表示2^24種顏色。


位深與色深

在windows上查看一張圖片的信息會(huì)發(fā)現(xiàn)有位深度這個(gè)東西,但沒看到有色深:

78a35b0c-0e7a-11ed-ba43-dac502259ad0.png ?


這里介紹一下位深與色深的概念:

色深:顧名思義,就是"色彩的深度",指是每一個(gè)像素點(diǎn)用多少bit來存儲(chǔ)ARGB值,屬于圖片自身的一種屬性。色深可以用來衡量一張圖片的色彩處理能力(即色彩豐富程度)。


典型的色深是8-bit、16-bit、24-bit和32-bit等。上述的Bitmap.Config參數(shù)的值指的就是色深。比如ARGB_8888方式的色深為32位,RGB_565方式的色深是16位。色深是數(shù)字圖像參數(shù)。



位深度是指在記錄數(shù)字圖像的顏色時(shí),計(jì)算機(jī)實(shí)際上是用每個(gè)像素需要的二進(jìn)制數(shù)值位數(shù)來表示的。當(dāng)這些數(shù)據(jù)按照一定的編排方式被記錄在計(jì)算機(jī)中,就構(gòu)成了一個(gè)數(shù)字圖像的計(jì)算機(jī)文件。每一個(gè)像素在計(jì)算機(jī)中所使用的這種位數(shù)就是“位深度”,位深是物理硬件參數(shù),主要用來存儲(chǔ)。


舉個(gè)例子:某張圖片100像素*100像素 色深32位(ARGB_8888),保存時(shí)位深度為24位,那么:

  • 該圖片在內(nèi)存中所占大小為:100 * 100 * (32 / 8) Byte
  • 在文件中所占大小為 100 * 100 * ( 24/ 8 ) * 壓縮率 Byte

拓展小知識(shí)


24位顏色可稱之為真彩色,色深度是24,它能組合成2的24次冪種顏色,即:16777216種顏色,超過了人眼能夠分辨的顏色數(shù)量。


內(nèi)存中Bitmap的大小



網(wǎng)上很多文章都會(huì)介紹說,計(jì)算一張圖片占用的內(nèi)存大小公式:分辨率 * 每個(gè)像素點(diǎn)的大小,但事實(shí)真的如此嗎?

我們都知道我們的手機(jī)屏幕有著一定的分辨率(如:1920×1080),圖像也有自己的像素(如拍攝圖像的分辨率為4032×3024)。 如果將一張1920×1080的圖片加載鋪滿1920×1080的屏幕上這就是最合適的了,此時(shí)顯示效果最好。 如果將一張4032×3024的圖像放到1920×1080的屏幕并不會(huì)得到更好的顯示效果(和1920×1080的圖像顯示效果是一致的),反而會(huì)浪費(fèi)更多的內(nèi)存,如果按ARGB_8888來顯示的話,需要48MB的內(nèi)存空間(404830364 bytes),這么大的內(nèi)存消耗極易引發(fā)OOM,后面我們會(huì)講到針對大圖加載的內(nèi)存優(yōu)化,在這里不過多介紹。 Android 原生的 Bitmap操作中,圖片來源是res內(nèi)的不同資源目錄時(shí),圖片被加載進(jìn)內(nèi)存時(shí)的分辨率會(huì)經(jīng)過一層轉(zhuǎn)換,所以,雖然最終圖片大小的計(jì)算公式仍舊是分辨率*像素點(diǎn)大小,但此時(shí)的分辨率已不是圖片本身的分辨率了。

詳細(xì)請看字節(jié)跳動(dòng)面試官:一張圖片占據(jù)的內(nèi)存大小是如何計(jì)算,規(guī)則如下:



新分辨率 = 原圖橫向分辨率 * (設(shè)備的 dpi / 目錄對應(yīng)的 dpi ) * 原圖縱向分辨率 * (設(shè)備的 dpi / 目錄對應(yīng)的 dpi )。 當(dāng)使用 Glide時(shí),如果有設(shè)置圖片顯示的控件,那么會(huì)自動(dòng)按照控件的大小,降低圖片的分辨率加載。圖片來源是res 的分辨率轉(zhuǎn)換規(guī)則對它也無效。

當(dāng)使用 fresco 時(shí),不管圖片來源是哪里,即使是res,圖片占用的內(nèi)存大小仍舊以原圖的分辨率計(jì)算。 其他圖片的來源,如磁盤,文件,流等,均按照原圖的分辨率來進(jìn)行計(jì)算圖片的內(nèi)存大小。 那么如何計(jì)算Bitmap占用的內(nèi)存?

來看BitmapFactory.decodeResource()的源碼:


	BitmapFactory.java publicstaticBitmapdecodeResourceStream(Resourcesres,TypedValuevalue,InputStreamis,Rectpad,Optionsopts){ if(opts==null){ opts=newOptions(); } if(opts.inDensity==0&&value!=null){ finalintdensity=value.density; if(density==TypedValue.DENSITY_DEFAULT){ //inDensity默認(rèn)為圖片所在文件夾對應(yīng)的密度 opts.inDensity=DisplayMetrics.DENSITY_DEFAULT; }elseif(density!=TypedValue.DENSITY_NONE){ opts.inDensity=density; } } if(opts.inTargetDensity==0&&res!=null){ //inTargetDensity為當(dāng)前系統(tǒng)密度。 opts.inTargetDensity=res.getDisplayMetrics().densityDpi; } returndecodeStream(is,pad,opts); }  BitmapFactory.cpp 此處只列出主要代碼。 staticjobjectdoDecode(JNIEnv*env,SkStreamRewindable*stream,jobjectpadding,jobjectoptions){ //初始縮放系數(shù) floatscale=1.0f; if(env->GetBooleanField(options,gOptions_scaledFieldID)){ constintdensity=env->GetIntField(options,gOptions_densityFieldID); constinttargetDensity=env->GetIntField(options,gOptions_targetDensityFieldID); constintscreenDensity=env->GetIntField(options,gOptions_screenDensityFieldID); if(density!=0&&targetDensity!=0&&density!=screenDensity){ //縮放系數(shù)是當(dāng)前系數(shù)密度/圖片所在文件夾對應(yīng)的密度; scale=(float)targetDensity/density; } } //原始解碼出來的Bitmap; SkBitmapdecodingBitmap; if(decoder->decode(stream,&decodingBitmap,prefColorType,decodeMode) !=SkImageDecoder::kSuccess){ returnnullObjectReturn("decoder->decodereturnedfalse"); } //原始解碼出來的Bitmap的寬高; intscaledWidth=decodingBitmap.width(); intscaledHeight=decodingBitmap.height(); //要使用縮放系數(shù)進(jìn)行縮放,縮放后的寬高; if(willScale&&decodeMode!=SkImageDecoder::kDecodeBounds_Mode){ scaledWidth=int(scaledWidth*scale+0.5f); scaledHeight=int(scaledHeight*scale+0.5f); } //源碼解釋為因?yàn)闅v史原因;sx、sy基本等于scale。 constfloatsx=scaledWidth/float(decodingBitmap.width()); constfloatsy=scaledHeight/float(decodingBitmap.height()); canvas.scale(sx,sy); canvas.drawARGB(0x00,0x00,0x00,0x00); canvas.drawBitmap(decodingBitmap,0.0f,0.0f,&paint); //nowcreatethejavabitmap returnGraphicsJNI::createBitmap(env,javaAllocator.getStorageObjAndReset(), bitmapCreateFlags,ninePatchChunk,ninePatchInsets,-1); }
	

Android中圖片壓縮的方法介紹

在 Android 中進(jìn)行圖片壓縮是非常常見的開發(fā)場景,主要的壓縮方法有兩種:其一是質(zhì)量壓縮,其二是下采樣壓縮。 前者是在不改變圖片尺寸的情況下,改變圖片的存儲(chǔ)體積,而后者則是降低圖像尺寸,達(dá)到相同目的。

質(zhì)量壓縮

在Android中,對圖片進(jìn)行質(zhì)量壓縮,通常我們的實(shí)現(xiàn)方式如下所示:

	ByteArrayOutputStreamoutputStream=newByteArrayOutputStream(); //quality為0~100,0表示最小體積,100表示最高質(zhì)量,對應(yīng)體積也是最大 bitmap.compress(Bitmap.CompressFormat.JPEG,quality,outputStream);

在上述代碼中,我們選擇的壓縮格式是CompressFormat.JPEG,除此之外還有兩個(gè)選擇: 其一,CompressFormat.PNG,PNG格式是無損的,它無法再進(jìn)行質(zhì)量壓縮,quality這個(gè)參數(shù)就沒有作用了,會(huì)被忽略,所以最后圖片保存成的文件大小不會(huì)有變化; 其二,CompressFormat.WEBP,這個(gè)格式是google推出的圖片格式,它會(huì)比JPEG更加省空間,經(jīng)過實(shí)測大概可以優(yōu)化30%左右。 在某些應(yīng)用場景需要bitmap轉(zhuǎn)換成ByteArrayOutputStream,需要根據(jù)你要壓縮的圖片格式來判斷使用CompressFormat.PNG還是Bitmap.CompressFormat.JPEG,這時(shí)候quality為100。 Android質(zhì)量壓縮邏輯,函數(shù)compress經(jīng)過一連串的java層調(diào)用之后,最后來到了一個(gè)native函數(shù),如下:


	//Bitmap.cpp staticjbooleanBitmap_compress(JNIEnv*env,jobjectclazz,jlongbitmapHandle, jintformat,jintquality, jobjectjstream,jbyteArrayjstorage){ LocalScopedBitmapbitmap(bitmapHandle); SkImageEncoder::Typefm; switch(format){ casekJPEG_JavaEncodeFormat: fm=SkImageEncoder::kJPEG_Type; break; casekPNG_JavaEncodeFormat: fm=SkImageEncoder::kPNG_Type; break; casekWEBP_JavaEncodeFormat: fm=SkImageEncoder::kWEBP_Type; break; default: returnJNI_FALSE; } if(!bitmap.valid()){ returnJNI_FALSE; } boolsuccess=false; std::unique_ptrstrm(CreateJavaOutputStreamAdaptor(env,jstream,jstorage)); if(!strm.get()){ returnJNI_FALSE; } std::unique_ptrencoder(SkImageEncoder::Create(fm)); if(encoder.get()){ SkBitmapskbitmap; bitmap->getSkBitmap(&skbitmap); success=encoder->encodeStream(strm.get(),skbitmap,quality); } returnsuccess?JNI_TRUE:JNI_FALSE; }  


可以看到最后調(diào)用了函數(shù)encoder->encodeStream(…)編碼保存本地。該函數(shù)是調(diào)用skia引擎來對圖片進(jìn)行編碼壓縮,對skia的介紹將在后文講解。


尺寸壓縮 鄰近采樣(Nearest Neighbour Resampling)



	BitmapFactory.Optionsoptions=newBitmapFactory.Options(); //或者inDensity搭配inTargetDensity使用,算法和inSampleSize一樣 options.inSampleSize=2;//設(shè)置圖片的縮放比例(寬和高) , google推薦用2的倍數(shù): Bitmapbitmap=BitmapFactory.decodeFile("xxx.png"); Bitmapcompress=BitmapFactory.decodeFile("xxx.png",options);  


在這里著重講一下這個(gè)inSampleSize。從字面上理解,它的含義是: “設(shè)置取樣大小”。它的作用是:設(shè)置inSampleSize的值(int類型)后,假如設(shè)為4,則寬和高都為原來的1/4,寬高都減少了,自然內(nèi)存也降低了。 參考Google官方文檔的解釋,我們從中可以看到 x(x 為 2 的倍數(shù))個(gè)像素最后對應(yīng)一個(gè)像素,由于采樣率設(shè)置為 1/2,所以是兩個(gè)像素生成一個(gè)像素。


鄰近采樣的方式比較粗暴,直接選擇其中的一個(gè)像素作為生成像素,另一個(gè)像素直接拋棄,這樣就造成了圖片變成了純綠色,也就是紅色像素被拋棄。 鄰近采樣采用的算法叫做鄰近點(diǎn)插值算法。

雙線性采樣(Bilinear Resampling)


雙線性采樣(Bilinear Resampling)在 Android 中的使用方式一般有兩種:


	Bitmapbitmap=BitmapFactory.decodeFile("xxx.png"); Bitmapcompress=Bitmap.createScaledBitmap(bitmap,bitmap.getWidth()/2,bitmap.getHeight()/2,true); 或者直接使用matrix進(jìn)行縮放 Bitmapbitmap=BitmapFactory.decodeFile("xxx.png"); Matrixmatrix=newMatrix(); matrix.setScale(0.5f,0.5f); bm=Bitmap.createBitmap(bitmap,0,0,bit.getWidth(),bit.getHeight(),matrix,true);  


看源碼可以知道createScaledBitmap函數(shù)最終也是使用第二種方式的matrix進(jìn)行縮放,雙線性采樣使用的是雙線性內(nèi)插值算法,這個(gè)算法不像鄰近點(diǎn)插值算法一樣,直接粗暴的選擇一個(gè)像素,而是參考了源像素相應(yīng)位置周圍2x2個(gè)點(diǎn)的值,根據(jù)相對位置取對應(yīng)的權(quán)重,經(jīng)過計(jì)算之后得到目標(biāo)圖像。 雙線性內(nèi)插值算法在圖像的縮放處理中具有抗鋸齒功能, 是最簡單和常見的圖像縮放算法,當(dāng)對相鄰2x2個(gè)像素點(diǎn)采用雙線性內(nèi)插值算法時(shí),所得表面在鄰域處是吻合的,但斜率不吻合,并且雙線性內(nèi)插值算法的平滑作用可能使得圖像的細(xì)節(jié)產(chǎn)生退化,這種現(xiàn)象在上采樣時(shí)尤其明顯。 雙線性采樣對比鄰近采樣的優(yōu)勢在于: 它的系數(shù)可以是小數(shù),而不一定是整數(shù),在某些壓縮限制下,效果尤為明顯處理文字比較多的圖片在展示效果上的差別,雙線性采樣效果要更好還有雙三次采樣和**Lanczos **采樣等,具體分析可以參考 Android 中圖片壓縮分析(下)這篇QQ音樂大佬的分享。


小節(jié)總結(jié)


在 Android 中,前兩種采樣方法根據(jù)實(shí)際情況去選擇即可,如果對時(shí)間要求不高,傾向于使用雙線性采樣去縮放圖片。如果對圖片質(zhì)量要求很高,雙線性采樣也已經(jīng)無法滿足要求,則可以考慮引入另外幾種算法去處理圖片,但是同時(shí)需要注意的是后面兩種算法使用的都是卷積核去計(jì)算生成像素,計(jì)算量會(huì)相對比較大,Lanczos的計(jì)算量則是最大,在實(shí)際開發(fā)過程中根據(jù)需求進(jìn)行算法的選擇即可,往往我們是尺寸壓縮和質(zhì)量壓縮搭配來使用。 下面我們要進(jìn)入到實(shí)戰(zhàn)中,參考一個(gè)仿微信朋友圈壓縮策略的Android圖片壓縮工具——Luban,進(jìn)入我們的下一章節(jié)魯班壓縮算法解析。

魯班壓縮的背景

魯班壓縮 —— Android圖片壓縮工具,仿微信朋友圈壓縮策略。 目前做App開發(fā)總繞不開圖片這個(gè)元素。但是隨著手機(jī)拍照分辨率的提升,圖片的壓縮成為一個(gè)很重要的問題,隨便一張圖片都是好幾M,甚至幾十M,這樣的照片加載到app,可想而知,隨便加載幾張圖片,手機(jī)內(nèi)存就不夠用了,自然而然就造成了OOM ,所以,Android的圖片壓縮異常重要。 單純對圖片進(jìn)行裁切,壓縮已經(jīng)有很多文章介紹。但是裁切成多少,壓縮成多少卻很難控制好,裁切過頭圖片太小,質(zhì)量壓縮過頭則顯示效果太差。于是自然想到App巨頭——微信會(huì)是怎么處理,Luban(魯班)就是通過在微信朋友圈發(fā)送近100張不同分辨率圖片,對比原圖與微信壓縮后的圖片逆向推算出來的壓縮算法。


效果與對比


因?yàn)槭悄嫦蛲扑?,效果還沒法跟微信一模一樣,但是已經(jīng)很接近微信朋友圈壓縮后的效果,具體看以下對比! 78bc6304-0e7a-11ed-ba43-dac502259ad0.png

Luban算法解析

微信的算法解析


第一步進(jìn)行采樣率壓縮; 第二步進(jìn)行寬高的等比例壓縮(微信對原圖和縮略圖限制了最大長寬或者最小長寬); 第三步就是對圖片的質(zhì)量進(jìn)行壓縮(一般75或者70); 第四步就是采用webP的格式。 經(jīng)過這四部的處理,基本上和微信朋友圈的效果一致,包括文件大小和顯示效果


Luban的算法解析


Luban壓縮目前的步驟只占了微信算法中的第二與第三步,算法邏輯如下: 判斷圖片比例值,是否處于以下區(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)

簡單解釋一下:獲取圖片的比例系數(shù),如果在區(qū)間 [1, 0.5625) 中即圖片處于 [1:1 ~ 9:16)比例范圍內(nèi),比例以此類推,如果這個(gè)系數(shù)小于0.5,那么就給它放到 [1:2 ~ 1:∞)比例范圍內(nèi)。 判斷圖片最長邊是否過邊界值。

  • [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)

步驟二:上去一看一臉懵,1664是什么,n是什么,pow又是什么。。。這寫的估計(jì)只有作者自己能看懂了。其實(shí)就是判斷圖片最長邊是否過邊界值,此邊界值是模仿微信的一個(gè)經(jīng)驗(yàn)值,就是說1664、4990都是經(jīng)驗(yàn)值,模仿微信的策略。 至于n,是返回的是options.inSampleSize的值,就是采樣壓縮的系數(shù),是int型,Google建議是2的倍數(shù),所以為了配合這個(gè)建議,代碼中出現(xiàn)了小于10240返回的是4這種操作。最后說一下pow,其實(shí)是(長邊/1280), 這個(gè)1280也是個(gè)經(jīng)驗(yàn)值,逆向推出來的,解釋到這里邏輯也清晰了。真是坑啊啊,哈哈哈 計(jì)算壓縮圖片實(shí)際邊長值,以第2步計(jì)算結(jié)果為準(zhǔn),超過某個(gè)邊界值則:

  • width / pow(2, n-1)
  • height/ pow(2, n-1)

步驟三:這個(gè)感覺沒什么用,還是計(jì)算壓縮圖片實(shí)際邊長值,人家也說了,以第2步計(jì)算結(jié)果為準(zhǔn),其實(shí)就是晃你的,乍一看 ,這么多步驟,哈哈哈哈,唬你呢! 計(jì)算壓縮圖片的實(shí)際文件大小,以第2、3步結(jié)果為準(zhǔn),圖片比例越大則文件越大。 size = (newW * newH) / (width * height) * m;

  • [1, 0.5625) 則 width & height 對應(yīng) 1664,4990,1280 * n(n≥3),m 對應(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為比例值

步驟四:這個(gè)感覺也沒什么用,這個(gè)m應(yīng)該是壓縮比。但整個(gè)過程就是驗(yàn)證一下壓縮完之后,size的大小,是否超過了你的預(yù)期,如果超過了你的預(yù)期,將進(jìn)行重復(fù)壓縮。 判斷第4步的size是否過小。

  • [1, 0.5625) 則最小 size 對應(yīng) 60,60,100
  • [0.5625, 0.5) 則最小 size 都為 100
  • [0.5, 0) 則最小 size 都為 100

步驟五:這一步也沒啥用,也是為了后面循環(huán)壓縮使用。這個(gè)size就是上面計(jì)算出來的,最小 size 對應(yīng)的值公式為:size = (newW * newH) / (width * height) * m,對應(yīng)的三個(gè)值,就是上面根據(jù)圖片的比例分成的三組,然后計(jì)算出來的。 將前面求到的值壓縮圖片 width, height, size 傳入壓縮流程,壓縮圖片直到滿足以上數(shù)值。 最后一步也沒啥用,看字就知道是為了循環(huán)壓縮,或許是微信也這樣做?既然你已經(jīng)有了預(yù)期,為什么不根據(jù)預(yù)期直接一步到位呢?但是裁剪的系數(shù)和壓縮的系數(shù)怎么調(diào)整會(huì)達(dá)到最優(yōu)一個(gè)效果,我的項(xiàng)目中已經(jīng)對此功能進(jìn)行了增加,目前還在內(nèi)測,沒有開源,后期穩(wěn)定后會(huì)開源給大家使用。



將算法帶入到開源代碼中



咱們直接看算法所在類 Engine.java:


	//計(jì)算采樣壓縮的值,也就是模仿微信的經(jīng)驗(yàn)值,核心內(nèi)容 privateintcomputeSize(){ //補(bǔ)齊寬度和長度 srcWidth=srcWidth%2==1?srcWidth+1:srcWidth; srcHeight=srcHeight%2==1?srcHeight+1:srcHeight; //獲取長邊和短邊 intlongSide=Math.max(srcWidth,srcHeight); intshortSide=Math.min(srcWidth,srcHeight); //獲取圖片的比例系數(shù),如果在區(qū)間[1,0.5625)中即圖片處于[1:1~9:16)比例 floatscale=((float)shortSide/longSide); //開始判斷圖片處于那種比例中,就是上面所說的第一個(gè)步驟 if(scale<=?1&&scale>0.5625){ //判斷圖片最長邊是否過邊界值,此邊界值是模仿微信的一個(gè)經(jīng)驗(yàn)值,就是上面所說的第二個(gè)步驟 if(longSide1664){ //返回的是options.inSampleSize的值,就是采樣壓縮的系數(shù),是int型,Google建議是2的倍數(shù) return1; }elseif(longSide4990){ return2; //這個(gè)10240上面的邏輯沒有提到,也是經(jīng)驗(yàn)值,不用去管它,你可以隨意調(diào)整 }elseif(longSide>4990&&longSide10240){ return4; }else{ returnlongSide/1280==0?1:longSide/1280; } //這些判斷都是逆向推導(dǎo)的經(jīng)驗(yàn)值,也可以說是一種策略 }elseif(scale<=?0.5625&&scale>0.5){ returnlongSide/1280==0?1:longSide/1280; }else{ //此時(shí)圖片的比例是一個(gè)長圖,采用策略向上取整 return(int)Math.ceil(longSide/(1280.0/scale)); } } //圖片旋轉(zhuǎn)方法 privateBitmaprotatingImage(Bitmapbitmap,intangle){ Matrixmatrix=newMatrix(); //將傳入的bitmap進(jìn)行角度旋轉(zhuǎn) matrix.postRotate(angle); //返回一個(gè)新的bitmap returnBitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true); } //壓縮方法,返回一個(gè)File Filecompress()throwsIOException{ //創(chuàng)建一個(gè)option對象 BitmapFactory.Optionsoptions=newBitmapFactory.Options(); //獲取采樣壓縮的值 options.inSampleSize=computeSize(); //把圖片進(jìn)行采樣壓縮后放入一個(gè)bitmap,參數(shù)1是bitmap圖片的格式,前面獲取的 BitmaptagBitmap=BitmapFactory.decodeStream(srcImg.open(),null,options); //創(chuàng)建一個(gè)輸出流的對象 ByteArrayOutputStreamstream=newByteArrayOutputStream(); //判斷是否是JPG圖片 if(Checker.SINGLE.isJPG(srcImg.open())){ //Checker.SINGLE.getOrientation這個(gè)方法是檢測圖片是否被旋轉(zhuǎn)過,對圖片進(jìn)行矯正 tagBitmap=rotatingImage(tagBitmap,Checker.SINGLE.getOrientation(srcImg.open())); } //對圖片進(jìn)行質(zhì)量壓縮,參數(shù)1:通過是否有透明通道來判斷是PNG格式還是JPG格式, //參數(shù)2:壓縮質(zhì)量固定為60,參數(shù)3:壓縮完后將bitmap寫入到字節(jié)流中 tagBitmap.compress(focusAlpha?Bitmap.CompressFormat.PNG:Bitmap.CompressFormat.JPEG,60,stream); //bitmap用完回收掉 tagBitmap.recycle(); //將圖片流寫入到File中,然后刷新緩沖區(qū),關(guān)閉文件流和Byte流 FileOutputStreamfos=newFileOutputStream(tagImg); fos.write(stream.toByteArray()); fos.flush(); fos.close(); stream.close(); returntagImg; }
	

Luban原框架問題分析

原框架問題分析

  • 解碼前沒有對內(nèi)存做出預(yù)判
  • 質(zhì)量壓縮寫死 60
  • 沒有提供圖片輸出格式選擇
  • 不支持多文件合理并行壓縮,輸出順序和壓縮順序不能保證一致
  • 檢測文件格式和圖像的角度多次重復(fù)創(chuàng)建InputStream,增加不必要開銷,增加OOM風(fēng)險(xiǎn)
  • 可能出現(xiàn)內(nèi)存泄漏,需要自己合理處理生命周期
  • 圖片要是有大小限制,只能進(jìn)行重復(fù)壓縮
  • 原框架用的還是RxJava1.0

技術(shù)改造方案

  • 解碼前利用獲取的圖片寬高對內(nèi)存占用做出計(jì)算,超出內(nèi)存的使用RGB-565嘗試解碼
  • 針對質(zhì)量壓縮的時(shí)候,提供傳入質(zhì)量系數(shù)的接口
  • 對圖片輸出支持多種格式,不局限于File
  • 利用協(xié)程來實(shí)現(xiàn)異步壓縮和并行壓縮任務(wù),可以在合適時(shí)機(jī)取消協(xié)程來終止任務(wù)
  • 參考Glide對字節(jié)數(shù)組的復(fù)用,以及InputStream的mark()、reset()來優(yōu)化重復(fù)打開開銷
  • 利用LiveData來實(shí)現(xiàn)監(jiān)聽,自動(dòng)注銷監(jiān)聽。
  • 壓縮前計(jì)算好大小,逆向推導(dǎo)出尺寸壓縮系數(shù)和質(zhì)量壓縮系數(shù)
  • 現(xiàn)在已經(jīng)出了RxJava3和協(xié)程,但大多數(shù)項(xiàng)目中已經(jīng)有了線程池,要利用項(xiàng)目中的線程池,而不是導(dǎo)入一個(gè)三方庫就建一個(gè)線程池而造成資源浪費(fèi)

小結(jié)


Luban壓縮當(dāng)初出來的時(shí)候號(hào)稱 "可能是最接近微信朋友圈的圖片壓縮算法" ,但這個(gè)庫已經(jīng)三四年沒有維護(hù)了,隨著產(chǎn)品的迭代微信已經(jīng)也不是當(dāng)初的那個(gè)微信了,Luban壓縮的庫也要進(jìn)行更新了。所以為了適應(yīng)現(xiàn)在的項(xiàng)目,我之后會(huì)根據(jù)上面的技術(shù)改造方案對圖片壓縮出一個(gè)船新版本的庫,更為強(qiáng)大。


Luban還有一個(gè)turbo分支,這個(gè)分支主要是為了兼容Android 7.0以前的系統(tǒng)版本,導(dǎo)入libjpeg-turbo的jni版本。 libjpeg-turbo是一個(gè)C語音編寫的高效JPEG圖像處理庫,Android系統(tǒng)在7.0版本之前內(nèi)部使用的是libjpeg非turbo版,并且為了性能關(guān)閉了Huffman編碼。在7.0之后的系統(tǒng)內(nèi)部使用了libjpeg-turbo庫并且啟用Huffman編碼。 那么什么是Huffman編碼呢?前面提到的skio引擎又是什么東西呢? / 底層哈夫曼壓縮講解 / 在前面的Android圖片壓縮必備基礎(chǔ)知識(shí)中,提到的Skia是Android的重要組成部分。在魯班壓縮算法解析中提到哈夫曼壓縮,那么他們之間到底是什么關(guān)系呢?


Android Skia 圖像引擎


Skia 是一個(gè)2D向量圖形處理函數(shù)庫,2005年被Google收購后并自己維護(hù)的 c++ 實(shí)現(xiàn)的圖像引擎,實(shí)現(xiàn)了各種圖像處理功能,并且廣泛地應(yīng)用于谷歌自己和其它公司的產(chǎn)品中(如:Chrome、Firefox、 Android等),基于它可以很方便為操作系統(tǒng)、瀏覽器等開發(fā)圖像處理功能。 Skia 在 Android 中提供了基本的畫圖和簡單的編解碼功能,可以掛接其他的第三方編碼解碼庫或者硬件編解碼庫,例如libpng 和 libjpeg ,libgif等等。因此,這個(gè)函數(shù)調(diào)用bitmap.compress(Bitmap.CompressFormat.JPEG...),實(shí)際會(huì)調(diào)用 libjpeg.so動(dòng)態(tài)庫進(jìn)行編碼壓縮。



最終Android編碼保存圖片的邏輯是Java層函數(shù)→Native函數(shù)→Skia函數(shù)→對應(yīng)第三庫函數(shù)(例如libjpeg)。所以skia就像一個(gè) 膠水層,用來鏈接各種第三方編解碼庫,不過Android也會(huì)對這些庫做一些修改,比如修改內(nèi)存管理的方式等等。 Android 在之前從某種程度來說使用的算是 libjpeg 的功能閹割版,壓縮圖片默認(rèn)使用的是 standard huffman,而不是 optimized huffman,也就是說使用的是默認(rèn)的哈夫曼表,并沒有根據(jù)實(shí)際圖片去計(jì)算相對應(yīng)的哈夫曼表,Google 在初期考慮到手機(jī)的性能瓶頸,計(jì)算圖片權(quán)重這個(gè)階段非常占用 CPU 資源的同時(shí)也非常耗時(shí),因?yàn)榇藭r(shí)需要計(jì)算圖片所有像素 argb 的權(quán)重,這也是 Android 的圖片壓縮率對比 iOS 來說差了一些的原因之一。

圖像壓縮與Huffman算法

這里簡單介紹一下哈夫曼算法,哈夫曼算法是在多媒體處理里常用的算法之一。比如一個(gè)文件中可能會(huì)出現(xiàn)五個(gè)值 a,b,c,d,e,它們用二進(jìn)制表達(dá)是:


	a.1010b.1011c.1100d.1101e.1110  我們可以看到,最前面的一位數(shù)字是 1,其實(shí)是浪費(fèi)掉了,在定長算法下最優(yōu)的表達(dá)式為: 



	a.010b.011c.100d.101e.110  這樣我們就能做到節(jié)省一位的損耗,那哈夫曼算法比起定長算法改進(jìn)的地方在哪里呢?在哈夫曼算法中我們可以給信息賦予權(quán)重,即為信息加權(quán)重,假設(shè) a 占據(jù)了 60%,b 占據(jù)了 20%, c 占據(jù)了 20%,d,e 都是 0%: 


	a:010(60%)b:011(20%)c:100(20%)d:101(0%)e:110(0%)  在這種情況下,我們可以使用哈夫曼樹算法再次優(yōu)化為: 


	a:1b:01c:00  所以思路當(dāng)然就是出現(xiàn)頻率高的字母使用短碼,對出現(xiàn)頻率低的使用長碼,不出現(xiàn)的直接就去掉,最后 abcde 的哈夫曼編碼就對應(yīng):1 01 00 定長編碼下的abcde:010 011 100 101 110, 使用 哈夫曼樹 加權(quán)重后的 編碼則為 1 01 00,這就是哈夫曼算法的整體思路(關(guān)于算法的詳細(xì)介紹可以參考哈夫曼樹及編碼講解及例題)。 所以這個(gè)算法一個(gè)很重要的思路是必須知道每一個(gè)元素出現(xiàn)的權(quán)重,如果我們能夠知道每一個(gè)元素的權(quán)重,那么就能夠根據(jù)權(quán)重動(dòng)態(tài)生成一個(gè)最優(yōu)的哈夫曼表。 但是怎么去獲取每一個(gè)元素,對于圖片就是每一個(gè)像素中 argb 的權(quán)重呢,只能去循環(huán)整個(gè)圖片的像素信息,這無疑是非常消耗性能的,所以早期 android 就使用了默認(rèn)的哈夫曼表進(jìn)行圖片壓縮。
	

libjpeg與optimize_coding

libjpeg在壓縮圖像時(shí),有一個(gè)參數(shù)叫optimize_coding,關(guān)于這個(gè)參數(shù),libjpeg.doc有如下解釋:


	TRUEcausesthecompressortocomputeoptimalHuffmancodingtables fortheimage.Thisrequiresanextrapassoverthedataand thereforecostsagooddealofspaceandtime.Thedefaultis FALSE,whichtellsthecompressortousethesuppliedordefault Huffmantables.Inmostcasesoptimaltablessaveonlyafewpercent offilesizecomparedtothedefaulttables.Notethatwhenthisis TRUE,youneednotsupplyHuffmantablesatall,andanyyoudo supplywillbeoverwritten.  

由上可知,如果設(shè)置optimize_coding 為TRUE,將會(huì)使得壓縮圖像過程中,會(huì)先基于圖像數(shù)據(jù)計(jì)算哈弗曼表。由于這個(gè)計(jì)算會(huì)顯著消耗空間和時(shí)間,默認(rèn)值被設(shè)置為FALSE。 那么optimize_coding參數(shù)的影響究竟會(huì)有多大呢?Skia的官方人員經(jīng)過實(shí)際測試,分別設(shè)置optimize_coding=TRUE 和 FALSE 進(jìn)行壓縮,發(fā)現(xiàn)FALSE時(shí)的圖片大小大約是 TRUE時(shí)的2倍+。換言之就是相同文件體積的圖片,不使用哈夫曼編碼圖片質(zhì)量會(huì)比使用哈夫曼低2倍+。 從Android 7.0版本開始,optimize_code標(biāo)示已經(jīng)設(shè)置為了TRUE,也就是默認(rèn)使用圖像生成哈夫曼表,而不是使用默認(rèn)哈夫曼表。 以上內(nèi)容借鑒了Android中圖片壓縮分析(上)中的內(nèi)容,自認(rèn)為不能比他寫的更好,感謝QQ音樂技術(shù)團(tuán)隊(duì),如有冒犯,請立即聯(lián)系刪除。


Android 中圖片壓縮分析(上):

https://cloud.tencent.com/developer/article/1006307

手寫JPEG圖像處理引擎

我們都知道bitmap是在native層被創(chuàng)建的,在Bitmap.cpp文件中,創(chuàng)建的bitmap其實(shí)是創(chuàng)建了一個(gè)SKBitmap的對象,交給了skia引擎去處理。導(dǎo)入jpeglib.h的頭文件會(huì)需要其他的.h頭文件,具體如下:

78c6ca10-0e7a-11ed-ba43-dac502259ad0.png ?


然后開始擼代碼,照著安卓源碼中l(wèi)ibjpeg-turbo庫里的example.c文件(系統(tǒng)提供的例子),開始編寫native-lib.cpp文件:



	#include #include #include #include #include//因?yàn)轭^文件都是c文件,咱們寫的是.cpp是C++文件,這時(shí)候就需要混編,所以加入下面關(guān)鍵字 extern"C" { #include"jpeglib.h" } #defineLOGE(...)__android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) #defineLOG_TAG"louis" #definetrue1 typedefuint8_tBYTE; //寫入圖片函數(shù) voidwriteImg(BYTE*data,constchar*path,intw,inth){ //信使:java與C溝通的橋梁,jpeg的結(jié)構(gòu)體,保存的比如寬、高、位深、圖片格式等信息 structjpeg_compress_structjpeg_struct; //設(shè)置錯(cuò)誤處理信息當(dāng)讀完整個(gè)文件的時(shí)候就會(huì)回調(diào)my_error_exit,例如內(nèi)置卡出錯(cuò)、沒權(quán)限等 jpeg_error_mgrerr; jpeg_struct.err=jpeg_std_error(&err); //給結(jié)構(gòu)體分配內(nèi)存 jpeg_create_compress(&jpeg_struct); //打開輸出文件 FILE*file=fopen(path,"wb"); //設(shè)置輸出路徑 jpeg_stdio_dest(&jpeg_struct,file); jpeg_struct.image_width=w; jpeg_struct.image_height=h; //初始化初始化 //改成FALSE---》開啟hufuman算法 jpeg_struct.arith_code=FALSE; //是否采用哈弗曼表數(shù)據(jù)計(jì)算品質(zhì)相差2倍多,官方實(shí)測,吹5-10倍的都是扯淡 jpeg_struct.optimize_coding=TRUE; //設(shè)置結(jié)構(gòu)體的顏色空間為RGB jpeg_struct.in_color_space=JCS_RGB; //顏色通道數(shù)量 jpeg_struct.input_components=3; //其他的設(shè)置默認(rèn) jpeg_set_defaults(&jpeg_struct); //設(shè)置質(zhì)量 jpeg_set_quality(&jpeg_struct,60,true); //開始?jí)嚎s,(是否寫入全部像素) jpeg_start_compress(&jpeg_struct,TRUE); JSAMPROWrow_pointer[1]; //一行的rgb introw_stride=w*3; //一行一行遍歷如果當(dāng)前的行數(shù)小于圖片的高度,就進(jìn)入循環(huán) while(jpeg_struct.next_scanline//得到一行的首地址 row_pointer[0]=&data[jpeg_struct.next_scanline*w*3]; //此方法會(huì)將jcs.next_scanline加1 jpeg_write_scanlines(&jpeg_struct,row_pointer,1);//row_pointer就是一行的首地址,1:寫入的行數(shù) } jpeg_finish_compress(&jpeg_struct); jpeg_destroy_compress(&jpeg_struct); fclose(file); } extern"C" JNIEXPORTvoidJNICALL Java_com_maniu_wechatimagesend_MainActivity_compress(JNIEnv*env, jobjectinstance, jobjectbitmap, jstringpath_){ constchar*path=env->GetStringUTFChars(path_,0); //獲取Bitmap信息 AndroidBitmapInfobitmapInfo; AndroidBitmap_getInfo(env,bitmap,&bitmapInfo); //存儲(chǔ)ARGB所有像素點(diǎn) BYTE*pixels; //1、讀取Bitmap所有像素信息 AndroidBitmap_lockPixels(env,bitmap,(void**)&pixels); //獲取bitmap的寬,高,format inth=bitmapInfo.height; intw=bitmapInfo.width; //存儲(chǔ)RGB所有像素點(diǎn) BYTE*data,*tmpData; //2、解析每個(gè)像素,去除A通量,取出RGB通量, //假如圖片的像素是1920*1080,只有RGB三個(gè)顏色通道的話,計(jì)算公式為w*h*3 data=(BYTE*)malloc(w*h*3); //存儲(chǔ)RGB首地址 tmpData=data; BYTEr,g,b; intcolor; for(inti=0;ifor(intj=0;jint*)pixels); //取出RGB r=((color&0x00FF0000)>>16); g=((color&0x0000FF00)>>8); b=((color&0x000000FF)); //賦值 *data=b; *(data+1)=g; *(data+2)=r; //指針后移 data+=3; pixels+=4; } } //3、讀取像素點(diǎn)完畢解鎖, AndroidBitmap_unlockPixels(env,bitmap); //直接用data寫數(shù)據(jù) writeImg(tmpData,path,w,h); env->ReleaseStringUTFChars(path_,path); }  


整個(gè)講解已經(jīng)在代碼里已經(jīng)做了注釋。


小結(jié)


查閱源碼發(fā)現(xiàn): 在Android系統(tǒng)在7.0版本之前內(nèi)部使用的是libjpeg非turbo版,并且為了性能關(guān)閉了Huffman編碼計(jì)算,使用默認(rèn)的哈夫曼表,而不是算數(shù)編碼。 從Android 7.0版本開始,系統(tǒng)內(nèi)部使用了libjpeg-turbo庫并且啟用Huffman編碼,標(biāo)示就是optimize_code已經(jīng)設(shè)置為了TRUE,也就是默認(rèn)使用Huffman壓縮計(jì)算生成新的哈夫曼表。libjpeg-turbo是一個(gè)C語音編寫的高效JPEG圖像處理庫,相當(dāng)于是一個(gè)libjpeg的增強(qiáng)版。 這也就是Luban壓縮為什么會(huì)給出一個(gè)turbo分支,其實(shí)是為了兼容Android 7.0版本之前。

審核編輯:湯梓紅


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • BITMAP
    +關(guān)注

    關(guān)注

    0

    文章

    4

    瀏覽量

    6375
  • 圖片壓縮
    +關(guān)注

    關(guān)注

    0

    文章

    6

    瀏覽量

    5532

原文標(biāo)題:最詳細(xì)的圖片壓縮攻略,讓你一次過足癮(建議收藏)

文章出處:【微信號(hào):vision263com,微信公眾號(hào):新機(jī)器視覺】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    吐血分享—史上最全51單片機(jī)工具

    `這是我收集的51單片機(jī)工具,如果大家需要就下載,認(rèn)為好的話就支持一下。吐血分享—史上最全51單片機(jī)工具`
    發(fā)表于 11-16 10:52

    史上最全面示波器教材

    由淺入深逐步介紹示波器的原理、使用,以及各性能術(shù)語詳解。實(shí)乃不可多得的教材之一,堪稱史上最全面的示波器介紹!
    發(fā)表于 08-01 20:51

    史上最全的運(yùn)放資料(實(shí)戰(zhàn)應(yīng)用)

    ` 本帖最后由 gk320830 于 2015-3-4 20:18 編輯 史上最全的運(yùn)放典型應(yīng)用電路及分析.pdf運(yùn)算放大器的原理和應(yīng)用.pdf運(yùn)算放大器基本電路大全.pdf運(yùn)算放大器經(jīng)典應(yīng)用.pdf運(yùn)算放大器設(shè)計(jì)及應(yīng)用.pdf運(yùn)算放大器應(yīng)用電路.pdf`
    發(fā)表于 08-04 23:33

    史上最全LED貼片燈知識(shí)資料集合

    史上最全LED貼片燈知識(shí)資料集合
    發(fā)表于 08-16 19:11

    史上最全的畢業(yè)設(shè)計(jì)資料--歡迎下載

    史上最全的畢業(yè)設(shè)計(jì)及產(chǎn)品設(shè)計(jì)資料--歡迎下載https://bbs.elecfans.com/forum.php?mod=viewthread&tid=271527&fromuid=779708
    發(fā)表于 08-30 00:32

    史上最全的Quartus II 中的warning及解決辦法

    史上最全的Quartus II 中的warning及解決辦法
    發(fā)表于 11-28 10:59

    史上最全的示波器學(xué)習(xí)資料

    史上最全的示波器學(xué)習(xí)資料
    發(fā)表于 04-21 12:58

    史上最全的PCB封裝命名規(guī)范

    史上最全的PCB封裝命名規(guī)范
    發(fā)表于 06-12 15:58

    史上最全的Protel99的元件庫、封裝庫

    `史上最全的Protel99的元件庫、封裝庫,希望能對大家有所幫助`
    發(fā)表于 08-04 16:00

    史上最全word用法

    史上最全word用法
    發(fā)表于 08-29 11:27

    史上最全的51單片機(jī)指令集

    史上最全的51單片機(jī)指令集
    發(fā)表于 09-27 22:28

    史上最全的PCB封裝命名規(guī)范

    史上最全的PCB封裝命名規(guī)范
    發(fā)表于 11-27 17:23

    linux下各種格式的壓縮包的解壓方法總結(jié)

    大致總結(jié)了一下linux下各種格式的壓縮包的壓縮、解壓方法。但是部分方法我沒有用到,也就不全,希望大家?guī)臀已a(bǔ)充,我將隨時(shí)修改完善,謝謝!  
    發(fā)表于 07-04 07:21

    史上最全的紅外遙控器編碼協(xié)議

    本帖最后由 王棟春 于 2020-12-12 22:35 編輯 史上最全的紅外遙控器編碼協(xié)議資料來自網(wǎng)絡(luò)資源
    發(fā)表于 12-12 22:34

    電腦上的圖片怎么批量壓縮

    ? ? 對電腦上的文件我們都會(huì)定期的清理,相信大家也發(fā)現(xiàn)了在整理圖片文件時(shí)總是會(huì)用到壓縮,我們電腦上都會(huì)保存著各式各樣的圖片,單項(xiàng)的壓縮圖片很浪費(fèi)時(shí)間,那么怎樣
    發(fā)表于 09-21 17:59 ?629次閱讀
    RM新时代网站-首页