启动优化之一——启动分析及优化方案

启动优化之一——启动分析及优化方案博客结构1.前言2.启动类型1.冷启动2.温启动3.热启动3.冷启动流程3.冷启动分析工具及方法1.命令行2.代码打印3.高清摄像机4.通用优化方案1.前置2.并发3.延迟5.业务优化方案1.原理6.站在巨人肩膀用简单通俗易懂的话来记录自己对对象存活判断算法的理解1.前言一个应用的性能如何,冷启动是个重要的衡量指标,毕竟用户第一次使用应用时,多久能进入页面(包括首页、启动页、广告页等),是用户的第一感官。对于一般应用,有启动页和广告页,对启动速度的要求相对于来说降低了一些,毕竟,进来就是启动页,只是时

用简单通俗易懂的话来记录自己对对象存活判断算法的理解

1.前言

一个应用的性能如何,冷启动是个重要的衡量指标,毕竟用户第一次使用应用时,多久能进入页面(包括首页、启动页、广告页等),是用户的第一感官。对于一般应用,有启动页和广告页,对启动速度的要求相对于来说降低了一些,毕竟,进来就是启动页,只是时间停留时间长短问题(当然,不是说对时间不敏感,你要是在启动页停留个3,5s,谁也受不了。不过,话说,谷歌统计的所有应用的平均时间竟然是4000多ms,这还是挺吃惊的)。
我所做的是个系统工具类,没有启动页,直接就是干到首页。又因为是系统应用,要有和桌面有一体化的感觉,所以,对启动速度要求极高。同时,又由于要适配全机型(Android5.0 -Android 12.0),前期没有做版本区分,所以,挑战来说更大。无论怎么说吧,经过一轮又一轮的优化,低端机达到了550ms以内,高端机更是在300ms以内。整体启动速度提供65%左右。
其实,现在写冷启动的并不多,为啥呢?有一些是公司并不在意,个人也就不在意,还有就是业务有启动页,可能并不明显。最后也有一部分原因来讲就是应用层去优化有限,更多的是业务逻辑的优化,感觉写不出创意来,借鉴于此,我这篇博客也是先简单讲下冷启动的流程,看下那些我们可以去做,以及一些通用通吃的方案,一些结合业务做的优化(图片的加载优化、耗时业务的巧妙处理),期望大家看到后引起更多的思考和更多的交流学习。

2.启动类型

根据启动的场景,简单的将应用的启动分为三种类型,而我们一般的处理的都是在冷启动这种场景

1.冷启动

应用从零开始,进程和页面都没有创建。
场景:任务后台杀死进程、桌面长安点击应用信息,强制停止(以OPPO手机为例)

2.温启动

再次启动时,只是重建首页页面。
场景:首页点击返回键,进入后台。再次点击应用图标或者从后台任务里点击

3.热启动

再次启动时,进程和页面都未曾销毁,直接显示页面。
场景:点击Home键,应用进入后台任务

3.App启动

谈到App的启动,说实话,每次要梳理它的时候,都要重新看一遍,每次都感觉好像懂了,好想知道了,但是,又好像哪点不知道。所以,这次,只是为了冷启动的分析,所以,咱们用图梳理它的逻辑,期望能加深印象。

1.App启动语言描述

1.点击图标,Launcher向AMS请求启动app
2.AMS收到请求后,记录app的信息,并告知Launcher进入pause状态
3.Launcher进入pause状态后,告知AMS
4.AMS检测新的app进程时候已经启动,否则通过Zygote创建新的进程并启动ActivityThread的main方法
5.进程创建好后,调用上面的ActivityThread.main()
6.ActivityThread中H处理需要启动Activity的请求消息

2.App启动进程图述

在这个进程启动图中,只是节选了我们正常冷启动的四个进程间的唤起与创建,详细的描述了App进程的调用图,是为了让大家对流程优化有更清晰的认知。
有图可以知道,View的绘制是在生命周期的onResume之后
在这里插入图片描述

3.App启动类图述

