前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >App 启动优化

App 启动优化

作者头像
Yif
发布2020-04-23 17:41:56
1.3K0
发布2020-04-23 17:41:56
举报
文章被收录于专栏:Android 进阶Android 进阶

Android 启动时间测量方式

adb shell start am -W packname/首屏activity

输出: - This time:最后一个activity启动耗时 - Totaltime:所有activity 启动耗时 - WaitTime:AMS启动activity总耗时

手动打点设置启动结束

不在onWindowFocusChanged中进行,它只是首帧时间 在第一条数据获取结束时表示启动结束,进行获取启动完成时间

Systrace

轻量级,开销小 直观反映CPU利用率

cpu time与 wall time - cpu time:代码消耗CPU时间(重点指标,也是优化方向) - wall time:代码执行时间

为什么两个时间不一样:锁冲突,比如有一个a方法,它需要获取一把锁,但是这个锁被别人获取者,代码就在a方法停了下来,一直等待,造成wall time比cpu time长,cpu无消耗

异步优化

Application中进行初始化框架操作,可以放在线程池中进行,使用线程池中的线程数量,比如核心与非核心线程,为了可以更好的利用线程数量,可以参考AsyncTask源码中构建的线程池,进行操作。但是某些框架初始化必须在Application onCreate中初始化完成,所以可以使用CountDownLatch让主线程进行等待,在子线程池中进行,CountDownLatch操作。

public class MyApplication extends Application {
    /**
     * 使用Asynctask 中创建线程池的方法,进行创建线程池,根据CPU核心数量创建线程池操作
     */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    /**
     * 相当于加一个锁,被满足一次条件
     */
    private CountDownLatch mCountDownLatch = new CountDownLatch(1);
 
    @Override
    public void onCreate() {
        super.onCreate();
        ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        //分开进行初始化操作,可以更好的利用CPU核心创建的线程数量,时间会非常短
        service.submit(new Runnable() {
            @Override
            public void run() {
                //初始化bugly
                initBugly();
            }
        });
 
        service.submit(new Runnable() {
            @Override
            public void run() {
                //初始化Fresco 图片框架
                initFresco();
            }
        });
 
        service.submit(new Runnable() {
            @Override
            public void run() {
                //初始化Weex 框架
                initWeex();
                mCountDownLatch.countDown();
            }
        });
        try {
            //不满足条件会一直等待,这里等待weex框架初始化完成
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    private void initWeex() {
    }
 
    private void initFresco() {
    }
 
    private void initBugly() {
    }
}
 

使用ThreadPoolExecutor创建线程池

public class DispatcherExecutor {
    private static ThreadPoolExecutor sCPUThreadPoolExecutor;
    private static ExecutorService sIOThreadPoolExecutor;
 
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
 
    public static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));
    private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;
    private static final int KEEP_ALIVE_SECONDS = 5;
    private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<>();
    private static final DefaultThreadFactory sThreadFactory = new DefaultThreadFactory();
    private static final RejectedExecutionHandler sHandler = new RejectedExecutionHandler() {// 一般不会到这里
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            Executors.newCachedThreadPool().execute(r);
        }
    };
 
    /**
     * 获取CPU线程池
     * @return
     */
    public static ThreadPoolExecutor getCPUExecutor() {
        return sCPUThreadPoolExecutor;
    }
 
    /**
     * 获取IO线程池
     * @return
     */
    public static ExecutorService getIOExecutor() {
        return sIOThreadPoolExecutor;
    }
 
    /**
     * The default thread factory.
     */
    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
 
        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "TaskDispatcherPool-" +
                    poolNumber.getAndIncrement() +
                    "-Thread-";
        }
 
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
 
    static {
        sCPUThreadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory, sHandler);
        sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
        sIOThreadPoolExecutor = Executors.newCachedThreadPool(sThreadFactory);
    }
 
}
 

异步优化注意

  1. 不符合异步要求
  2. 需要在某些阶段完成,比如使用CountDownLatch
  3. 区分CPU密集型与IO密集型任务
    • CPU密集型 :线程数量=核心数+1进行设定
    • IO密集型:线程数量=核心数*(1+平均等待时间/平均工作时间)进行设定

异步痛点

  1. 代码不优雅,维护成本高
  2. 创建场景不好处理,不如依赖关系

冷启动(Cold start)

冷启动是指APP在手机启动后第一次运行,或者APP进程被kill掉后在再次启动。 可见冷启动的必要条件是该APP进程不存在,这就意味着系统需要创建进程,APP需要初始化。在这三种启动方式中,冷启动耗时最长,对于冷启动的优化也是最具挑战的。因此本文重点谈论的是对冷启动相关的优化。

温启动(Warm start)

App进程存在,当时Activity可能因为内存不足被回收。这时候启动App不需要重新创建进程,但是ActivityonCrate还是需要重新执行的。场景类似打开淘宝逛了一圈然后切到微信去聊天去了,过了半小时再次回到淘宝。这时候淘宝的进程存在,但是Activity可能被回收,这时候只需要重新加载Activity即可。

热启动(Hot start)

App进程存在,并且Activity对象仍然存在内存中没有被回收。可以重复避免对象初始化,布局解析绘制。 场景就类似你打开微信聊了一会天这时候出去看了下日历 在打开微信 微信这时候启动就属于冷启动。 在最近任务给App加锁和启动方式有什么关系

某些厂商为了用户体验提供了给APP上锁的功能,目的就是让用户自己做主是上锁的APP不被杀,启动的时候不会处于冷启动方式,但是加锁也不是万能的,Low memory killer在内存极度吃紧的情况下也会杀死加锁APP,在此启动时也将以冷启动方式运行。

