美文网首页面试AndroidAndroid开发感悟
Android App优化之提升你的App启动速度之实例挑战

Android App优化之提升你的App启动速度之实例挑战

作者: anly_jun | 来源:发表于2016-08-29 08:51 被阅读29452次

    系列文:

    1. 背景:Android App优化, 要怎么做?
    2. Android App优化之性能分析工具
    3. Android App优化之提升你的App启动速度之理论基础
    4. Android App优化之提升你的App启动速度之实例挑战
    5. Android App优化之Layout怎么摆
    6. Android App优化之ANR详解
    7. Android App优化之消除卡顿
    8. Android App优化之内存优化
    9. Android App优化之持久电量
    10. Android App优化之如何高效网络请求

    1, 代码分析

    之前写的Github App为例.

    因为这个App集成了Bugly, Push, Feedback等服务, 所以Application的onCreate有很多第三方平台的初始化工作...

    public class GithubApplication extends MultiDexApplication {
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            // init logger.
            AppLog.init();
    
            // init crash helper
            CrashHelper.init(this);
    
            // init Push
            PushPlatform.init(this);
    
            // init Feedback
            FeedbackPlatform.init(this);
    
            // init Share
            SharePlatform.init(this);
    
            // init Drawer image loader
            DrawerImageLoader.init(new AbstractDrawerImageLoader() {
                @Override
                public void set(ImageView imageView, Uri uri, Drawable placeholder) {
                    ImageLoader.loadWithCircle(GithubApplication.this, uri, imageView);
                }
            });
        }
    }
    

    当前冷启动效果:


    code_start_before_optimize

    可以看到启动时白屏了很长时间.

    2, Traceview上场

    接下来我们结合我们上文的理论知识, 和介绍的Traceview工具, 来分析下Application的onCreate耗时.

    在onCreate开始和结尾打上trace.

    Debug.startMethodTracing("GithubApp");
    ...
    Debug.stopMethodTracing();
    

    运行程序, 会在sdcard上生成一个"GithubApp.trace"的文件.

    注意: 需要给程序加上写存储的权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    

    通过adb pull将其导出到本地

    adb pull /sdcard/GithubApp.trace ~/temp
    

    广告: adb的众多用法, 可以参考我的另一篇文

    打开DDMS分析trace文件

    ddms_open_trace

    分析trace文件

    traceview_ui
    1. 在下方的方法区点击"Real Time/Call", 按照方法每次调用耗时降序排.
    2. 耗时超过500ms都是值得注意的.
    3. 看左边的方法名, 可以看到耗时大户就是我们用的几大平台的初始化方法, 特别是Bugly, 还加载native的lib, 用ZipFile操作等.
    4. 点击每个方法, 可以看到其父方法(调用它的)和它的所有子方法(它调用的).
    5. 点击方法时, 上方的该方法执行时间轴会闪动, 可以看该方法的执行线程及相对时长.

    3, 调整Application onCreate再试

    既然已经知道了哪些地方耗时长, 我们不妨调整下Application的onCreate实现, 一般来说我们可以将这些初始化放在一个单独的线程中处理, 为了方便今后管理, 这里我用了一个InitializeService的IntentService来做初始化工作.

    明确一点, IntentService不同于Service, 它是工作在后台线程的.

    InitializeService.java代码如下:

    package com.anly.githubapp.compz.service;
    
    import android.app.IntentService;
    import android.content.Context;
    import android.content.Intent;
    import android.graphics.drawable.Drawable;
    import android.net.Uri;
    import android.widget.ImageView;
    
    import com.anly.githubapp.common.wrapper.AppLog;
    import com.anly.githubapp.common.wrapper.CrashHelper;
    import com.anly.githubapp.common.wrapper.FeedbackPlatform;
    import com.anly.githubapp.common.wrapper.ImageLoader;
    import com.anly.githubapp.common.wrapper.PushPlatform;
    import com.anly.githubapp.common.wrapper.SharePlatform;
    import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
    import com.mikepenz.materialdrawer.util.DrawerImageLoader;
    
    /**
     * Created by mingjun on 16/8/25.
     */
    public class InitializeService extends IntentService {
    
        private static final String ACTION_INIT_WHEN_APP_CREATE = "com.anly.githubapp.service.action.INIT";
    
        public InitializeService() {
            super("InitializeService");
        }
    
        public static void start(Context context) {
            Intent intent = new Intent(context, InitializeService.class);
            intent.setAction(ACTION_INIT_WHEN_APP_CREATE);
            context.startService(intent);
        }
    
        @Override
        protected void onHandleIntent(Intent intent) {
            if (intent != null) {
                final String action = intent.getAction();
                if (ACTION_INIT_WHEN_APP_CREATE.equals(action)) {
                    performInit();
                }
            }
        }
    
        private void performInit() {
            AppLog.d("performInit begin:" + System.currentTimeMillis());
    
            // init Drawer image loader
            DrawerImageLoader.init(new AbstractDrawerImageLoader() {
                @Override
                public void set(ImageView imageView, Uri uri, Drawable placeholder) {
                    ImageLoader.loadWithCircle(getApplicationContext(), uri, imageView);
                }
            });
    
            // init crash helper
            CrashHelper.init(this.getApplicationContext());
    
            // init Push
            PushPlatform.init(this.getApplicationContext());
    
            // init Feedback
            FeedbackPlatform.init(this.getApplication());
    
            // init Share
            SharePlatform.init(this.getApplicationContext());
    
            AppLog.d("performInit end:" + System.currentTimeMillis());
        }
    }
    

    GithubApplication的onCreate改成:

    public class GithubApplication extends MultiDexApplication {
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            // init logger.
            AppLog.init();
    
            InitializeService.start(this);
        }
    }
    

    看看现在的效果:

    improved-1.gif

    可以看到提升了很多, 然后还有一点瑕疵, 就是起来的时候会有一个白屏, 如果手机较慢的话, 这个白屏就会持续一段时间, 不太友好.

    那么还有没有什么办法优化呢?

    4, 给我们的应用窗口弄一个PlaceHolder

    Android最新的Material Design有这么个建议的. 建议我们使用一个placeholder UI来展示给用户直至App加载完毕.

    怎么做呢?

    给Window加上背景

    如第3节所言, 当App没有完全起来时, 屏幕会一直显示一块空白的窗口(一般来说是黑屏或者白屏, 根据App主题).

    前文理论基础有说到, 这个空白的窗口展示跟主题相关, 那么我们是不是可以从首屏的主题入手呢? 恰好有一个windowBackground的主题属性, 我们来给Splash界面加上一个主题, 带上我们想要展示的背景.

    做一个logo_splash的背景:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- 底层白色 -->
        <item android:drawable="@color/white" />
        
        <!-- 顶层Logo居中 -->
        <item>
            <bitmap
                android:gravity="center"
                android:src="@drawable/ic_github" />
        </item>
    </layer-list>
    

    弄一个主题:

    <style name="SplashTheme" parent="AppTheme">
        <item name="android:windowBackground">@drawable/logo_splash</item>
    </style>
    

    将一个什么不渲染布局的Activity作为启动屏

    写一个什么都不做的LogoSplashActivity.

    public class LogoSplashActivity extends BaseActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            
            // 注意, 这里并没有setContentView, 单纯只是用来跳转到相应的Activity.
            // 目的是减少首屏渲染
            
            if (AppPref.isFirstRunning(this)) {
                IntroduceActivity.launch(this);
            }
            else {
                MainActivity.launch(this);
            }
            finish();
        }
    }
    

    在AndroidManifest.xml中设置其为启动屏, 并加上主题:

    <activity
      android:name=".ui.module.main.LogoSplashActivity"
      android:screenOrientation="portrait"
      android:theme="@style/SplashTheme">
      <intent-filter>
          <action android:name="android.intent.action.MAIN"/>
          <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    

    5, 最终的效果

    让我们来看下最终的效果:

    improved-2.gif

    相比之前, 呈现给用户的不再是一个白屏了, 带上了logo, 当然这个背景要显示什么, 我们可以根据实际情况来自定义.

    这种优化, 对于有些Application内的初始化工作不能移到子线程做的情况, 是非常友好的. 可以避免我们的App长时间的呈现给用户一个空白的窗口.

    6, 结语

    照例, 总结下.
    这次关于App启动时间的优化, 写了两篇. 写这么多, 还是想传达下个人做技术的思想, 也算是个人的经验回顾, 抛砖引玉.

    实际场景可能远比这个复杂,在此更多的提供一种分析思路~欢迎扩展

    矫情了, 还是总结下本文相关的吧:

    1. Application的onCreate中不要做太多事情.
    2. 首屏Activity尽量简化.
    3. 善用工具分析.
    4. 多阅读官方文档, 很多地方貌似无关, 实际有关联, 例如这次就用了Material Design文档中的解决方案.

    本文完整源码, 请移步Github

    相关文章

      网友评论

      • b3975341e31d:楼主这个就相当于放在一个子线程启动,感觉开Service还麻烦,不如直接new Thread()
      • 流金岁月_难忘憶秋年:楼主,据说8.0的SDK不能这样使用IntentService了,有木有其他好的解决方法:blush:
      • 50871e6bd13e:bugly在intentservice里初始化的话可能会上报失败,怎么解决的
        Dear月:会么?
      • 1s的消失:这里为什么用IntentService而不是直接开启子线程异步呢?
      • CROAD:博主,我将库/sdk初始化放在intentService里,譬如okhttp,视频sdk等,结果发现视频sdk没有初始化。这是为什么呢
        Dear月:这个还是一个Service 因此一定要配置哟,不然不能启用
        Veken_Fly:可能是你的视频sdk不能放在intentService里面初始化,必须放到Application里面
      • EvanPoison:出现了一些问题,第一个问题是:intentService被创建了两次,onHandlerIntent()也回调了两次,发现把激光推送的注释了,就只创建一次;第二个问题是,我在splashActivity设置了主题,主题了有张图片,当我进入主界面,回到桌面,再点APP图标的时候,看到了这张图片,然后才回到主界面
        one_cup:@南朝小木瓜 因为有两个进程,你在引入一些推送的时候,为了保证推送的送达率,他们会另起一个进程,起两个进程就会初始化两次Application,修改的方法就是在初始化的方法里面加进程的判断
        南朝小木瓜:补充一下,我用友盟推送也是这样,而且在有的机型上会多次调用onHandlerIntent导致APP崩溃,只要把推送初始化的代码放回Application里就行了
        8b74161fe687:intentService被创建了两次,应该是创建了两个process导致的,仔细查看下代码
      • 沐风雨木:本渣渣还没能力评论,但是可以赞一下 - -
      • c6ca7305e330:intentService是一个单独线程,启动服务后application的oncreate方法会一直重复调用,博主有什么好的方法
      • jiushiwo:可以,不管其他的 现在启动页设置style style中设置背景这一条已经帮了忙了
      • i冰点:大神,为windowBackground设置了一个图片资源,在splash页结束之后,怎么释放这个资源文件占的内存🤔🤔🤔?
        meskal:4.0之后不需要手动释放了
      • andyhaha007:支持一哈
      • 流水不腐小夏:IntentService是有个单独线程的,有些启动放在这个里面,可能会有问题。

        我试过使用hook 的方法获取app的Application,在4.x以下,在非UI线程中获取会有问题。

        我这边在第一个Activity的onResume中post一个初始化Runnable,然后在这个runnable中初始化,楼主你怎么看看。
        流水不腐小夏:@辉_95ea 是这样的,可以放在主线程中初始化,但是此次Activity已经完全打开了,有一种秒开的感觉,而不是加载完以后再打开Activity。不太一样,简单理解就是延迟加载。
        sunhuihui:handler.postRunable(runnable) 这个runnable的工作,不是一样在主线程执行吗?

        handler.post 只是把runnable的工作提交到与之对应的looper 中调度; 如果你是直接在Activity.中new Handler() ,那么runnable的工作,不一样在主线程中执行吗?
      • ac2c0732fd94:博主这一系列写的很不错,这篇的话推荐添加关于 SDK 采用优先级分块加载的策略,会更完美。
      • 7a9d7a958cab:还是启动慢,几秒才是启动,可以加你q q吗,我的892256290
      • bb4fe0bc50e1:楼主,我照你的写的,但是运行时报错了,说是ImageLoader在使用前没初始化。因为我要用ImageLoader,本来在Application中初始化可以运行,但是在IntentService中就报错。
        ac2c0732fd94:按照这些 SDK 的优先级来分块加载:比如有个启动页,这个启动页是个2秒的启动页面,那么很多 SDK 初始化工作可以迁移到这个启动页的 Activity 的 onCreate 中和 主页面 MainActivity 中。
        一般来说 启动页 做的是初始化工作,问题出现相对于其他逻辑代码低很多,并且此页面没有需要被观察的地方,因此这里可以做那些需要在从到 MainActivity 主页面之后收集数据或者使用频次高且在 MainActivity 这个层级的 SDK 初始化,但这里也需要根据启动页定义的过渡时间来分配(至于时间的计算就要用到博主说的 TraceView 去观测)。至于那些层级深的(比如从首页到B页面到C页面后才调用的资源)就可以使用 IntentService 去异步加载。这些都要考虑加载失败的情况。
        骑熊的道士:@bb4fe0bc50e1 建议打开界面就要使用的功能,还是放在这个Activity的OnCreate()方法中,其他的话放在IntentService中进行初始化 我是这么想的
      • 1琥珀川1:放在intentservice中未初始化完就跳到主页面但是主页面需要用到这个未初始化的类怎么处理?
        EvanPoison:我也想问这个问题
      • 捡淑:马克
      • 皮球二二:博主,我不知道第三方的库是不是能放在子线程里面,这个怎么判断呢?
        sunhuihui:如果第三方的API不是在Mainactivity中立即用到,就可以延迟初始化,建议使用单例去初始化它

        如果是MainActivity中立即使用,那么也可以使用loader 异步处理
        anly_jun:@r17171709 需要初始化的第三方, 一般分为两种, 一种是第三方平台的SDK(推送, 分享, 反馈, 统计等) 这个可以通过看其SDK文档, 结合业务需求考虑. 例如分享, 反馈一般不是必须要应用一开启就能用的, 这类业务一般层级比较深, 有足够的理由让它们在后台异步初始化. 另外一种第三方是第三方的库, 一般来说, 建议阅读其源码, 了解其实现原理, 再决定是否放在后台初始化.
      • ramblejoy:学习
      • 92d250077f02:release不会有冷启动 冷启动用第一次的几秒钟换取调试开发的速度难道不值吗。 gradle 2.10以及之前没有冷启动
        sunhuihui:如果是debug阶段当然是可以的,如果是release的话,估计 很少有用户能忍受加载时间太长
        anly_jun:@哈哈哈rbdnxn 这篇文讲的是应用的启动速度,跟调试开发的速度没有关系的~

      本文标题:Android App优化之提升你的App启动速度之实例挑战

      本文链接:https://www.haomeiwen.com/subject/igkxettx.html