SoFunction
Updated on 2025-04-04

Summary of Android App memory optimization picture optimization

Preface

When the Android device memory is always on G, it is indeed not necessary to care too much about the consumption of the APP on the Android system memory. However, in actual work, I am doing an educational elementary school APP. The buttons, backgrounds, and animation transformations in the APP are basically all pictures. On a 2K screen (resolution 2048*1536), a background image will occupy 12M of memory. The memory usage will increase to hundreds of megabytes after switching back and forth. In order not to affect the visual effect of the APP, it is necessary to reduce the consumption of the APP on various means.

Through the analysis of the DDMS APP memory occupancy viewing tool, it was found that the picture occupies the most memory in the APP, and the picture occupies most memory in each activity. This article focuses on sharing the memory optimization of pictures.

Don't set the background of the Button to selector

In the layout file and code, the background can be set as a selector for Button, which is convenient for the forward and reverse selection of the button. However, actual tracking found that if the Button background is set to the selector, the forward and reverse selection images will be loaded in memory when the Button is initialized (for details, you can check the Android source code, in the classofcreateFromXmlInnerThe image is parsed in the method and finally calledDrawableofinflateMethod), which is equivalent to a button occupies the memory used by two pictures of the same size. If there are many buttons on an interface or the button is large, the memory occupied by the button alone will be large. You can set only the background image in the normal state for the button in the layout file, and then listen to the button click status in the code. When the button is pressed, set the picture with the inverse selection effect for the button, and reset it to the background in the normal state when lifted. The specific implementation method is as follows:

 public class ImageButtonClickUtils {
 private ImageButtonClickUtils(){

 }

 /**
   * Set the button's forward and reverse selection effect
   *
   * */
 public static void setClickState(View view, final int normalResId, final int pressResId){
  (new OnTouchListener() {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
   switch(()){
   case MotionEvent.ACTION_DOWN:{
   (pressResId);
   }
   break;
   case MotionEvent.ACTION_MOVE:{
   (pressResId);
   }
   break;
   case MotionEvent.ACTION_UP:{
   (normalResId);
   }
   break;
   default:{

   }
   break;
   }

   // In order not to affect the onClick callback of the listen button, the return value should be false   return false;
  }
  });
 }
}

The above method can solve the problem that the same button occupies twice the memory. If you think that selecting two images for a button will cause the size of the APK to grow larger, you can realize the reverse selection effect of button clicks in the following way. This method will not have the Button occupies twice the memory, but also reduce the volume of the APK (tintColor in Android 5.0 can also achieve similar effects):

 ImageButton personalInfoBtn = (ImageButton)findViewById();
 (new OnTouchListener() {
 @SuppressLint("ClickableViewAccessibility")
 @Override
 public boolean onTouch(View v, MotionEvent event) {
  int action = ();

  if(action == MotionEvent.ACTION_DOWN){
  ((ImageButton)v).setColorFilter(getResources().getColor(0X50000000));
  }else if(action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL){
  ((ImageButton)v).clearColorFilter();
  }

  // In order not to affect the onClick callback of the listen button, the return value should be false  return false;
 }
 });

Put the background image in non-UI thread to improve the efficiency of the APP

On high-resolution tablet devices, drawing large background pictures will affect the operation efficiency of the program. In severe cases, it is the same as using the handwriting function when hardware acceleration is not turned on, which is quite stuck. In the end, our solution is to pass the background picture.SurfaceViewTo draw, this is equivalent to drawing on non-UI threads and will not affect the UI threads to do other things:

import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

import ;