AI和启动方式有什么关系

AI在进程管理方面可谓是大有可为。MIUI10发布了进程AI唤醒功能,是APP启动速度远超友商。这其中的道理简单说就是学习用户的使用习惯,提前将App进程创建好,当用户打开APP时不会出去冷启动。比如你是微信重度用户你发现用了MIUI10就再也见不到微信启动页面的那个地球了,这就是AI唤醒的功劳。

Android APP启动时会出现白屏

由于应用程序启动时冷启动,系统会默认在启动时启动空白窗口 应用程序启动有三种状态,每种状态都会影响应用程序对用户可见所需的时间:冷启动,热启动和温启动。 在冷启动时,应用程序从头开始。在其他状态下,系统需要将正在运行的应用程序从后台运行到前台。我们建议您始终根据冷启动的假设进行优化。这样做也可以改善热启动和温启动的性能。 在冷启动开始时,系统有三个任务。这些任务是: - 加载并启动应用程序。 - 启动后立即显示应用程序空白的启动窗口。 - 创建应用程序进程。

一旦系统创建应用程序进程,应用程序进程就会负责下一阶段。这些阶段是: - 创建app对象. - 启动主线程(main thread). - 创建应用入口的Activity对象. - 填充加载布局Views - 在屏幕上执行View的绘制过程measure -> layout -> draw

应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户可以开始使用该应用程序。

启动优化

透明主题优化

为了解决启动窗口白屏问题,许多开发者使用透明主题来解决这个问题,但是治标不治本。 虽然解决了上面这个问题,但是仍然有些不足。

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>
 

设置闪屏图片主题

为了更顺滑无缝衔接我们的闪屏页,可以在启动 Activity 的 Theme中设置闪屏页图片,这样启动窗口的图片就会是闪屏页图片,而不是白屏。

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:windowBackground">@mipmap/launch</item> //闪屏页图片
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowContentOverlay">@null</item>
</style>
 

代码优化

Application 优化 Application 作为 应用程序的整个初始化配置入口,时常担负着它不应该有的负担~ 有很多第三方组件(包括App应用本身)都在 Application 中抢占先机,完成初始化操作。 但是在 Application 中完成繁重的初始化操作和复杂的逻辑就会影响到应用的启动性能 通常,有机会优化这些工作以实现性能改进,这些常见问题包括: - 复杂繁琐的布局初始化 - 阻塞主线程 UI 绘制的操作,如 I/O 读写或者是网络访问. - Bitmap 大图片或者 VectorDrawable加载 - 其它占用主线程的操作

我们可以根据这些组件的轻重缓急之分,对初始化做一下分类 : - 必要的组件一定要在 主线程中立即初始化(入口 Activity 可能立即会用到) - 组件一定要在 主线程中初始化,但是可以延迟初始化。 - 组件可以在 子线程中初始化。

放在子线程的组件初始化建议延迟初始化 ,这样就可以了解是否会对项目造成影响! 所以对于上面的分析,我们可以在项目中 Application 的加载组件进行如下优化 : - 将Bugly,x5内核初始化,SP的读写,友盟等组件放到子线程中初始化。(子线程初始化不能影响到组件的使用)

        new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程的优先级,不与主线程抢资源
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //子线程初始化第三方组件
                Thread.sleep(5000);//建议延迟初始化,可以发现是否影响其它功能,或者是崩溃!
            }
        }).start();
 

将需要在主线程中初始化但是可以不用立即完成的动作延迟加载(原本是想在入口 Activity 中进行此项操作,不过组件的初始化放在 Application 中统一管理为妙.)

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //延迟初始化组件
            }
        }, 3000);
 

闪屏页业务优化

应用App通常会设置一个固定的闪屏页展示时间,例如2000ms,所以我们可以根据用户手机的运行速度,对展示时间做出调整,但是总时间仍然为 2000ms。 闪屏页政展示总时间 = 组件初始化时间 + 剩余展示时间。 也就是2000ms的总时间,组件初始化了800ms,那么就再展示1200ms即可 获取闪屏时间 Application 初始化后会调用 attachBaseContext() 方法,再调用 Application 的 onCreate(),再到入口 Activity的创建和执行 onCreate() 方法。所以我们就可以在 Application 中记录启动时间。

//Application
 
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        SPUtil.putLong("application_attach_time", System.currentTimeMillis());//记录Application初始化时间
    }
 

有了启动时间,我们得知道入口的 Acitivty 显示给用户的时间(View绘制完毕),在onWindowFocusChanged()的回调时机中表示可以获取用户的触摸时间和View的流程绘制完毕,所以我们可以在这个方法里记录显示时间。

//入口Activity
 
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
 
          long appAttachTime = SPUtil.getLong("application_attach_time");
          long diffTime = System.currentTimeMillis() - appAttachTime;//从application到入口Acitity的时间
 
         //所以闪屏页展示的时间为 2000ms - diffTime.
    }
 

所以我们就可以动态的设置应用闪屏的显示时间,尽量让每一部手机展示的时间一致,这样就不会让手机配置较低的用户感觉漫长难熬的闪屏页时间(例如初始化了2000ms,又要展示2000ms的闪屏页时间.),优化用户体验。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020年4月22日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Android 启动时间测量方式
  • 手动打点设置启动结束
  • Systrace
  • 异步优化
    • 异步优化注意
      • 异步痛点
      • 冷启动(Cold start)
      • 温启动(Warm start)
      • 热启动(Hot start)
      • AI和启动方式有什么关系
      • Android APP启动时会出现白屏
      • 启动优化
        • 透明主题优化
          • 设置闪屏图片主题
            • 代码优化
              • 闪屏页业务优化
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档