網上有很多大拿分享的關於Android效能優化的文章,主要是通過各種工具分析,使用合理的技巧優化APP的體驗,提升APP的流暢度,但關於記憶體優化的文章很少有看到。在Android裝置記憶體動不動就上G的情況下,的確沒有必要去太在意APP對Android系統記憶體的消耗,但在實際工作中我做的是教育類的小學APP,APP中的按鈕、背景、動畫變換基本上全是圖片,在2K屏上(解析度2048*1536)一張背景圖片就會佔用記憶體12M,來回切換幾次記憶體佔用就會增漲到上百兆,為了在不影響APP的視覺效果的前提下,有必要通過各種手段來降低APP對記憶體的消耗,下面是我在實踐過程中使用的一些方法,很多都是不太成熟的專案,也不夠深入,只是將其作為一種處理方式分享給大家。
通過DDMS的APP記憶體佔用檢視工具分析發現,APP中佔用記憶體最多的是圖片,每個Activity中圖片佔用記憶體佔大半,本文重點分享對圖片的記憶體優化。
不要將Button的背景設定為selector
在佈局檔案和程式碼中,都可以為Button設定background為selector,這樣方便實現按鈕的正反選效果,但實際跟蹤發現,如果是將Button的背景設定為selector,在初始化Button的時候會將正反選圖片都載入在記憶體中(具體可以檢視Android原始碼,在類Drawable.java的createFromXmlInner方法中對圖片進行解析,最終呼叫Drawable的inflate方法),相當於一個按鈕佔用了兩張相同大小圖片所使用的記憶體,如果一個介面上按鈕很多或者是按鈕很大,光是按鈕佔用的記憶體就會很大,可以通過在佈局檔案中給按鈕只設定正常狀態下的背景圖片,然後在程式碼中監聽按鈕的點選狀態,當按下按鈕時為按鈕設定反選效果的圖片,抬起時重新設定為正常狀態下的背景,具體實現方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class ImageButtonClickUtils { private ImageButtonClickUtils(){ } /** * 設定按鈕的正反選效果 * * */ public static void setClickState(View view, final int normalResId, final int pressResId){ view.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch(event.getAction()){ case MotionEvent.ACTION_DOWN:{ v.setBackgroundResource(pressResId); } break; case MotionEvent.ACTION_MOVE:{ v.setBackgroundResource(pressResId); } break; case MotionEvent.ACTION_UP:{ v.setBackgroundResource(normalResId); } break; default:{ } break; } // 為了不影響監聽按鈕的onClick回撥,返回值應為false return false; } }); } } |
通過上面這種方式就可以解決同一個按鈕佔用兩倍記憶體的問題,如果你覺得為一個按鈕提供正反選兩張圖片會導致APK的體積變大,可以通過如下方式實現按鈕點選的反選效果,這種方式既不會存在Button佔用兩倍記憶體的情況,又減小了APK的體積(Android 5.0中的tintColor也可以實現類似的效果):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
ImageButton personalInfoBtn = (ImageButton)findViewById(R.id.personalBtnId); personalInfoBtn.setOnTouchListener(new OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN){ ((ImageButton)v).setColorFilter(getResources().getColor(0X50000000)); }else if(action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL){ ((ImageButton)v).clearColorFilter(); } // 為了不影響監聽按鈕的onClick回撥,返回值應為false return false; } }); |
將背景圖片放在非UI執行緒繪製,提升APP的效率
在高解析度的平板裝置上,繪製大背景的圖片會影響程式的執行效率,嚴重情況下就和沒有開硬體加速的時候使用手寫功能一樣,相當地卡,最後我們的解決方案是將背景圖片通過SurfaceView來繪製,這樣相當於是在非UI執行緒繪製,不會影響到UI執行緒做其它事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.SurfaceHolder; import android.view.SurfaceView; import com.eebbk.hanziLearning.activity.R; public class RootSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable{ private float mViewWidth = 0; private float mViewHeight = 0; private int mResourceId = 0; private Context mContext = null; private volatile boolean isRunning = false; private SurfaceHolder mSurfaceHolder = null; public RootSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initRootSurfaceView(context, attrs, defStyleAttr, 0); } public RootSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); initRootSurfaceView(context, attrs, 0, 0); } private void initRootSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ mContext = context; DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RootSurfaceView, defStyleAttr, defStyleRes); int n = a.getIndexCount(); mViewWidth = displayMetrics.widthPixels; mViewHeight = displayMetrics.heightPixels; for(int index=0; index<n; index++){ int attr = a.getIndex(index); switch(attr){ case R.styleable.RootSurfaceView_background:{ mResourceId = a.getResourceId(attr, 0); } break; case R.styleable.RootSurfaceView_view_width:{ mViewWidth = a.getDimension(attr, displayMetrics.widthPixels); } break; case R.styleable.RootSurfaceView_view_height:{ mViewHeight = a.getDimension(attr, displayMetrics.heightPixels); } break; default:{ } break; } } a.recycle(); mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT); } private Bitmap getDrawBitmap(Context context, float width, float height) { Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mResourceId); Bitmap resultBitmap = zoomImage(bitmap, width, height); return resultBitmap; } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { System.out.println("RootSurfaceView surfaceChanged"); } @Override public void surfaceCreated(SurfaceHolder holder) { drawBackGround(holder); System.out.println("RootSurfaceView surfaceCreated"); } @Override public void surfaceDestroyed(SurfaceHolder holder) { isRunning = false; System.out.println("RootSurfaceView surfaceDestroyed"); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); System.out.println("RootSurfaceView onAttachedToWindow"); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); System.out.println("RootSurfaceView onDetachedFromWindow"); } @Override public void run(){ while(isRunning){ synchronized (mSurfaceHolder) { if(!mSurfaceHolder.getSurface().isValid()){ continue; } drawBackGround(mSurfaceHolder); } isRunning = false; break; } } private void drawBackGround(SurfaceHolder holder) { Canvas canvas = holder.lockCanvas(); Bitmap bitmap = getDrawBitmap(mContext, mViewWidth, mViewHeight); canvas.drawBitmap(bitmap, 0, 0, null); bitmap.recycle(); holder.unlockCanvasAndPost(canvas); } public static Bitmap zoomImage( Bitmap bgimage , float newWidth , float newHeight ) { float width = bgimage.getWidth( ); float height = bgimage.getHeight( ); Matrix matrix = new Matrix(); float scaleWidth = newWidth/width; float scaleHeight = newHeight/height; matrix.postScale( scaleWidth, scaleHeight ); Bitmap bitmap = Bitmap.createBitmap( bgimage, 0, 0, ( int ) width , ( int ) height, matrix, true ); if( bitmap != bgimage ){ bgimage.recycle(); bgimage = null; } return bitmap; } } |
在res/values/attr.xml檔案中定義自定義View的自定義屬性:
1 2 3 4 5 |
<declare-styleable name="RootSurfaceView"> <attr name="background" format="reference" /> <attr name="view_width" format="dimension" /> <attr name="view_height" format="dimension" /> </declare-styleable> |
沒有必要使用硬體加速的介面建議關掉硬體加速
通過DDMS的heap跟蹤發現,相比於關閉硬體加速,在開啟硬體加速的情況下會消耗更多的記憶體,但有的介面開啟或者關閉硬體加速對程式的執行效率並沒有太大的影響,此種情況下可以考慮在AndroidManifest.xml檔案中關閉掉對應Activity的硬體加速,like this:
1 2 3 4 5 6 7 |
<!-- 設定介面 --> <activity android:name=".SettingActivity" android:hardwareAccelerated="false" android:screenOrientation="sensorLandscape" android:theme="@style/Translucent_NoTitle"> </activity> |
注意:如果使用到WebView、視訊播放、手寫、動畫等功能時,關掉硬體加速會嚴重音效程式的執行效率,這種情況可以只關閉掉Activity中某些view的硬體加速,整個Activity的硬體加速不關閉。
如果Activity中某個View需要關閉硬體加速,但整個Activity不能關閉,可以呼叫view層級關閉硬體加速的方法:
1 2 |
// view.setLayerType || 在定義view的構造方法中呼叫該方法 setLayerType(View.LAYER_TYPE_SOFTWARE, null); |
儘量少用AnimationDrawable,如果必須要可以自定義圖片切換器代替AnimationDrawable
AnimationDrawable也是一個耗記憶體大戶,圖片幀數越多耗記憶體越大,具體可以檢視AnimationDrawable的原始碼,在AnimationDrawable例項化的時候,Drawable的createFromXmlInner方法會呼叫AnimationDrawable的inflate方法,該方法裡面有一個while迴圈去一次性將所有幀都讀取出來,也就是在初始化的時候就將所有的幀讀在記憶體中了,有多少張圖片,它就要消耗對應大小的記憶體。
雖然可以通過如下方式釋放AnimationDrawable佔用的記憶體,但是當退出使用AnimationDrawable的介面,再次進入使用其播放動畫時,會報使用已經回收了的圖片的異常,這個應該是Android對圖片的處理機制導致的,雖然Activity被finish掉了,但是這個Activity中使用到的圖片還是在記憶體中,如果被回收,下次進入時就會報異常資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * 釋放AnimationDrawable佔用的記憶體 * * * */ @SuppressWarnings("unused") private void freeAnimationDrawable(AnimationDrawable animationDrawable) { animationDrawable.stop(); for (int i = 0; i < animationDrawable.getNumberOfFrames(); ++i){ Drawable frame = animationDrawable.getFrame(i); if (frame instanceof BitmapDrawable) { ((BitmapDrawable)frame).getBitmap().recycle(); } frame.setCallback(null); } animationDrawable.setCallback(null); } |
通常情況下我會自定義一個ImageView來實現AnimationDrawable的功能,根據圖片之間切換的時間間隔來定時設定ImageView的背景圖片,這樣始終只是一個ImageView例項,更換的只是其背景,佔用記憶體會比AnimationDrawable小很多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
/** * 圖片動態切換器 * * */ public class AnimImageView { private static final int MSG_START = 0xf1; private static final int MSG_STOP = 0xf2; private static final int STATE_STOP = 0xf3; private static final int STATE_RUNNING = 0xf4; /* 執行狀態*/ private int mState = STATE_RUNNING; private ImageView mImageView; /* 圖片資源ID列表*/ private List<Integer> mResourceIdList = null; /* 定時任務*/ private Timer mTimer = null; private AnimTimerTask mTimeTask = null; /* 記錄播放位置*/ private int mFrameIndex = 0; /* 播放形式*/ private boolean isLooping = false; public AnimImageView( ){ mTimer = new Timer(); } /** * 設定動畫播放資源 * * */ public void setAnimation( HanziImageView imageview, List<Integer> resourceIdList ){ mImageView = imageview; mResourceIdList = resourceIdList; } /** * 開始播放動畫 * @param loop 時候迴圈播放 * @param duration 動畫播放時間間隔 * */ public void start(boolean loop, int duration){ stop(); isLooping = loop; mFrameIndex = 0; mState = STATE_RUNNING; mTimeTask = new AnimTimerTask( ); mTimer.schedule(mTimeTask, 0, duration); } /** * 停止動畫播放 * * */ public void stop(){ if (mTimeTask != null) { mFrameIndex = 0; mState = STATE_STOP; mTimer.purge(); mTimeTask.cancel(); mTimeTask = null; mImageView.setBackgroundResource(0); } } /** * 定時器任務 * * */ class AnimTimerTask extends TimerTask { @Override public void run() { if(mFrameIndex < 0 || mState == STATE_STOP){ return; } if( mFrameIndex < mResourceIdList.size() ){ Message msg = AnimHanlder.obtainMessage(MSG_START,0,0,null); msg.sendToTarget(); }else{ mFrameIndex = 0; if(!isLooping){ Message msg = AnimHanlder.obtainMessage(MSG_STOP,0,0,null); msg.sendToTarget(); } } } } private Handler AnimHanlder = new Handler(){ public void handleMessage(android.os.Message msg) { switch (msg.what) { case MSG_START:{ if(mFrameIndex >=0 && mFrameIndex < mResourceIdList.size() && mState == STATE_RUNNING){ mImageView.setImageResource(mResourceIdList.get(mFrameIndex)); mFrameIndex++; } } break; case MSG_STOP:{ if (mTimeTask != null) { mFrameIndex = 0; mTimer.purge(); mTimeTask.cancel(); mState = STATE_STOP; mTimeTask = null; mImageView.setImageResource(0); } } break; default: break; } } }; } |
其它優化方式
- 儘量將Activity中的小圖片和背景合併,一張小圖片既浪費佈局的時間,又平白地增加了記憶體佔用;
- 不要在Activity的主題中為Activity設定預設的背景圖片,這樣會導致Activity佔用的記憶體翻倍:<!–千萬不要在主題中為Activity設定預設背景<style name=”Activity_Style” parent=”@android:Theme.Holo.Light.NoActionBar”>
<item name=”android:background”>@drawable/*</item>
</style> - 對於在需要時才顯示的圖片或者佈局,可以使用ViewStub標籤,通過sdk/tools目錄下的hierarchyviewer.bat檢視佈局檔案會發現,使用viewstub標籤的元件幾乎不消耗佈局的時間,在程式碼中當需要顯示時再去例項化有助於提高Activity的佈局效率和節省Activity消耗的記憶體。