深入探讨 Android 开发中的性能优化

在 Android 开发领域,打造高性能应用至关重要。随着移动设备和用户需求的不断演进,应用性能成为决定用户体验的关键因素。性能卓越的应用不仅能提升用户满意度,还能在竞争激烈的应用市场中脱颖而出。今天,我们就来深入探讨 Android 开发中的性能优化相关问题。​

一、性能优化的重要性​

在当今快节奏的数字时代,用户对应用的响应速度和流畅度要求极高。调查显示,用户卸载软件的主要原因之一就是性能差。例如,一款启动缓慢、界面切换卡顿的购物应用,会让用户在购物过程中失去耐心,转而选择其他竞品应用。因此,性能优化在 Android 开发中占据着举足轻重的地位。​

二、性能相关的参考指标​

(一)启动速度​

启动速度分为冷启动和热启动。冷启动指杀掉应用后重新启动的速度,热启动则是应用在后台运行一段时间后再次启动的速度。快速的启动速度能让用户迅速进入应用,提升用户体验。例如,一款新闻资讯应用如果能在 1 秒内完成冷启动,就能让用户更快地获取最新资讯,增强用户粘性。​

(二)界面切换​

界面切换应确保无卡顿现象。在实际使用中,当用户在社交应用中快速切换不同的聊天窗口、相册等界面时,如果出现卡顿,会严重影响用户的使用感受。​

(三)内存管理​

内存管理主要关注内存泄漏问题。内存泄漏会导致应用不断消耗内存,最终可能引发应用崩溃。例如,一个图片编辑应用,如果在处理大量图片时存在内存泄漏,随着使用时间的增加,应用会变得越来越卡顿,甚至出现闪退现象。​

(四)UI 过度绘制​

UI 过度绘制通常由 xml 布局、自定义控件多次调用 onlayout 等原因导致。这会增加 CPU 和 GPU 的负担,降低应用的性能。比如一个界面复杂的游戏应用,如果存在大量的 UI 过度绘制,会导致游戏运行时发热严重,帧率下降。​

三、常见性能问题及解决方案​

(一)界面切换卡顿​

1.人为在 UI 线程中做轻微耗时操作,导致 UI 线程卡顿:​

  • 原因:Android 的 UI 操作必须在主线程(UI 线程)中执行,但如果在 UI 线程中执行诸如网络请求、复杂计算等耗时操作,就会阻塞 UI 线程,导致界面卡顿。例如,在 Activity 的 onCreate 方法中直接进行大量数据的数据库查询操作。​
  • 解决方案:将耗时操作放到子线程中执行。可以使用 Thread、AsyncTask、HandlerThread 等方式。例如,使用 AsyncTask 来执行网络请求,示例代码如下:​

class DownloadTask extends AsyncTask {​
    @Override​
    protected String doInBackground(String... urls) {​
        // 执行网络请求等耗时操作​
        return result;​
    }​
​
    @Override​
    protected void onPostExecute(String result) {​
        // 更新UI​
    }​
}

2.布局 Layout 过于复杂,无法在 16ms 内完成渲染:​

  • 原因:当布局嵌套层级过多、使用了过多的复杂控件等,都会增加布局的解析和渲染时间。例如,一个界面使用了多层 LinearLayout 嵌套,并且每个 LinearLayout 中都有大量的子控件。​
  • 解决方案:​
  • 优化布局结构:避免不必要的布局嵌套,尽量使用相对布局(RelativeLayout)减少层级。例如,将一些可以通过相对位置摆放的控件使用 RelativeLayout 来布局,减少 LinearLayout 的嵌套。​
  • 使用合适的布局标签:​
  • include 标签:用于重用布局。当多个界面有公共部分时,将公共部分提取到一个独立布局文件中,然后在每个界面布局文件中通过 include 标签引用。例如,多个 Activity 的顶部都有相同的标题栏布局,可将标题栏布局提取为 header_layout.xml,在其他 Activity 的布局文件中通过引用。​
  • merge 标签:作为 include 标签的辅助扩展,防止引入多余的布局嵌套。比如在一个布局文件中使用 include 引用另一个布局时,如果被引用布局的根元素是 LinearLayout,且该 LinearLayout 没有设置特殊属性,可将其根元素改为 merge,这样在解析布局时可以减少一层嵌套。​
  • ViewStub 标签:实现布局动态加载。对于一些在特定条件下才显示的布局,可以使用 ViewStub。例如,一个设置界面中,有一个高级设置部分,默认不显示,只有当用户点击 “高级设置” 按钮时才显示,可将高级设置部分的布局用 ViewStub 来加载,代码如下:​