public class RootSurfaceView extends SurfaceView implements , 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 = ().getDisplayMetrics();
 TypedArray a = (attrs, , defStyleAttr, defStyleRes);
 int n = ();
 mViewWidth = ;
 mViewHeight = ;
 for(int index=0; index<n; index++){
  int attr = (index);
  switch(attr){
  case .RootSurfaceView_background:{
  mResourceId = (attr, 0);
  }
  break;
  case .RootSurfaceView_view_width:{
  mViewWidth = (attr, );
  }
  break;
  case .RootSurfaceView_view_height:{
  mViewHeight = (attr, );
  }
  break;
  default:{

  }
  break;
  }
 }
 ();
 mSurfaceHolder = getHolder();
 (this);
 ();
 }

 private Bitmap getDrawBitmap(Context context, float width, float height) {
 Bitmap bitmap = (getResources(), mResourceId);
 Bitmap resultBitmap = zoomImage(bitmap, width, height);
 return resultBitmap;
 }

 @Override
 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
 ("RootSurfaceView surfaceChanged");
 }

 @Override
 public void surfaceCreated(SurfaceHolder holder) {
 drawBackGround(holder);
 ("RootSurfaceView surfaceCreated");
 }

 @Override
 public void surfaceDestroyed(SurfaceHolder holder) {
 isRunning = false;
 ("RootSurfaceView surfaceDestroyed");
 }

 @Override
 protected void onAttachedToWindow() {
 ();
 ("RootSurfaceView onAttachedToWindow");
 }

 @Override
 protected void onDetachedFromWindow() {
 ();
 ("RootSurfaceView onDetachedFromWindow");
 }

 @Override
 public void run(){ 
 while(isRunning){ 
  synchronized (mSurfaceHolder) { 
  if(!().isValid()){
   continue;
  }
  drawBackGround(mSurfaceHolder);
  }
  isRunning = false;
  break;
 } 
 }

 private void drawBackGround(SurfaceHolder holder) {
 Canvas canvas = ();
 Bitmap bitmap = getDrawBitmap(mContext, mViewWidth, mViewHeight);
 (bitmap, 0, 0, null);
 ();
 (canvas);
 }

 public static Bitmap zoomImage( Bitmap bgimage , float newWidth , float newHeight ) {
 float width = ( );
 float height = ( );
 Matrix matrix = new Matrix();
 float scaleWidth = newWidth/width;
 float scaleHeight = newHeight/height;
 ( scaleWidth, scaleHeight );
 Bitmap bitmap = ( bgimage, 0, 0, ( int ) width , ( int ) height, matrix, true );
 if( bitmap != bgimage ){
  ();
  bgimage = null;
 }
 return bitmap;
 }
}

Define custom properties of custom View in the res/values/ file:

<declare-styleable name="RootSurfaceView">
 <attr name="background" format="reference" />
 <attr name="view_width" format="dimension" />
 <attr name="view_height" format="dimension" />
</declare-styleable>

It is recommended to turn off the hardware acceleration interface without using the hardware acceleration interface.

Through DDMS heap tracking, it is found that compared to turning off hardware acceleration, it consumes more memory when turning on hardware acceleration, but turning on hardware acceleration on some interfaces does not have much impact on the program's running efficiency. In this case, you can consider closing the hardware acceleration corresponding to the activity in the file, like this:

&lt;!-- Settings interface --&gt;
&lt;activity
 android:name=".SettingActivity"
 android:hardwareAccelerated="false"
 android:screenOrientation="sensorLandscape"
 android:theme="@style/Translucent_NoTitle"&gt;
&lt;/activity&gt;

Note: If you use WebView, video playback, handwriting, animation and other functions, turning off hardware acceleration will seriously affect the running efficiency of the sound effects program. In this case, you can only turn off the hardware acceleration of some views in the Activity, and the hardware acceleration of the entire Activity will not be turned off.

If a View in the Activity needs to turn off hardware acceleration, but the entire Activity cannot be turned off, you can call the method of turning off hardware acceleration at the view level:

// || Call this method in the constructor that defines the viewsetLayerType(View.LAYER_TYPE_SOFTWARE, null);

Try to use AnimationDrawable as little as possible. If you have to, you can customize the image switch instead of AnimationDrawable.

AnimationDrawable is also a memory-consuming user. The more frames the picture, the larger the memory is, the more you consume. For details, you can check the source code of AnimationDrawable. When the AnimationDrawable is instantiated, the createFromXmlInner method of Drawable will call the inflate method of AnimationDrawable. There is a while loop in this method to read all frames at once, that is, all frames are read in memory during initialization. The number of pictures there are to consume the corresponding memory size.

Although the memory occupied by AnimationDrawable can be released in the following way, when exiting the interface of using AnimationDrawable and entering the playback of animations again, an exception will be reported using the recycled image. This should be caused by Android's image processing mechanism. Although the Activity is finished, the pictures used in this Activity are still in memory. If they are recycled, an exception message will be reported next time they enter:

/**
  * Free the memory occupied by AnimationDrawable
  *
  *
  * */
@SuppressWarnings("unused")
private void freeAnimationDrawable(AnimationDrawable animationDrawable) {
 (); 
 for (int i = 0; i &lt; (); ++i){
 Drawable frame = (i);
 if (frame instanceof BitmapDrawable) {
  ((BitmapDrawable)frame).getBitmap().recycle();
 } 
 (null);
 } 

 (null);
}

Normally, I will customize an ImageView to implement the function of AnimationDrawable, and set the background image of the ImageView regularly according to the time interval between images. This is always just an ImageView instance, and the background is replaced, and the memory occupies much smaller than that of AnimationDrawable:

/**
  * Picture dynamic switcher
  *
  * */
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;

 /* Running status*/
 private int mState = STATE_RUNNING;
 private ImageView mImageView;
 /* Image resource ID list*/
 private List&lt;Integer&gt; mResourceIdList = null;
 /* Timed tasks*/
 private Timer mTimer = null;
 private AnimTimerTask mTimeTask = null;
 /* Record playback position*/
 private int mFrameIndex = 0;
 /* Playback format*/
 private boolean isLooping = false;

 public AnimImageView( ){
 mTimer = new Timer();
 }

 /**
  * Set animation playback resources
  *
  * */
 public void setAnimation( HanziImageView imageview, List&lt;Integer&gt; resourceIdList ){
 mImageView = imageview;
 mResourceIdList = resourceIdList;
 }

 /**
  * Start playing animation
  * @param loop playback
  * @param duration Animation playback time interval
  * */
 public void start(boolean loop, int duration){
 stop();
 isLooping = loop;
 mFrameIndex = 0;
 mState = STATE_RUNNING;
 mTimeTask = new AnimTimerTask( );
 (mTimeTask, 0, duration);
 }

 /**
  * Stop animation playback
  *
  * */
 public void stop(){
 if (mTimeTask != null) {
  mFrameIndex = 0;
  mState = STATE_STOP;
  ();
  ();
  mTimeTask = null;
  (0);
 }
 }

 /**
  * Timer task
  *
  *
  */
 class AnimTimerTask extends TimerTask {
 @Override
 public void run() {
  if(mFrameIndex &lt; 0 || mState == STATE_STOP){
  return;
  }

  if( mFrameIndex &lt; () ){
  Message msg = (MSG_START,0,0,null);
  ();
  }else{
  mFrameIndex = 0;
  if(!isLooping){
   Message msg = (MSG_STOP,0,0,null);
   ();
  }
  }
 }
 }

 private Handler AnimHanlder = new Handler(){
  public void handleMessage( msg) {
  switch () {
  case MSG_START:{
   if(mFrameIndex &gt;=0 &amp;&amp; mFrameIndex &lt; () &amp;&amp; mState == STATE_RUNNING){
   ((mFrameIndex));
   mFrameIndex++;
   }
  }
   break;
  case MSG_STOP:{
   if (mTimeTask != null) {
   mFrameIndex = 0;
   ();
   ();
   mState = STATE_STOP;
   mTimeTask = null;
   (0);
   }
  }
   break;
  default:
   break;
  }
  }
 };
}

Other optimization methods

1. Try to merge the small pictures and backgrounds in the Activity. A small picture not only wastes the layout time, but also increases memory usage in a simple way;

2. Do not set the default background image for Activity in the theme of Activity, as this will cause the Activity to double the memory occupied by the Activity:

&lt;!--Never do it in the subjectActivitySet the default background

&lt;style name="Activity_Style" parent="@android:"&gt;
&lt;item name="android:background"&gt;@drawable/*&lt;/item&gt;
&lt;/style&gt;

3. For pictures or layouts that are displayed only when needed, you can use the ViewStub tag. By viewing the layout file in the sdk/tools directory, you will find that components using the Viewstub tag almost consume no layout time. De-instanced when displaying in the code helps to improve the layout efficiency of the Activity and save the memory consumed by the Activity.

Summarize

The above is all about this article. I hope it will be helpful to everyone to develop Android. If you have any questions, you can leave a message to discuss.