类图是从网上down下来的,如有雷同,纯属巧合。这是基于Android8.0的源码类图
在这里插入图片描述
这是一个详细的类图,对于类图关键的几个类做个简单解释
1.ActivityThread是个Thread么?

/**
 * This manages the execution of the main thread in an
 * application process, scheduling and executing activities,
 * broadcasts, and other operations on it as the activity
 * manager requests.
 *
 * { 
   @hide}
 */

不是哟,正如注释,它只是主线程的执行管理类,对四大组件进行调用。
2.View的测量绘制在Activity的onResume之后?
是的,一验证一源码
一验证方式:
(1)在onResume中进行控件高宽的获取
(2)post发送个消息后再测量

  protected void onResume() { 
   
        super.onResume();
        LogUtils.d("haha","measureSize begin");
        measureSize();
        LogUtils.d("haha","measureSize end");

        viewDelegate.mRadioGroup.post(new Runnable() { 
   
            @Override
            public void run() { 
   
                LogUtils.d("haha","after sleep measureSize begin");
                measureSize();
            }
        });

    }
 private void measureSize() { 
   
        int radioGroupHeight = viewDelegate.mRadioGroup.getMeasuredHeight();
        int radioGroupWidth = viewDelegate.mRadioGroup.getMeasuredWidth();

        int radioButtonHeight = viewDelegate.rbUser.getMeasuredHeight();
        int radioButtonWidth = viewDelegate.rbUser.getMeasuredWidth();

        LogUtils.d("haha","radioGroupHeight:"+radioGroupHeight+"radioGroupWidth:"+radioGroupWidth
        +"radioButtonHeight:"+radioButtonHeight+"radioButtonWidth:"+radioButtonWidth);
    }

結果如下:

2021-05-30 10:49:28.909 12221-12221/ D/haha: packageName;com.fcbox.hivestation postion:0
2021-05-30 10:49:29.168 12221-12221/ D/haha: measureSize begin
2021-05-30 10:49:29.168 12221-12221/ D/haha: radioGroupHeight:0radioGroupWidth:0radioButtonHeight:0radioButtonWidth:0
2021-05-30 10:49:29.168 12221-12221/ D/haha: measureSize end
2021-05-30 10:49:34.169 12221-12221/? D/haha: after sleep measureSize begin
2021-05-30 10:49:34.170 12221-12221/? D/haha: radioGroupHeight:186radioGroupWidth:1080radioButtonHeight:186radioButtonWidth:360

这个结果说明了什么?说明在onResume中,View竟没有测量,布局,绘制。而是在super.onResume的一些队列事件处理后,才能测量出宽高。好了,接下来,从代码层面去解释这个问题

一源码方式:

1.ActivityThread.handleResumeActivity做了啥?
  final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { 
   
        ActivityClientRecord r = mActivities.get(token);
        if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) { 
   
            return;
        }

        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) { 
   
            final Activity a = r.activity;

            if (localLOGV) Slog.v(
                TAG, "Resume " + r + " started activity: " +
                a.mStartedActivity + ", hideForNow: " + r.hideForNow
                + ", finished: " + a.mFinished);

            final int forwardBit = isForward ?
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

            // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) { 
   
                try { 
   
                    willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) { 
   
                    throw e.rethrowFromSystemServer();
                }
            }
            if (r.window == null && !a.mFinished && willBeVisible) { 
   
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) { 
   
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) { 
   
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient && !a.mWindowAdded) { 
   
                    a.mWindowAdded = true;
                    wm.addView(decor, l);// -----------------------------1
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if (!willBeVisible) { 
   
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }

            // Get rid of anything left hanging around.
            cleanUpPendingRemoveWindows(r, false /* force */);

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) { 
   
                if (r.newConfig != null) { 
   
                    performConfigurationChangedForActivity(r, r.newConfig, REPORT_TO_ACTIVITY);
                    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                            + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig);
                    r.newConfig = null;
                }
                if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                        + isForward);
                WindowManager.LayoutParams l = r.window.getAttributes();
                if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                            | forwardBit;
                    if (r.activity.mVisibleFromClient) { 
   
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        wm.updateViewLayout(decor, l);
                    }
                }
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) { 
   
                    r.activity.makeVisible();
                }
            }

            if (!r.onlyLocalRequest) { 
   
                r.nextIdle = mNewActivities;
                mNewActivities = r;
                if (localLOGV) Slog.v(
                    TAG, "Scheduling idle handler for " + r);
                Looper.myQueue().addIdleHandler(new Idler());
            }
            r.onlyLocalRequest = false;

            // Tell the activity manager we have resumed.
            if (reallyResume) { 
   
                try { 
   
                    ActivityManagerNative.getDefault().activityResumed(token);
                } catch (RemoteException ex) { 
   
                    throw ex.rethrowFromSystemServer();
                }
            }

        } else { 
   
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try { 
   
                ActivityManagerNative.getDefault()
                    .finishActivity(token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) { 
   
                throw ex.rethrowFromSystemServer();
            }
        }
    }