在代码中通过ViewStub viewStub = findViewById(R.id.view_stub); viewStub.inflate();来加载布局。​

3. 同一时间动画执行的次数过多,导致 CPU 或 GPU 负载过重:

  • 原因:在一些动画效果丰富的应用中,如游戏、动画制作应用等,如果同时启动大量动画,会占用过多的 CPU 和 GPU 资源。​
  • 解决方案:合理控制动画的执行数量和时机。可以使用动画队列,将多个动画依次执行,而不是同时执行。例如,使用 AnimatorSet 来组合多个动画并按顺序播放:​
AnimatorSet animatorSet = new AnimatorSet();​
ObjectAnimator animator1 = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f);​
ObjectAnimator animator2 = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);​
animatorSet.playSequentially(animator1, animator2);​
animatorSet.start();

4.View 过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使 CPU 或 GPU 负载过重:

  • 原因:常见原因包括布局中存在多余的背景、不必要的重叠控件等。例如,一个 LinearLayout 设置了背景色,其内部的子 TextView 也设置了背景色,导致该区域像素被重复绘制。​
  • 解决方案:​
  • 移除或修改 Window 默认的 Background:在主题中设置合适的背景,避免默认背景造成的过度绘制。例如,在 styles.xml 中:​

如果不需要背景,可设置为null:​

null

  • 移除 XML 布局文件中非必需的 Background:检查布局文件,去除不必要的背景设置。例如,对于一些只用于布局结构而不需要显示背景的 ViewGroup,移除其背景属性。​
  • 按需显示占位背景图片:对于一些需要加载图片的场景,在图片加载完成前显示占位图,但要注意占位图的显示时机和方式,避免过度绘制。例如,使用 Glide 加载图片时,可设置占位图:​
    Glide.with(context)​
       .load(imageUrl)​
       .placeholder(R.drawable.placeholder_image)​
       .into(imageView);

5.View 频繁的触发 measure、layout,导致 measure、layout 累计耗时过多及整个 View 频繁的重新渲染:​

  • 原因:当在代码中频繁修改 View 的属性,如 LayoutParams 等,会导致 View 重新进行测量和布局。例如,在一个循环中不断修改 TextView 的宽度。​
  • 解决方案:尽量减少在运行时对 View 属性的频繁修改。如果必须修改,可考虑批量修改。例如,先创建一个新的 LayoutParams 对象,设置好所有需要修改的属性,然后一次性应用到 View 上:​
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(​
        LinearLayout.LayoutParams.WRAP_CONTENT,​
        LinearLayout.LayoutParams.WRAP_CONTENT);​
    params.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);​
    textView.setLayoutParams(params);

6.内存频繁触发 GC 过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作:​

  • 原因:在短时间内大量创建对象,如在一个方法中循环创建大量临时对象。例如,在一个频繁调用的动画帧更新方法中,每次都创建新的 Point 对象来记录坐标。​
  • 解决方案:尽量复用对象。可以使用对象池技术,提前创建好一些对象,在需要时从对象池中获取,使用完后再放回对象池。例如,对于上述记录坐标的需求,可以创建一个 Point 对象池:​
  public PointPool() {​
        for (int i = 0; i < POOL_SIZE; i++) {​
            pointStack.push(new Point());​
        }​
    }​
​
    public Point obtain() {​
        if (pointStack.isEmpty()) {​
            return new Point();​
        }​
        return pointStack.pop();​
    }​
​
    public void recycle(Point point) {​
        pointStack.push(point);​
    }​
}

