在性能优化的整个知识体系中,最重要的就是稳定性优化,在上一篇文章 《深刻探索Android稳定性优化》 中咱们已经深刻探索了Android稳定性优化的疆域。那么,除了稳定性之外,对于性能纬度来讲,哪一个方面的性能是最重要的呢?毫无疑问,就是应用的启动速度。下面,就让咱们扬起航帆,一块儿来逐步深刻探索Android启动速度优化的奥秘。html
若是咱们去一家餐厅吃饭,在点餐的时候等了半天都没有服务人员过来,可能就没有耐心等待直接走了。java
对于App来讲,也是一样如此,若是用户点击App后,App半天都打不开,用户就可能失去耐心卸载应用。python
启动速度是用户对咱们App的第一体验,打开应用后才能去使用其中提供的强大功能,就算咱们应用的内部界面设计的再精美,功能再强大,若是启动速度过慢,用户第一印象就会不好。linux
所以,拯救App的启动速度,迫在眉睫。android
应用启动的类型总共分为以下三种:git
下面,咱们来详细分析下各个启动类型的特色及流程。github
从点击应用图标到UI界面彻底显示且用户可操做的所有过程。web
耗时最多,衡量标准。算法
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImplshell
首先,用户进行了一个点击操做,这个点击事件它会触发一个IPC的操做,以后便会执行到Process的start方法中,这个方法是用于进程建立的,接着,便会执行到ActivityThread的main方法,这个方法能够看作是咱们单个App进程的入口,至关于Java进程的main方法,在其中会执行消息循环的建立与主线程Handler的建立,建立完成以后,就会执行到 bindApplication 方法,在这里使用了反射去建立 Application以及调用了 Application相关的生命周期,Application结束以后,便会执行Activity的生命周期,在Activity生命周期结束以后,最后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制。
直接从后台切换到前台。
启动速度最快。
只会重走Activity的生命周期,而不会重走进程的建立,Application的建立与生命周期等。
较快,介于冷启动和热启动之间的一个速度。
LifeCycle -> ViewRootImpl
它是GUI管理系统与GUI呈现系统之间的桥梁。每个ViewRootImpl关联一个Window, ViewRootImpl 最终会经过它的setView方法绑定Window所对应的View,并经过其performTraversals方法对View进行布局、测量和绘制。
须要注意的是,这些都是系统的行为,通常状况下咱们是没法直接干预的。
一般到了界面首帧绘制完成后,咱们就能够认为启动已经结束了。
咱们的优化方向就是 Application和Activity的生命周期 这个阶段,由于这个阶段的时机对于咱们来讲是可控的。
在Android Studio Logcat中过滤关键字“Displayed”,能够看到对应的冷启动耗时日志。
使用adb shell获取应用的启动时间
// 其中的AppstartActivity全路径能够省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路径]
复制代码
执行后会获得三个时间:ThisTime、TotalTime和WaitTime,详情以下:
表示最后一个Activity启动耗时。
表示全部Activity启动耗时。
表示AMS启动Activity的总耗时。
通常来讲,只需查看获得的TotalTime,即应用的启动时间,其包括 建立进程 + Application初始化 + Activity初始化到界面显示 的过程。
能够写一个统计耗时的工具类来记录整个过程的耗时状况。其中须要注意的有:
其代码以下所示:
/**
* 耗时监视器对象,记录整个过程的耗时状况,能够用在不少须要统计的地方,好比Activity的启动耗时和Fragment的启动耗时。
*/
public class TimeMonitor {
private final String TAG = TimeMonitor.class.getSimpleName();
private int mMonitord = -1;
// 保存一个耗时统计模块的各类耗时,tag对应某一个阶段的时间
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0;
public TimeMonitor(int mMonitorId) {
Log.d(TAG, "init TimeMonitor id: " + mMonitorId);
this.mMonitorId = mMonitorId;
}
public int getMonitorId() {
return mMonitorId;
}
public void startMonitor() {
// 每次从新启动都把前面的数据清除,避免统计错误的数据
if (mTimeTag.size() > 0) {
mTimeTag.clear();
}
mStartTime = System.currentTimeMillis();
}
/**
* 每打一次点,记录某个tag的耗时
*/
public void recordingTimeTag(String tag) {
// 若保存过相同的tag,先清除
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
Log.d(TAG, tag + ": " + time);
mTimeTag.put(tag, time);
}
public void end(String tag, boolean writeLog) {
recordingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
//写入到本地文件
}
}
public HashMap<String, Long> getTimeTags() {
return mTimeTag;
}
}
复制代码
为了使代码更好管理,咱们须要定义一个打点配置类,以下所示:
/**
* 打点配置类,用于统计各阶段的耗时,便于代码的维护和管理。
*/
public final class TimeMonitorConfig {
// 应用启动耗时
public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}
复制代码
此外,耗时统计可能会在多个模块和类中须要打点,因此须要一个单例类来管理各个耗时统计的数据:
/**
* 采用单例管理各个耗时统计的数据。
*/
public class TimeMonitorManager {
private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;
public synchronized static TimeMonitorManager getInstance() {
if (mTimeMonitorManager == null) {
mTimeMonitorManager = new TimeMonitorManager();
}
return mTimeMonitorManager;
}
public TimeMonitorManager() {
this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
}
/**
* 初始化打点模块
*/
public void resetTimeMonitor(int id) {
if (mTimeMonitorMap.get(id) != null) {
mTimeMonitorMap.remove(id);
}
getTimeMonitor(id).startMonitor();
}
/**
* 获取打点器
*/
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor monitor = mTimeMonitorMap.get(id);
if (monitor == null) {
monitor = new TimeMonitor(id);
mTimeMonitorMap.put(id, monitor);
}
return monitor;
}
}
复制代码
主要在如下几个方面须要打点:
例如,启动时在Application和第一个Activity加入打点统计:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}
复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
super.onCreate(savedInstanceState);
initData();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}
@Override
protected void onStart() {
super.onStart();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}
复制代码
精确,可带到线上,可是代码有侵入性,修改为本高。
一、在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
二、onWindowFocusChanged只是首帧时间,App启动完成的结束点应该是真实数据展现出来的时候(一般来讲都是首帧数据),如列表第一条数据展现,记得使用getViewTreeObserver().addOnPreDrawListener()(在API 16以上能够使用addOnDrawListener),它会把任务延迟到列表显示后再执行,例如,在Awesome-WanAndroid项目的主页就有一个RecyclerView实现的列表,启动结束的时间就是列表的首帧时间,也即列表第一条数据展现的时候。这里,咱们直接在RecyclerView的适配器ArticleListAdapter的convert(onBindViewHolder)方法中加上以下代码便可:
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
LogHelper.i("FeedShow");
return true;
}
});
}
复制代码
具体的实例代码可在 这里查看。
由于用户看到真实的界面是须要有网络请求返回真实数据的,可是onWindowFocusChanged只是界面绘制的首帧时机,可是列表中的数据是须要从网络中下载获得的,因此应该以列表的首帧数据做为启动结束点。
面向切面编程,经过预编译和运行期动态代理实现程序功能统一维护的一种技术。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性下降,提升程序的可重用性,同时大大提升了开发效率。
对哪些方法进行拦截,拦截后怎么处理。
类是对物体特征的抽象,切面就是对横切关注点的抽象。
被拦截到的点(方法、字段、构造器)。
对JoinPoint进行拦截的定义。
拦截到JoinPoint后要执行的代码,分为前置、后置、环绕三种类型。
首先,为了在Android使用AOP埋点须要引入AspectJ,在项目根目录的build.gradle下加入:
classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'
复制代码
而后,在app目录下的build.gradle下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
复制代码
JoinPoint通常定位在以下位置
使用PointCut对咱们指定的链接点进行拦截,经过Advice,就能够拦截到JoinPoint后要执行的代码。Advice一般有如下几种类型:
首先,咱们举一个小栗子:
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
复制代码
在 execution 中的是一个匹配规则,第一个 * 表明匹配任意的方法返回值,后面的语法代码匹配全部Activity中on开头的方法。
其中execution是处理Join Point的类型,在AspectJx中共有两种类型,以下所示:
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
复制代码
在上述代码中,咱们须要注意 不一样的Action类型其对应的方法入参是不一样的,具体的差别以下所示:
ProceedingPoint不一样于JoinPoint,其提供了proceed方法执行目标方法。
使用 Profile 的 CPU 模块能够帮咱们快速找到耗时的热点方法,下面,咱们来详细来分析一下这个模块。
Trace types 有四种,以下所示。
会记录每一个方法的时间、CPU信息。对运行时性能影响较大。
相比于Trace Java Methods会记录每一个方法的时间、CPU信息,它会在应用的Java代码执行期间频繁捕获应用的调用堆栈,对运行时性能的影响比较小,可以记录更大的数据区域。
需部署到Android 8.0及以上设备,内部使用simpleperf跟踪应用的native代码,也能够命令行使用simpleperf。
用于显示应用程序在其生命周期中转换不一样状态的活动,如用户交互、屏幕旋转事件等。
用于显示应用程序 实时CPU使用率、其它进程实时CPU使用率、应用程序使用的线程总数。
列出应用程序进程中的每一个线程,并使用了不一样的颜色在其时间轴上指示其活动。
Profile提供的检查跟踪数据窗口有四种,以下所示:
提供函数跟踪数据的图形表示形式。
右键点击 Jump to source 跳转至指定函数。
将具备相同调用方顺序的彻底相同的方法收集起来。
看顶层的哪一个函数占据的宽度最大(表现为平顶),可能存在性能问题。
咱们在查看上面4个跟踪数据的区域时,应该注意右侧的两个时间,以下所示:
主要作热点分析,用来获得如下两种数据:
首先,咱们能够定义一个Trace静态工厂类,将Trace.begainSection(),Trace.endSection()封装成i、o方法,而后再在想要分析的方法先后进行插桩便可。
而后,在命令行下执行systrace.py脚本,命令以下所示:
python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "com.wanandroid.json.chao" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html
复制代码
具体参数含义以下:
在UIThread一栏能够看到核心的系统方法时间区域和咱们本身使用代码插桩捕获的方法时间区域。
其中,Android Framework 里面一些重要的模块都插入了label信息,用户App中也能够添加自定义的Lable。
覆盖高中低端机型不一样的场景。
须要准确地统计启动耗时。
是不是使用界面显示且用户真正能够操做的时间做为启动结束时间。
闪屏、广告和新手引导这些时间都应该从启动时间里扣除。
Broadcast、Server拉起,启动过程进入后台都须要排除统计。
一些体验不好的用户极可能被平均了。
如2s快开比,5s慢开比,能够看到有多少比例的用户体验好,多少比例的用户比较糟糕。
若是90%用户的启动时间都小于5s,那么90%区间的启动耗时就是5s。
借鉴Facebook的 profilo 工具原理,对启动整个流程进行耗时监控,在后台对不一样的版本作自动化对比,监控新版本是否有新增耗时的函数。
Application、Activity建立以及回调等过程。
使用Activity的windowBackground主题属性预先设置一个启动图片(layer-list实现),在启动后,在Activity的onCreate()方法中的super.onCreate()前再setTheme(R.style.AppTheme)。
按需初始化,特别是针对于一些应用启动时不须要初始化的库,能够等到用时才进行加载。
轮流获取、均分CPU。
优先级高的获取。
设置线程优先级。
它是一种更严格的群组调度策略,主要分为以下两种类型:
由强大的调度器Scheduler集合提供。
不一样类型的Scheduler:
特别适合Hook手段,找Hook点:构造函数或者特定方法,如Thread的构造函数。
这里咱们直接使用维数的 epic 对Thread进行Hook。在attachBaseContext中调用DexposedBridge.hookAllConstructors方法便可,以下所示:
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override protected void afterHookedMethod(MethodHookParam param)throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
LogUtils.i("stack " + Log.getStackTraceString(new Throwable());
}
);
复制代码
从log找到线程建立信息,根据堆栈信息跟相关业务方沟通解决方案。
直接依赖线程库,但问题在于线程库更新可能会致使基础库更新。
目前基础线程池组件位于启动器sdk之中,使用很是简单,示例代码以下所示:
// 若是当前执行的任务是CPU密集型任务,则从基础线程池组件
// DispatcherExecutor中获取到用于执行 CPU 密集型任务的线程池
DispatcherExecutor.getCPUExecutor().execute(YourRunable());
// 若是当前执行的任务是IO密集型任务,则从基础线程池组件
// DispatcherExecutor中获取到用于执行 IO 密集型任务的线程池
DispatcherExecutor.getIOExecutor().execute(YourRunable());
复制代码
具体的实现源码也比较简单,而且我对每一处代码都进行了详细的解释,就不一一具体分析了。代码以下所示:
public class DispatcherExecutor {
/**
* CPU 密集型任务的线程池
*/
private static ThreadPoolExecutor sCPUThreadPoolExecutor;
/**
* IO 密集型任务的线程池
*/
private static ExecutorService sIOThreadPoolExecutor;
/**
* 当前设备能够使用的 CPU 核数
*/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
/**
* 线程池核心线程数,其数量在2 ~ 5这个区域内
*/
private 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;
/**
* 线程池中空闲线程等待工做的超时时间,当线程池中
* 线程数量大于corePoolSize(核心线程数量)或
* 设置了allowCoreThreadTimeOut(是否容许空闲核心线程超时)时,
* 线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。
* 不然,线程会永远等待新的工做。
*/
private static final int KEEP_ALIVE_SECONDS = 5;
/**
* 建立一个基于链表节点的阻塞队列
*/
private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>();
/**
* 用于建立线程的线程工厂
*/
private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory();
/**
* 线程池执行耗时任务时发生异常所须要作的拒绝执行处理
* 注意:通常不会执行到这里
*/
private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
Executors.newCachedThreadPool().execute(r);
}
};
/**
* 获取CPU线程池
*
* @return CPU线程池
*/
public static ThreadPoolExecutor getCPUExecutor() {
return sCPUThreadPoolExecutor;
}
/**
* 获取IO线程池
*
* @return IO线程池
*/
public static ExecutorService getIOExecutor() {
return sIOThreadPoolExecutor;
}
/**
* 实现一个默认的线程工厂
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = 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-" +
POOL_NUMBER.getAndIncrement() +
"-Thread-";
}
@Override
public Thread newThread(Runnable r) {
// 每个新建立的线程都会分配到线程组group当中
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,
S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER);
// 设置是否容许空闲核心线程超时时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。不然,线程会永远等待新的工做。
sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
// IO密集型任务线程池直接采用CachedThreadPool来实现,
// 它最多能够分配Integer.MAX_VALUE个非核心线程用来执行任务
sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY);
}
}
复制代码
项目发展阶段忽视基础设施建设,没有采用统一的线程池,致使线程数量过多。
异步任务执行太耗时,致使主线程卡顿。
子线程分担主线程任务,并行减小时间。
充分利用CPU多核,自动梳理任务顺序。
启动器的流程图以下所示:
启动器的主题流程为上图中的中间区域,即主线程与并发两个区域块。须要注意的是,在上图中的 head task与tail task 并不包含在启动器的主题流程中,它仅仅是用于处理启动前/启动后的一些通用任务,例如咱们能够在head task中作一些获取通用信息的操做,在tail task能够作一些log输出、数据上报等操做。
那么,这里咱们总结一下启动的核心流程,以下所示:
下面,咱们就来使用异步启动器来在Application的onCreate方法中进行异步优化,代码以下所示:
// 一、启动器初始化
TaskDispatcher.init(this);
// 二、建立启动器实例,这里每次获取的都是新对象
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
// 三、给启动器配置一系列的(异步/非异步)初始化任务并启动启动器
dispatcher
.addTask(new InitAMapTask())
.addTask(new InitStethoTask())
.addTask(new InitWeexTask())
.addTask(new InitBuglyTask())
.addTask(new InitFrescoTask())
.addTask(new InitJPushTask())
.addTask(new InitUmengTask())
.addTask(new GetDeviceIdTask())
.start();
// 四、须要等待微信SDK初始化完成,程序才能往下执行
dispatcher.await();
复制代码
这里的 TaskDispatcher 就是咱们的启动器调用类。首先,在注释1处,咱们须要先调用TaskDispatcher的init方法进行启动器的初始化,其源码以下所示:
public static void init(Context context) {
if (context != null) {
sContext = context;
sHasInit = true;
sIsMainProcess = Utils.isMainProcess(sContext);
}
}
复制代码
能够看到,仅仅是初始化了几个基础字段。接着,在注释2处,咱们建立了启动器实例,其源码以下所示:
/**
* 注意:这里咱们每次获取的都是新对象
*/
public static TaskDispatcher createInstance() {
if (!sHasInit) {
throw new RuntimeException("must call TaskDispatcher.init first");
}
return new TaskDispatcher();
}
复制代码
在createInstance方法的中咱们每次都会建立一个新的TaskDispatcher实例。而后,在注释3处,咱们给启动器配置了一系列的初始化任务并启动启动器,须要注意的是,这里的Task既能够是用于执行异步任务(子线程)的也能够是用于执行非异步任务(主线程)。下面,咱们来分析下这两种Task的用法,好比InitStethoTask这个异步任务的初始化,代码以下所示:
/**
* 异步的Task
*/
public class InitStethoTask extends Task {
@Override
public void run() {
Stetho.initializeWithDefaults(mContext);
}
}
复制代码
这里的InitStethoTask直接继承自Task,Task中的runOnMainThread方法返回为false,说明 task 是用于处理异步任务的task,其中的run方法就是Runnable的run方法。下面,咱们再看看另外一个用于初始化非异步任务的例子,例如用于微信SDK初始化的InitWeexTask,代码以下所示:
/**
* 主线程执行的task
*/
public class InitWeexTask extends MainTask {
@Override
public boolean needWait() {
return true;
}
@Override
public void run() {
InitConfig config = new InitConfig.Builder().build();
WXSDKEngine.initialize((Application) mContext, config);
}
}
复制代码
能够看到,它直接继承了MainTask,MainTask的源码以下所示:
public abstract class MainTask extends Task {
@Override
public boolean runOnMainThread() {
return true;
}
}
复制代码
MainTask 直接继承了Task,并仅仅是重写了runOnMainThread方法返回了true,说明它就是用来初始化主线程中的非异步任务的。
此外,咱们注意到InitWeexTask中还重写了一个needWait方法并返回了true,其目的是为了在某个时刻以前必须等待InitWeexTask初始化完成程序才能继续往下执行,这里的某个时刻指的就是咱们在Application的onCreate方法中的注释4处的代码所执行的地方:dispatcher.await(),其实现源码以下所示:
/**
* 须要等待的任务数
*/
private AtomicInteger mNeedWaitCount = new AtomicInteger();
/**
* 调用了 await 还没结束且须要等待的任务列表
*/
private List<Task> mNeedWaitTasks = new ArrayList<>();
private CountDownLatch mCountDownLatch;
private static final int WAITTIME = 10000;
@UiThread
public void await() {
try {
// 一、仅仅在测试阶段才输出需等待的任务列表数与任务名称
if (DispatcherLog.isDebug()) {
DispatcherLog.i("still has " + mNeedWaitCount.get());
for (Task task : mNeedWaitTasks) {
DispatcherLog.i("needWait: " + task.getClass().getSimpleName());
}
}
// 二、只要还有须要等待的任务没有执行完成,就调用mCountDownLatch的await方法进行等待,这里咱们设定超时时间为10s
if (mNeedWaitCount.get() > 0) {
if (mCountDownLatch == null) {
throw new RuntimeException("You have to call start() before call await()");
}
mCountDownLatch.await(WAITTIME, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
}
}
复制代码
首先,在注释1处,咱们仅仅只会在测试阶段才会输出需等待的任务列表数与任务名称。而后,在注释2处,只要须要等待的任务数mNeedWaitCount大于0,即只要还有须要等待的任务没有执行完成,就调用mCountDownLatch的await方法进行等待,注意咱们这里设定了超时时间为10s。当一个task执行完成后,不管它是异步仍是非异步的,最终都会执行到mTaskDispatcher的markTaskDone(mTask)方法,咱们看看它的实现源码,以下所示:
/**
* 已经结束的Task
*/
private volatile List<Class<? extends Task>> mFinishedTasks = new ArrayList<>(100);
public void markTaskDone(Task task) {
if (ifNeedWait(task)) {
mFinishedTasks.add(task.getClass());
mNeedWaitTasks.remove(task);
mCountDownLatch.countDown();
mNeedWaitCount.getAndDecrement();
}
}
复制代码
能够看到,这里每执行完成一个task,就会将mCountDownLatch的锁计数减1,与此同时,也会将咱们的mNeedWaitCount这个原子整数包装类的数量减1。
此外,咱们在前面说到了启动器将各个任务之间的依赖关系抽象成了一个有向无环图,在上面一系列的初始化代码中,InitJPushTask是须要依赖于GetDeviceIdTask的,那么,咱们怎么告诉启动器它们二者之间的依赖关系呢?
这里只须要在InitJPushTask中重写dependsOn()方法,并返回包含GetDeviceIdTask的task列表便可,代码以下所示:
/**
* InitJPushTask 须要在 getDeviceId 以后执行
*/
public class InitJPushTask extends Task {
@Override
public List<Class<? extends Task>> dependsOn() {
List<Class<? extends Task>> task = new ArrayList<>();
task.add(GetDeviceIdTask.class);
return task;
}
@Override
public void run() {
JPushInterface.init(mContext);
MyApplication app = (MyApplication) mContext;
JPushInterface.setAlias(mContext, 0, app.getDeviceId());
}
}
复制代码
至此,咱们的异步启动器就分析完毕了。下面咱们来看看如何高效地进行延迟初始化。
利用IdleHandler特性,在CPU空闲时执行,对延迟任务进行分批初始化。
延迟初始化启动器的代码很简单,以下所示:
/**
* 延迟初始化分发器
*/
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 分批执行的好处在于每个task占用主线程的时间相对
// 来讲很短暂,而且此时CPU是空闲的,这些能更有效地避免UI卡顿
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
复制代码
在DelayInitDispatcher中,咱们提供了mDelayTasks队列用于将每个task添加进来,使用者只需调用addTask方法便可。当CPU空闲时,mIdleHandler便会回调自身的queueIdle方法,这个时候咱们能够将task一个一个地拿出来并执行。这种分批执行的好处在于每个task占用主线程的时间相对来讲很短暂,而且此时CPU是空闲的,这样能更有效地避免UI卡顿,真正地提高用户的体验。
至于使用就很是简单了,咱们能够直接利用SplashActivity的广告页停留时间去进行延迟初始化,代码以下所示:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
GlobalHandler.getInstance().getHandler().post((Runnable) () -> {
if (hasFocus) {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new InitOtherTask())
.start();
}
});
}
复制代码
须要注意的是,能异步的task咱们会优先使用异步启动器在Application的onCreate方法中加载(或者是必须在Application的onCreate方法完成前必须执行完的非异task务),对于不能异步的task,咱们能够利用延迟启动器进行加载。若是任务能够到用时再加载,能够使用懒加载的方式。
咱们都知道,安装或者升级后首次 MultiDex 花费的时间过于漫长,咱们须要进行Multidex的预加载优化。
5.0以上默认使用ART,在安装时已将Class.dex转换为oat文件了,无需优化,因此应判断只有在主进程及SDK 5.0如下才进行Multidex的预加载。
主要包括inline以及quick指令的优化。
使编译器在函数调用处用函数体代码代替函数调用指令。
函数调用的转移操做有必定的时间和空间方面的开销,特别是对于一些函数体不大且频繁调用的函数,解决其效率问题更为重要,引入inline函数就是为了解决这一问题。
inline函数至少在三个方面提高了程序的时间性能:
为了完全解决MutiDex加载时间慢的问题,抖音团队深刻挖掘了 Dalvik 虚拟机的底层系统机制,对 DEX 相关的处理逻辑进行了从新设计与优化,并推出了 BoostMultiDex 方案,它可以减小 80% 以上的黑屏等待时间,挽救低版本 Android 用户的升级安装体验。若有兴趣的同窗能够看看这篇文章:抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减小80%
能够利用MultiDex预加载期间的这段CPU去预加载SharedPreferences。
需重写getApplicationContext返回this,不然此时可能获取不到context。
在Application中提早异步加载初始化耗时较长的类。
替换系统的ClassLoader,打印类加载的时间,按需选取须要异步加载的类。
在主页空闲时,将其它页面的数据加载好保存到内存或数据库,等到打开该页面时,判断已经预加载过,就直接从内存或数据库取数据并显示。
子进程会共享CPU资源,致使主进程CPU紧张。此外,在多进程状况下必定要能够在onCreate中去区分进程作一些初始化工做。
App onCreate以前是ContentProvider初始化。
关于布局与绘制优化能够参考Android性能优化之绘制优化。
启动时CG抑制,容许堆一直增加,直到手动或OOM中止GC抑制。(空间换时间)
须要白名单覆盖全部设备,但维护成本高。
一个设备的CPU一般都是4核或者8核,可是应用在通常状况下对CPU的利用率并不高,可能只有30%或者50%,若是咱们在启动速度暴力拉伸CPU频率,以此提升CPU的利用率,那么,应用的启动速度会提高很多。
在Android系统中,CPU相关的信息存储在/sys/devices/system/cpu目录的文件中,经过对该目录下的特定文件进行写值,实现对CPU频率等状态信息的更改。
暴力拉伸CPU频率,致使耗电量增长。
对应的文件有:
这里须要注意的是,须要考虑重度用户的使用场景。
利用内存中的存储空间来暂存从磁盘中读出的一系列盘块中的信息。所以,磁盘高速缓存在逻辑上属于磁盘,物理上则是驻留在内存中的盘块。
其内存中分为两种形式:
当数据写入文件时,内核一般先将该数据复制到缓冲区高速缓存或页面缓存中,若是该缓冲区还没有写满,则不会将其排入输入队列,而是等待其写满或内核须要重用该缓冲区以便存放其余磁盘块数据时,再将该缓冲排入输出队列,最后等待其到达队首时,才进行实际的IO操做—延迟写。
延迟写减小了磁盘读写次数,可是却下降了文件内容的更新速度,可能会形成文件更新内容的丢失。为了保证数据一致性,则需使用同步IO。
若是当前硬盘的平均寻道时间是3-15ms,7200RPM硬盘的平均旋转延迟大约为4ms,所以一次IO操做的耗时大约为10ms。
若是使用内存映射文件的方式进行文件IO(mmap),将文件的page cache直接映射到进程的地址空间,这时须要使用msync系统调用确保修改的内容彻底同步到硬盘之上。
建立每一个log文件时先写文件的最后一个page,将log文件扩展为10MB大小,这样即可以使用fdatasync,每写10MB只有一次同步metadata的开销。
标准IO,大多数文件系统默认的IO操做。
优势
缺点
DMA方式能够将数据直接从磁盘读到页缓存中,或者将数据从页缓存中写回到磁盘,而不能在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程当中须要在应用程序地址空间(用户空间)和缓存(内核空间)中进行屡次数据拷贝操做,这带来的CPU以及内存开销是很是大的。
磁盘IO主要的延时(15000RPM硬盘为例)
机械转动延时(平均2ms)+ 寻址延时(2~3ms)+ 块传输延时(0.1ms左右)=> 平均5ms
网络IO主要延时
服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时(通常为几十毫秒到几千毫秒,受环境影响极大)
很早以前,磁盘和内存之间的数据传输是须要CPU控制的,也就是读取磁盘文件到内存中时,数据会通过CPU存储转发,这种方式称为PIO。
应用程序直接访问磁盘数据,而不通过内核缓冲区。以减小从内核缓冲区到用户数据缓存的数据复制。
当访问数据的线程发出请求后,线程会接着去处理其它事情,而不是阻塞等待。
能够为访问文件系统的系统调用提供一个统一的抽象接口。
Dex文件用到的类和APK里面各类资源文件都比较小,读取频繁,且磁盘地址分布范围比较广。咱们能够利用Linux文件IO流程中的page cache机制将它们按照读取顺序从新排列在一块儿,以减小真实的磁盘IO次数。
使用Facebook的 ReDex 的Interdex调整类在Dex中的排列顺序。
一个能够不修改APK就影响程序运行的Hook框架。
用自身实现的app_process替换掉系统/system/bin/app_process,加载一个额外的XposedBridge的jar包,用于将入口osZygoteInit.main()替换成XposedBridge.main()。以后,建立的Zygote进程和其子进程都是Hook过的了。
使用具体细节参见Xposed教程。
对象第一次建立的时候,JVM首先检查对应的Class对象是否已经加载。若是没有加载,JVM会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不须要加载类对象,而是直接实例化,建立时间就缩短了。
ART比较复杂,Hook须要兼容几个版本。并且在安装时,大部分Dex已经优化好了,去掉ART平台的verify只会对动态加载的Dex带来一些好处。因此暂时不建议在ART平台使用。
它们在设计上都存在大量的Hook和私有API调用,共同的缺点有以下两类问题。
因为厂商的兼容性、安装失败、ART加载时dex2oat失败等缘由,仍是会有一些代码和资源的异常。Android P推出的non-sdk-interface调用限制,之后适配只会愈来愈难,成本愈来愈高。
用到一些黑科技致使底层Runtime的优化享受不到。如Tinker加载补丁后,启动速度会下降5%~10%。
Android官方使用热补丁技术实现InstantRun。
构建 -> 部署 -> 安装 -> 重启app -> 重启activity
尽量多的剔除没必要要的步骤,而后提高必要步骤的速度。
一、HotSwap
增量构建 -> 改变部署
场景:
适用于多数简单的改变(包括一些方法实现的修改,或者变量值修改)。
二、Warm Swap
增量构建 -> 改变部署 -> activity重启
场景:
通常是修改了resources。
三、Cold Swap
增量构建 -> 改变部署 -> 应用重启 -> activity重启
场景:
涉及结构性变化,如修改了继承规则或方法签名。
Android Studio monitors 运行着Gradle任务来生成增量.dex文件(dex对应着开发中的修改类),AS会提取这些.dex文件发送到App Server,而后部署到App。由于原来版本的类都装载在运行中的程序了,Gradle会解释更新好这些.dex文件,发送到App Server的时候,交给自定义的类加载器来加载.dex文件。 App Server会不断地监听是否须要重写类文件,若是须要,任务会被立马执行,新的更改便能当即被响应。
须要注意的是,此时InstantRun是不能回退的,必须重启应用响应修改。
由于资源文件是在Activity建立时加载,因此必须重启Activity加载资源文件。
注意:AndroidManifest的值是在APK安装的时候被读取的,因此须要触发一个完整的应用构建和部署。
应用部署的时候,会把工程拆分红十个部分,每一个部分都拥有本身的.dex文件,而后全部的类会根据包名被分配给相应的.dex文件。当ColdSwap开启时,修改过的类所对应的的.dex文件,会重组生成新的.dex文件,而后再部署到设备上。
注意:应用多进程会被降级为ColdSwap。
manifest文件合并、打包,和res一块儿被AAPT合并到APK中,同时项目代码被编译成字节码,而后转换成.dex文件,也被合并到APK中。
在回答这个问题以前,咱们须要先了解下内存对齐(DSA,Data Structure Alignment):
各类类型的数据按照必定的规则在内存空间上排列,这就是对齐。
复制代码
内存对齐的优点在于可以以空间换时间,减小数据存取指令周期,提高程序运行时的速度。
zipalign优化的最根本目的是帮助操做系统更高效地根据请求索引资源,使用resource-handling code统一将DSA限定为4byte。
利用build-tools文件夹下对应Android版本中的zipalign工具:
zipalign -v 4 source.apk androidres.apk
复制代码
检查当前APK是否已经执行过Align优化:
zipalign -c -v 4 androidres.apk
复制代码
其中:
native hook -> dalvik_repleaceMethod -> 没法支持新增或删除filed的状况 -> 需修复特定问题
它是一个基于Android Dex分包方案。它将多个dex文件放入到app的classloader中,可是android dex拆包方案中的类是没有重复的,若是classes.dex和classes1.dex中有重复的类,当用到这个重复的类时,系统会选择哪一个类进行加载呢?
一个ClassLoader能够包含多个dex文件,每一个dex文件是一个Elements,多个dex文件排列成有序的dexElements,当找类的时候,会按顺序遍历dex文件,而后从当前遍历的dex文件中找类,若是找到则返回,若是找不到从下一个dex文件继续查找。
因此,若是在不一样的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。
Qzone热补丁方案就是把有问题的类打包到一个dex(patch.dex)中去,而后把这个dex插入到Elements的最前面。
一、当其它dex文件中的类引用了patch.dex中的类时,会出现校验错误。拆分dex的不少类都不是在同一个dex内的,怎么没有问题?
由于这个校验有个前提,当引用类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。
二、CLASS_ISPREVERIFIED标志是何时被打上去的?
有两步验证:
一、验证clazz -> directMethods方法,其包含如下方法:
二、clazz -> virtualMethods
若是以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFED标志。
为了解决补丁方案中遇到的问题,因此必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。空间的方案是往全部类的构造函数里面插入一段代码:
If (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
复制代码
其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex中的类都会引用一个在不一样dex中的AntilazyLoad类,这样就防止类被打上了CLASS_ISPREVERIFILED标志,只要没被打上这个标志的类均可以进行打补丁操做。
注意:
为何要选择构造函数?
由于他不增长方法数,一个类即便没有显示的构造函数,也有一个隐式的默认构造函数。
能够使用ASM/javaassist库在编译期间将相应的字节码插入Class文件中。
Art采用了新的方式,插桩对代码的执行效率没有影响。可是补丁中的类出现修改类变量或者方法,可能会致使出现内存地址错乱的状况。
缘由:
dex2oat时fast*已经将类能肯定的各个地址写死。若是运行时补丁包的地址出现改变,原始类去调用时就会出现地址错乱。
解决方法:
将其父类以及调用类的全部类都加入到补丁包中。
为了提升性能。
因为如今不少App都使用了MultiDex分包方案,这致使了不少类都没有被打上这个标志,因此此时禁用全部类打上CLASS_ISPREVERIFIED标志对性能的影响不是很大。
在补丁包大小与性能损耗上有必定的局限性。
插桩就是将一段代码插入或者替换本来的代码。 字节码插桩就是在咱们的代码编译成字节码(Class)后,在Android下生成dex以前修改Class文件,修改或者加强原有代码逻辑的操做。
除了AspectJ、Javassist框架外,还有一个应用更为普遍的ASM框架一样也是字节码操做框架,Instant Run包括Javassist就是借助ASM来实现各自的功能。
能够这样理解Class字节码与ASM之间的联系:
JSON对于GSON就相似于字节码Class对于Javassist/ASM。
复制代码
Android 1.5.0版本之后提供了Transform API,容许第三方Plugin在打包dex文件以前的编译过程当中操做.class文件,咱们作的就是实现Transform进行.class文件遍历拿到全部方法,修改完成后对文件进行替换。
大体的流程以下所示:
一、自动埋点追踪,遍历全部文件更换字节码
AutoTransform -> transform -> inputs.each {TransformInput input -> input.jarInput.each { JarInput jarInput -> … } input.directoryInputs.each { DirectoryInput directoryInput -> … }}
复制代码
二、Gradle插件实现
PluginEntry -> apply -> def android = project.extensions.getByType(AppExtension)
registerTransform(android) -> AutoTransform transform = new AutoTransform
android.registerTransform(transform)
复制代码
三、使用ASM进行字节码编写
ASM框架核心类
一、visit -> 在ClassVisitor中根据判断是不是实现View$OnClickListener接口的类,只有知足条件的类才会遍历其中的方法进行操做。
二、在MethodVisitor中对该方法进行修改
visitAnnotation -> onMethodEnter -> onMethodExit
复制代码
三、先在java文件中编写要插入的代码,而后使用ASM插件查看对应的字节码,根据其用ASM提供的Api一一对应地把代码填进来便可。
关于编译插桩的知识,笔者后面会有一系列的文章进行深刻讲解,具体的文章目录能够在这里查看。
DexDiff的粒度是Dex格式的每一项,BsDiff的粒度是文件,AndFix/Qzone的粒度为class。
若不care性能损耗与补丁包大小,Qzone是最简单且成功率最高的方案。
负责将补丁包交付给用户,包括特定用户和全量用户。
一、pull通道
在登陆/24小时等时机,经过pull方式查询后台是否有对应的补丁包更新。
二、指定版本的push通道
在紧急状况下,咱们能够在一个小时内向全部用户下发补丁包更新。
三、指定特定用户的push通道
对特定用户或用户组作远程调试。
快速上线,管理历史记录,以及监控补丁的运行状况。
构建了App与系统(ROM)之间可靠的通讯框架,让系统知道App的需求。
平均10%~30%。
一种优化资源调度的技术。
让应用程序与系统资源实现实时"双向对话"。当来自应用和游戏程序的不一样场景和用户行为被Hyper Boost识别后,手机会智能地匹配到合理的系统资源,让手机SoC的CPU、GPU、ISP、DSP提供的运算资源更加合理地利用,从而让用户使用手机更加流畅。
在某一个版本以后呢,咱们会发现这个启动速度变得特别慢,同时用户给咱们的反馈也愈来愈多,因此,咱们开始考虑对应用的启动速度来进行优化。而后,咱们就对启动的代码进行了代码层面的梳理,咱们发现应用的启动流程已经很是复杂,接着,咱们经过一系列的工具来确认是否在主线程中执行了太多的耗时操做。
咱们通过了细查代码以后,发现应用主线程中的任务太多,咱们就想了一个方案去针对性地解决,也就是进行异步初始化。(引导=>第2题) 而后,咱们还发现了另一个问题,也能够进行针对性的优化,就是在咱们的初始化代码当中有些的优先级并非那么高,它能够不放在Application的onCreate中执行,而彻底能够放在以后延迟执行的,由于咱们对这些代码进行了延迟初始化,最后,咱们还结合了idealHandler作了一个更优的延迟初始化的方案,利用它能够在主线程的空闲时间进行初始化,以减小启动耗时致使的卡顿现象。作完这些以后,咱们的启动速度就变得很快了。
最后,我简单说下咱们是怎么长期来保持启动优化的效果的。首先,咱们作了咱们的启动器,而且结合了咱们的CI,在线上加上了不少方面的监控。(引导=> 第4题)
咱们最初是采用的普通的一个异步的方案,即new Thread + 设置线程优先级为后台线程的方式在Application的onCreate方法中进行异步初始化,后来,咱们使用了线程池、IntentService的方式,可是,在咱们应用的演进过程中,发现代码会变得不够优雅,而且有些场景很是很差处理,好比说多个初始化任务直接的依赖关系,好比说某一个初始化任务须要在某一个特定的生命周期中初始化完成,这些都是使用线程池、IntentService没法实现的。因此说,咱们就开始思考一个新的解决方案,它可以完美地解决咱们刚刚所遇到的这些问题。
这个方案就是咱们目前所使用的启动器,在启动器的概念中,咱们将每个初始化代码抽象成了一个Task,而后,对它们进行了一个排序,根据它们之间的依赖关系排了一个有向无环图,接着,使用一个异步队列进行执行,而且这个异步队列它和CPU的核心数是强烈相关的,它可以最大程度地保证咱们的主线程和别的线程都可以执行咱们的任务,也就是你们几乎均可以同时完成。
首先,在CPU Profiler和Systrace中有两个很重要的指标,即cpu time与wall time,咱们必须清楚cpu time与wall time之间的区别,wall time指的是代码执行的时间,而cpu time指的是代码消耗CPU的时间,锁冲突会形成二者时间差距过大。咱们须要以cpu time来做为咱们优化的一个方向。
其次,咱们不只只追求启动速度上的一个提高,也须要注意延迟初始化的一个优化,对于延迟初始化,一般的作法是在界面显示以后才去进行加载,可是若是此时界面须要进行滑动等与用户交互的一系列操做,就会有很严重的卡顿现象,所以咱们使用了idealHandler来实现cpu空闲时间来执行耗时任务,这极大地提高了用户的体验,避免了因启动耗时任务而致使的页面卡顿现象。
最后,对于启动优化,还有一些黑科技,首先,就是咱们采用了类预先加载的方式,咱们在MultiDex.install方法以后起了一个线程,而后用Class.forName的方式来预先触发类的加载,而后当咱们这个类真正被使用的时候,就不用再进行类加载的过程了。同时,咱们再看Systrace图的时候,有一部分手机其实并无给咱们应用去跑满cpu,好比说它有8核,可是却只给了咱们4核等这些状况,而后,有些应用对此作了一些黑科技,它会将cpu的核心数以及cpu的频率在启动的时候去进行一个暴力的提高。
这种问题其实咱们以前也遇到过,这的确很是难以解决。可是,咱们后面对此进行了反复的思考与尝试,终于找到了一个比较好的解决方式。
首先,咱们使用了启动器去管理每个初始化任务,而且启动器中每个任务的执行都是被其自动进行分配的,也就是说这些自动分配的task咱们会尽可能保证它会平均分配在咱们每个线程当中的,这和咱们普通的异步是不同的,它能够很好地缓解咱们应用的启动变慢。
其次,咱们还结合了CI,好比说,咱们如今限制了一些类,如Application,若是有人修改了它,咱们不会让这部分代码合并到主干分支或者是修改以后会有一些内部的工具如邮件的形式发送到我,而后,我就会和他确认他加的这些代码究竟是耗时多少,可否异步初始化,不能异步的话就考虑延迟初始化,若是初始化时间太长,则能够考虑是否能进行懒加载,等用到的时候再去使用等等。
而后,咱们会将问题尽量地暴露在上线以前。同时,咱们真正已经到了线上的一个环境下时,咱们进行了监控的一个完善,咱们不只是监控了App的整个的启动时间,同时呢,咱们也将每个生命周期都进行了一个监控。好比说Application的onCreate与onAttachBaseContext方法的耗时,以及这两个生命周期之间间隔的时间,咱们都进行了一个监控,若是说下一次咱们发现了这个启动速度变慢了,咱们就能够去查找究竟是哪个环节变慢了,咱们会和之前的版本进行对比,对比完成以后呢,咱们就能够来找这一段新加的代码。
至此,探索Android启动速度优化的旅途也应该告一段落了,若是你耐心读到最后的话,会发现要想极致地提高App的性能,须要有必定的技术广度,如咱们引入了始于后端的AOP编程来实现无侵入式的函数插桩,也须要有必定的深度,从前面的探索之旅来看,咱们前后涉及了Framework层、Native层、Dalvik虚拟机、甚至是Linux IO和文件系统相关的原理。所以,我想说,Android开发并不简单,即便是App层面的性能优化这一知识体系,也是须要咱们不断地加深自身知识的深度和广度。
ps:在文章的黑科技部分涉及到了许多基础架构研发领域的知识,这部分没法理解的同窗不要灰心,先了解便可,
笔者以后的文章都会一一详细讲解。
复制代码
二、支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
三、支付宝 App 构建优化解析:经过安装包重排布优化 Android 端启动性能
七、Dalvik Optimization and Verification With dexopt
八、微信在Github开源了Hardcoder,对Android开发者有什么影响?
九、历时三年研发,OPPO 的 Hyper Boost 引擎如何对系统、游戏和应用实现加速?
十一、墙上时钟时间 ,用户cpu时间 ,系统cpu时间的理解
十二、《Android应用性能优化最佳实践》
1三、必知必会 | Android 性能优化的方面方面都在这儿
1四、极客时间之Top团队大牛带你玩转Android性能分析与优化
1五、启动器源码
1六、MultiDex优化源码
欢迎关注个人微信:
bcce5360
微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。
2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~