大家可以看到//—————-1的地方才开始建立window与View的关联,然后在ViewRootImpl中调用
requestLayout—>scheduleTraversals去渲染第一帧。

ViewRootImpl:
    public void requestLayout() { 
   
        if (!mHandlingLayoutInLayoutRequest) { 
   
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

    void scheduleTraversals() { 
   
        if (!mTraversalScheduled) { 
   
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//---------------2
            if (!mUnbufferedInputDispatch) { 
   
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

Choreographer:
  private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) { 
   
        if (DEBUG_FRAMES) { 
   
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) { 
   
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) { 
   
                scheduleFrameLocked(now);
            } else { 
   
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);//--------3
            }
        }
    }

大家看到了2和3的标记了吗?这也就是为啥在super.onResume后,post一个消息可以读取到控件大小的原因。
3.setCotentView是不是完成了绘制?
这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。

4.冷启动分析工具及方法

1.命令行

//查看冷启动时间
adb shell am start -S -W 包名/全包名launchActivity

以启动camera为例:这个包名和启动页面有些的手机不是的,所以,可以拿自己项目进行测试,这里只为了说明分析
adb shell am start -W com.mediatek.camera/com.android.camera.CameraActivity
结果;
在这里插入图片描述
该命令具体实现在/frameworks/base/cmds/am/src/com/android/commands/am/Am.java,原理是跨Binder调用ActivityManagerService.startActivityAndWait() 接口,其中返回数据分别调用对应

startTime: 调用startActivityAndWait()的时间点

endTime: 调用startActivityAndWait()函数调用返回的时间点

WaitTime: 调用startActivityAndWait()调用耗时。

再通过之间的计算得到。

来,让我们在一起温故一下咱们四个进程的调用逻辑。那么,这有什么用。知其所以然,比如,我们的测试同学本质兢兢业业的本分工作原则,根据高清摄像头的耗时和命令行耗时做对比,这样肯定不行,因为高清摄像拍照的计算是点击触发开始,而命令行是从AMS的启动开始咯。

2.代码打印

这种方式呢?是大家根据android提供的方法提供的打印,个人并没有使用过。

2021-05-30 12:24:16.917 1165-1334/? I/ActivityManager: Displayed /.ui.activity.MainActivity: +1s1ms (total +2s52ms)

我理解这种方式应该也是和命令行是样的,是从AMS计算的,但是暂时,没有找到有力证据。可以参考下面这个图
在这里插入图片描述

3.高清摄像机

就是利用高清摄像机一帧一帧的拍摄,这种更结合用户的体验,但是,对于应用端的同学是不友好,为啥呢?因为Launcher进程到ASM进程根据机型是不同的。

5.通用优化策略

1.前置加载优化

1.原理

利用Application.attachBaseContext提前进行初始化

ActivityThread.attath()->

AMS.attachApplication()->

sendMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG)->

ActivityThread.handleBindApplication()->

Application.attachBaseContext()->

ActivityThread.installContentProviders->

ContentProvider.onCreate()->

AMS.publishContentProviders()->

removeMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG)->

Application.onCreate()