7.冗余资源及逻辑等导致加载和执行缓慢:​

  • 原因:应用中包含未使用的资源文件、复杂且不必要的业务逻辑等。例如,项目中存在大量未使用的图片资源,或者在数据处理逻辑中有复杂的条件判断但实际上只有一种情况会执行。​
  • 解决方案:​
  • 清理冗余资源:定期检查项目,删除未使用的资源文件。可以使用 Android Studio 的代码分析工具来查找未使用的资源。​
  • 优化业务逻辑:简化复杂的业务逻辑,去除不必要的条件判断和重复代码。例如,将复杂的条件判断逻辑通过策略模式进行重构,使代码更加简洁和易于维护。​
  1. 工作线程优先级未设置为 Process.THREAD_PRIORITY_BACKGROUND 导致后台线程抢占 UI 线程 cpu 时间片,阻塞渲染操作:​
  • 原因:如果后台线程优先级设置过高,会与 UI 线程竞争 CPU 资源,导致 UI 线程得不到足够的时间来处理渲染等操作。​
  • 解决方案:在创建后台线程时,设置其优先级为Process.THREAD_PRIORITY_BACKGROUND。例如,使用 Thread 创建线程时:​
    Thread backgroundThread = new Thread(new Runnable() {​
        @Override​
        public void run() {​
            // 后台任务​
        }​
    });​
    backgroundThread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);​
    backgroundThread.start();

8.ANR(Application Not Responding):​

  • 原因:​
  • KeyDispatchTimeout (5 seconds) –主要类型:按键或触摸事件在特定时间内无响应。通常是因为 UI 线程被长时间阻塞,无法及时处理用户输入事件。例如,在点击按钮的事件处理方法中执行了一个耗时几分钟的数据库写入操作。​
  • BroadcastReceiver 在特定时间内无法处理完成:当 BroadcastReceiver 的 onReceive 方法执行时间过长,超过 10 秒(前台广播)或 60 秒(后台广播)时,会出现此类型 ANR。比如在 onReceive 方法中进行复杂的数据解析和网络请求。​
  • ServiceTimeout (20 seconds) –小概率类型:Service 在特定的时间内无法处理完成。例如,在 Service 的 onStartCommand 方法中执行耗时操作,且未在规定时间内返回。​
  • 解决方案:​
  • UI 线程只做跟 UI 相关的工作:将耗时的工作,如数据库操作、I/O、连接网络等放入单独的线程处理。并且尽量用 Handler 来处理 UI thread 和子线程之间的交互。例如,在子线程中完成网络请求后,通过 Handler 将结果发送到 UI 线程更新 UI:​
    Handler handler = new Handler(Looper.getMainLooper());​
    handler.post(new Runnable() {​
        @Override​
        public void run() {​
            // 更新UI​
        }​
    });

  • 优化 BroadcastReceiver 的处理逻辑:如果在 onReceive 方法中有耗时操作,可将其放到子线程中执行。例如,使用 IntentService 来处理广播中的耗时任务,IntentService 会在一个单独的工作线程中执行任务,并且在任务完成后自动停止。​
    public class MyIntentService extends IntentService {​
        public MyIntentService() {​
            super("MyIntentService");​
        }​
    ​
        @Override​
        protected void onHandleIntent(@Nullable Intent intent) {​
            // 处理耗时任务​
        }​
    }

在 BroadcastReceiver 中启动 IntentService:​

Intent intent = new Intent(context, MyIntentService.class);​
context.startService(intent);

  • 优化 Service 的处理逻辑:对于 Service 中的耗时操作,同样可以使用子线程来处理。如果是长期运行的任务,可考虑使用 IntentService 或在 Service 中创建线程池来管理线程。例如,使用 ThreadPoolExecutor 创建线程池:​
    ThreadPoolExecutor executor = new ThreadPoolExecutor(​
        corePoolSize,​
        maximumPoolSize,​
        keepAliveTime,​
        TimeUnit.MILLISECONDS,​
        new LinkedBlockingQueue());​
    executor.submit(new Runnable() {​
        @Override​
        public void run() {​
            // Service中的耗时任务​
        }​
    });