2.优化时间

2021-05-30 15:58:34.812 7269-7269/ D/haha: attachBaseContext.time:1622361514811
2021-05-30 15:58:34.836 7269-7269/ D/haha: onCreate.time:1622361514836
2021-05-30 16:01:23.952 8065-8065/? D/haha: attachBaseContext.time:1622361683952
2021-05-30 16:01:23.974 8065-8065/? D/haha: onCreate.time:1622361683974
2021-05-30 16:01:35.144 8171-8171/? D/haha: attachBaseContext.time:1622361695143
2021-05-30 16:01:35.166 8171-8171/? D/haha: onCreate.time:1622361695166

使用的是华为HUAWEI P20 Pro android 9.0,提速平均下来23ms。虽然不是很大,但是,蚊子腿也是肉。何况,很多场景为了优化1个毫秒,还把数据库给建索引呢!

2.异步加载优化

这个应该是最早大家想到的老三套的方案,虽然是老三套,但是,它的确是能有效提高的很多方式,比如,我们的有些WebView内核初始化需要500ms。因为它必须在主线程中,所以,只能选择延后加载。
2和3的方式需要梳理我们的一些需要初始化的业务的划分。
我司划分的维度如下四个:
1.在Application的主线程
2.在Application的异步线程
3.在Activity的空闲线程
4.即用即初始化

3.延迟加载优化

1.原理

利用 IdleHandler进行空闲处理:I
dleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。

    /**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler { 
   
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

2.优化耗时

和业务执行有关,业务在主线程耗时多少,就能优化多少

6.业务优化方案

1.原理

所谓业务优化策略,就是要结合具体的业务,结合TraceView的耗时市场,进行定位和优化。

2.优化场景

1.布局复杂度高

1.降低布局的层级。
2.对于可延后的场景使用ViewStub。

2.并发加载渲染图片

如果图片的加载使用了AsyncTask,建议将AsyncTask的线程池改为并发。

MyAsyncTask asynct = newMyAsyncTask(task);
asynct.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,0);

有些朋友知道AsyncTask串并行的发展史,就会困惑为啥现在又改回来,其实,我猜测是为了避免大量的抱怨,当并发同事访问一个资源时,有些同学并不会取出并发处理,从而会感觉组件的不好用。

3.Arouter初始化优化

1.原理
Arouter自动加载路由表的插件是使用的通过gradle插桩技术在编译期插入代码来达到自动加载路由表信息
2.实现

1.在 app module 的 build.gradle 中 加入:
apply plugin: 'com.alibaba.arouter'

2.在项目的 build.gradle中加入:
buildscript { 
   
    repositories { 
   
        jcenter()
    }

    dependencies { 
   
        classpath "com.alibaba:arouter-register:1.0.2"
    }
}

4.redex 重排列 class 文件

redex 是 Facebook 开源的一款字节码优化工具。
原理:
简单的说,通过文件重排列的目的,就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 Linux 文件系统的 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。
问题:
对于大型项目会报一些异常,因此未曾引入测试,腾讯进行了处理,大家可以参考

5.针对verifyClass的hook处理

在类加载的过程中通过 Hook 去掉类验证的过程,可以在 systrace 生成的文件中看到 verifyClass 过程,因为需要校验方法的每一个指令,所以是一个比较耗时的操作(是一种思路,但是,随着对非官方API反射的限制,方案的路更短)

7.站在巨人肩膀

1.App Start Time
这篇博客大家有时间还是建议看下的,看下官方建议咱们怎么玩。
2.市场上最全的冷启动优化方案
3.冷启动优化
4.Bugly的字节码层面redex优化
5.支付宝的重排布优化启动速度
6.Redex官网
7.当前大厂使用的冷启动优化方案
8.应用性能衡量指标
9.Android热修复技术选择和原理分析
10.微信tinker导致冷启动变慢的问题优化
11.历时1年,上百万行代码!首次揭秘手淘全链路性能优化(格式逻辑很清晰)

今天的文章启动优化之一——启动分析及优化方案分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23875.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注