(二)内存问题​

内存问题主要表现为内存泄露,或者内存使用不当导致的内存抖动。如果存在内存泄露,应用会不断消耗内存,易导致频繁 gc 使系统出现卡顿,或者出现 OOM(Out Of Memory)报错;内存抖动也会导致 UI 卡顿。​

1.单例造成的内存泄漏:​

  • 原因:Android 的单例模式使用广泛,但如果使用不当会造成内存泄漏。由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收。例如:​
    public class AManager {​
        private static AManager instance;​
        private Context context;​
    ​
        private AManager(Context context) {​
            this.context = context;​
        }​
    ​
        public static AManager getInstance(Context context) {​
            if (instance == null) {​
                instance = new AManager(context);​
            }​
            return instance;​
        }​
    }

当传入的是 Activity 的 Context 时,若该 Activity 退出,由于单例持有该 Activity 的 Context 引用,导致 Activity 的内存无法回收。​

  • 解决方案:修改单例模式,确保传入的是 Application 的 Context。例如:​
    public class AManager {​
        private static AManager instance;​
        private Context context;​
    ​
        private AManager(Context context) {​
            this.context = context.getApplicationContext();​
        }​
    ​
        public static AManager getInstance(Context context) {​
            if (instance == null) {​
                instance = new AManager(context);​
            }​
            return instance;​
        }​
    }

2.非静态内部类创建静态实例造成的内存泄漏:​

  • 原因:在启动频繁的 Activity 中,为避免重复创建相同的数据资源,可能会在 Activity 内部创建非静态内部类的单例。由于非静态内部类默认持有外部类(Activity)的引用,且创建的静态实例生命周期和应用一样长,导致 Activity 的内存资源不能正常回收。例如:​
    public class MainActivity extends AppCompatActivity {​
        private static InnerClass innerClass;​
    ​
        @Override​
        protected void onCreate(Bundle savedInstanceState) {​
            super.onCreate(savedInstanceState);​
            setContentView(R.layout.activity_main);​
            if (innerClass == null) {​
                innerClass = new InnerClass();​
            }​
        }​
    ​
        class InnerClass {​
            // 内部类代码​
        }​
    }

解决方案:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,请使用 ApplicationContext。修改后的代码如下:​

public class MainActivity extends AppCompatActivity {​
    private static InnerClass innerClass;​
​
    @Override​
    protected void onCreate(Bundle savedInstanceState) {​
        super.onCreate(savedInstanceState);​
        setContentView(R.layout.activity_main);​
        if (innerClass == null) {​
            innerClass = InnerClass.getInstance(this);​
        }​
    }​
​
    static class InnerClass {​
        private static InnerClass instance;​
        private Context context;​
​
        private InnerClass(Context context) {​
            this.context = context.getApplicationContext();​
        }​
​
        public static InnerClass getInstance(Context context) {​
            if (instance == null) {​
                instance = new InnerClass(context);​
            }​
            return instance;​
        }​
    }​
}

Handler 造成的内存泄漏:​

  • 原因:在使用 Handler 时,如果编写不规范易造成内存泄漏。例如:​
    public class MainActivity extends AppCompatActivity {​
        private Handler mHandler = new Handler() {​
            @Override​
            public void handleMessage(Message msg) {​
                // 处理消息​
            }​
        };​
    ​
        @Override​
        protected void onCreate(Bundle savedInstanceState) {​
            super.onCreate(savedInstanceState);​
            setContentView(R.layout.activity_main);​
            mHandler.sendEmptyMessageDelayed(0, 10000);​
        }​
    }

由于 mHandler 是 Handler 的非静态匿名内部类的实例,它持有外部类 Activity 的引用。当 Activity 退出时,如果消息队列中还有未处理的消息或正在处理消息,而 Message 持有 mHandler 实例的引用,mHandler 又持有 Activity 的引用,导致 Activity 的内存资源无法及时回收。​

你可能感兴趣的:(android,性能优化,gitee)