inetAddressList) {
52 | super.dnsEnd(call, domainName, inetAddressList);
53 | okHttpEvent.dnsEndTime = System.currentTimeMillis();
54 | }
55 |
56 | @Override
57 | public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
58 | super.connectStart(call, inetSocketAddress, proxy);
59 | }
60 |
61 | @Override
62 | public void secureConnectStart(Call call) {
63 | super.secureConnectStart(call);
64 | }
65 |
66 | @Override
67 | public void secureConnectEnd(Call call, @Nullable Handshake handshake) {
68 | super.secureConnectEnd(call, handshake);
69 | }
70 |
71 | @Override
72 | public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) {
73 | super.connectEnd(call, inetSocketAddress, proxy, protocol);
74 | }
75 |
76 | @Override
77 | public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol, IOException ioe) {
78 | super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe);
79 | }
80 |
81 | @Override
82 | public void connectionAcquired(Call call, Connection connection) {
83 | super.connectionAcquired(call, connection);
84 | }
85 |
86 | @Override
87 | public void connectionReleased(Call call, Connection connection) {
88 | super.connectionReleased(call, connection);
89 | }
90 |
91 | @Override
92 | public void requestHeadersStart(Call call) {
93 | super.requestHeadersStart(call);
94 | }
95 |
96 | @Override
97 | public void requestHeadersEnd(Call call, Request request) {
98 | super.requestHeadersEnd(call, request);
99 | }
100 |
101 | @Override
102 | public void requestBodyStart(Call call) {
103 | super.requestBodyStart(call);
104 | }
105 |
106 | @Override
107 | public void requestBodyEnd(Call call, long byteCount) {
108 | super.requestBodyEnd(call, byteCount);
109 | }
110 |
111 | @Override
112 | public void responseHeadersStart(Call call) {
113 | super.responseHeadersStart(call);
114 | }
115 |
116 | @Override
117 | public void responseHeadersEnd(Call call, Response response) {
118 | super.responseHeadersEnd(call, response);
119 | }
120 |
121 | @Override
122 | public void responseBodyStart(Call call) {
123 | super.responseBodyStart(call);
124 | }
125 |
126 | @Override
127 | public void responseBodyEnd(Call call, long byteCount) {
128 | super.responseBodyEnd(call, byteCount);
129 | okHttpEvent.responseBodySize = byteCount;
130 | }
131 |
132 | @Override
133 | public void callEnd(Call call) {
134 | super.callEnd(call);
135 | okHttpEvent.apiSuccess = true;
136 | }
137 |
138 | @Override
139 | public void callFailed(Call call, IOException ioe) {
140 | Log.i("lz","callFailed ");
141 | super.callFailed(call, ioe);
142 | okHttpEvent.apiSuccess = false;
143 | okHttpEvent.errorReason = Log.getStackTraceString(ioe);
144 | Log.i("lz","reason "+okHttpEvent.errorReason);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/src/main/java/com/optimize/performance/block/AppBlockCanaryContext.java:
--------------------------------------------------------------------------------
1 | package com.optimize.performance.block;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import com.github.moduth.blockcanary.BlockCanaryContext;
7 | import com.github.moduth.blockcanary.internal.BlockInfo;
8 |
9 | import java.io.File;
10 | import java.util.LinkedList;
11 | import java.util.List;
12 |
13 | /**
14 | * BlockCanary配置的各种信息
15 | */
16 | public class AppBlockCanaryContext extends BlockCanaryContext {
17 |
18 | /**
19 | * Implement in your project.
20 | *
21 | * @return Qualifier which can specify this installation, like version + flavor.
22 | */
23 | public String provideQualifier() {
24 | return "unknown";
25 | }
26 |
27 | /**
28 | * Implement in your project.
29 | *
30 | * @return user id
31 | */
32 | public String provideUid() {
33 | return "uid";
34 | }
35 |
36 | /**
37 | * Network type
38 | *
39 | * @return {@link String} like 2G, 3G, 4G, wifi, etc.
40 | */
41 | public String provideNetworkType() {
42 | return "unknown";
43 | }
44 |
45 | /**
46 | * Config monitor duration, after this time BlockCanary will stop, use
47 | * with {@code BlockCanary}'s isMonitorDurationEnd
48 | *
49 | * @return monitor last duration (in hour)
50 | */
51 | public int provideMonitorDuration() {
52 | return -1;
53 | }
54 |
55 | /**
56 | * Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
57 | * from performance of device.
58 | *
59 | * @return threshold in mills
60 | */
61 | public int provideBlockThreshold() {
62 | return 500;
63 | }
64 |
65 | /**
66 | * Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
67 | * stack according to current sample cycle.
68 | *
69 | * Because the implementation mechanism of Looper, real dump interval would be longer than
70 | * the period specified here (especially when cpu is busier).
71 | *
72 | *
73 | * @return dump interval (in millis)
74 | */
75 | public int provideDumpInterval() {
76 | return provideBlockThreshold();
77 | }
78 |
79 | /**
80 | * Path to save log, like "/blockcanary/", will save to sdcard if can.
81 | *
82 | * @return path of log files
83 | */
84 | public String providePath() {
85 | return "/blockcanary/";
86 | }
87 |
88 | /**
89 | * If need notification to notice block.
90 | *
91 | * @return true if need, else if not need.
92 | */
93 | public boolean displayNotification() {
94 | return true;
95 | }
96 |
97 | /**
98 | * Implement in your project, bundle files into a zip file.
99 | *
100 | * @param src files before compress
101 | * @param dest files compressed
102 | * @return true if compression is successful
103 | */
104 | public boolean zip(File[] src, File dest) {
105 | return false;
106 | }
107 |
108 | /**
109 | * Implement in your project, bundled log files.
110 | *
111 | * @param zippedFile zipped file
112 | */
113 | public void upload(File zippedFile) {
114 | throw new UnsupportedOperationException();
115 | }
116 |
117 |
118 | /**
119 | * Packages that developer concern, by default it uses process name,
120 | * put high priority one in pre-order.
121 | *
122 | * @return null if simply concern only package with process name.
123 | */
124 | public List concernPackages() {
125 | return null;
126 | }
127 |
128 | /**
129 | * Filter stack without any in concern package, used with @{code concernPackages}.
130 | *
131 | * @return true if filter, false it not.
132 | */
133 | public boolean filterNonConcernStack() {
134 | return false;
135 | }
136 |
137 | /**
138 | * Provide white list, entry in white list will not be shown in ui list.
139 | *
140 | * @return return null if you don't need white-list filter.
141 | */
142 | public List provideWhiteList() {
143 | LinkedList whiteList = new LinkedList<>();
144 | whiteList.add("org.chromium");
145 | return whiteList;
146 | }
147 |
148 | /**
149 | * Whether to delete files whose stack is in white list, used with white-list.
150 | *
151 | * @return true if delete, false it not.
152 | */
153 | public boolean deleteFilesInWhiteList() {
154 | return true;
155 | }
156 |
157 | /**
158 | * Block interceptor, developer may provide their own actions.
159 | */
160 | public void onBlock(Context context, BlockInfo blockInfo) {
161 | Log.i("lz","blockInfo "+blockInfo.toString());
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/docs/4.App内存优化.md:
--------------------------------------------------------------------------------
1 | ## 第4章 App内存优化
2 |
3 | #### 4-2 内存优化介绍及工具选择
4 |
5 | 1. 内存优化介绍
6 | 1. 内存是大问题但缺乏关注
7 | 2. 压死骆驼的最后一根稻草
8 | 2. 内存问题
9 | 1. 内存抖动:锯齿状、GC导致卡顿
10 | 2. 内存泄漏:可用内存减少、频繁GC
11 | 3. 内存溢出:OOM、程序异常
12 | 3. 优化工具选择
13 | 1. Memory Profiler
14 | 1. 实时图标展示应用内存使用量
15 | 2. 识别内存泄漏、抖动等
16 | 3. 提供捕获堆转储、强制GC以及跟踪内存分配的能力
17 | 4. 方便直观、线下平时使用
18 | 2. Memory Analyzer
19 | 1. 强大的Java Heap分析工具,查找内存泄漏及内存占用
20 | 2. 生成整体报告、分析问题等
21 | 3. 线下深入使用
22 | 3. LeakCanary
23 | 1. 自动内存泄漏检测
24 | 2. https://github.com/square/leakcanary
25 | 3. 线下集成
26 |
27 | #### 4-3 Android内存管理机制
28 |
29 | 1. Java内存管理机制
30 |
31 | 1. Java内存分配
32 |
33 | 1. 方法区、虚拟机栈、本地方法栈、堆、程序计数器
34 |
35 | 2. Java内存回收算法
36 |
37 | 1. 标记-清除算法
38 |
39 | 效率和清除效率不高
40 |
41 | 产生大量不连续的内存碎片
42 |
43 | 1. 标记出所有需要回收的对象
44 | 2. 统一回收所有被标记的对象
45 |
46 | 2. 复制算法
47 |
48 | 实现简单,运行高效
49 |
50 | 浪费一半空间,代价大
51 |
52 | 1. 将内存划分为大小相等的两块
53 | 2. 一块内存用完之后复制存活对象到另一块
54 | 3. 清理另一块内存
55 |
56 | 3. 标记-整理算法
57 |
58 | 1. 标记过程与“标记-清除”算法一样
59 | 2. 存活对象往一端进行移动
60 | 3. 清理其余内存
61 |
62 | 4. 分代收集算法
63 |
64 | 1. 结合多种收集算法优势
65 | 2. 新生代对象存活率低,复制
66 | 3. 老年代对象存活率高,标记-整理
67 |
68 | 2. Android内存管理机制
69 |
70 | 内存弹性分配,分配值与最大值受具体设备影响
71 |
72 | OOM场景:内存真正不足、可用内存不足
73 |
74 | 1. Dalvik与Art区别
75 | 1. Dalvik仅固定一种回收算法
76 | 2. Art回收算法可运行期选择
77 | 3. Art具备内存整理能力,减少内存空洞
78 | 2. Low Memory Killer
79 | 1. 进程分类
80 | 2. 回收收益
81 |
82 | #### 4-4 内存抖动解决实战
83 |
84 | 1. 内存抖动介绍
85 |
86 | 定义:内存频繁分配和回收导致内存不稳定
87 |
88 | 表现:频繁GC、内存曲线呈锯齿状
89 |
90 | 危害:导致卡顿、OOM
91 |
92 | 内存抖动导致OOM:
93 |
94 | 1. 频繁创建对象,导致内存不足及碎片
95 | 2. 不连续的内存片无法被分配,导致OOM
96 |
97 | 2. 内存抖动解决实战
98 |
99 | 1. 使用Memory Profiler初步排查
100 | 2. 使用Memory Profiler或CPU Profiler结合代码排查
101 | 3. 模拟内存抖动代码见:MemoryShakeActivity
102 |
103 | 3. 内存抖动解决技巧
104 |
105 | 找循环或者频繁调用的地方
106 |
107 | #### 4-5 内存泄露解决实战
108 |
109 | 1. 内存泄漏介绍
110 |
111 | 定义:内存中存在已经没有用的对象
112 |
113 | 表现:内存抖动、可用内存逐渐变少
114 |
115 | 危害:内存不足、GC频繁、OOM
116 |
117 | 2. Memory Analyzer
118 |
119 | 1. http://www.eclipse.org/mat/downloads.php
120 | 2. 转换:hprof-conv原文件路径转换后文件路径
121 | 3. 模拟内存泄露代码见:MemoryLeakActivity
122 |
123 | 3. 内存泄漏解决实战
124 |
125 | 1. 使用Memory Profiler初步观察
126 | 2. 使用Memory Analyzer结合代码确认
127 | 3. 找到内存泄露位置
128 |
129 | #### 4-6 全面理解MAT
130 |
131 | #### 4-7 ARTHook优雅检测不合理图片
132 |
133 | 1. Bitmap内存模型
134 |
135 | 1. API10之前Bitmap自身在Dalvik Heap中,像素在Native
136 | 2. API10之后像素也被放在Dalvik Heap中
137 | 3. API26之后像素在Native
138 |
139 | 2. 获取Bitmap占用内存
140 |
141 | 1. getByteCount
142 | 2. 宽 * 高 * 一像素占用内存
143 |
144 | 3. 常规方式
145 |
146 | 1. 背景:图片对内存优化至关重要、图片宽高大于控件宽高
147 | 2. 实现:集成ImageView,覆写实现计算大小
148 | 3. 侵入性强
149 | 4. 不通用
150 |
151 | 4. ARTHook实战
152 |
153 | 1. 挂钩,将额外的代码钩住原有方法,修改执行逻辑
154 |
155 | 1. 运行时插桩
156 | 2. 性能分析
157 |
158 | 2. Epic简介
159 |
160 | 1. Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架
161 | 2. 支持Android4.0 - 9.0
162 | 3. https://github.com/tiann/epic
163 |
164 | 3. Epic实战
165 |
166 | 1. `ImageHook.java`
167 |
168 | 2. ```java
169 | DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
170 | @Override
171 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
172 | super.afterHookedMethod(param);
173 | DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
174 | }
175 | });
176 | ```
177 |
178 | ARTHook 无侵入性,通用性强,兼容问题大,开源方案不能带到线上环境
179 |
180 | #### 4-8 线上内存监控方案
181 |
182 | 1. 常规方式
183 | 1. 设定场景线上Dump:`Debug.dumpHprofData()`
184 | 2. 超过最大内存80%线上Dump,回传文件,分析
185 | 3. Dump文件太大,和对象数正相关,可裁剪
186 | 4. 上传失败率高、分析困难
187 | 5. 配合一定策略,有一定效果
188 | 2. LeakCanary
189 | 1. LeakCanary带到线上
190 | 2. 预设泄漏怀疑点
191 | 3. 发现泄漏回传
192 | 4. 不适合所有情况,必须预设坏一点
193 | 5. 分析比较耗时、也容易OOM
194 | 3. LeakCanary原理
195 | 1. 监控生命周期,onDestory添加RefWatcher检测
196 | 2. 二次确认断定发生内存泄漏
197 | 3. 分析泄漏,找引用链
198 | 4. 监控组件+分析组件
199 | 4. LeakCanary定制
200 | 1. 预设怀疑点:自动找怀疑点
201 | 2. 分析泄漏链路慢:分析Retain size大的对象
202 | 3. 分析OOM:对象裁剪,不全部加载到内存
203 | 5. 线上监控完整方案
204 | 1. 待机内存、重点模块内存、OOM率
205 | 2. 整体及重点模块GC次数、GC时间
206 | 3. 增强的LeakCanary自动化内存泄漏分析
207 |
208 | #### 4-9 内存优化技巧总结
209 |
210 | 1. 优化大方向
211 | 1. 内存泄漏
212 | 2. 内存抖动
213 | 3. Bitmap
214 | 2. 优化细节
215 | 1. LargeHeap属性
216 | 2. onTrimMemory:系统低内存回调
217 | 3. 使用优化过的集合:SparseArray
218 | 4. 谨慎使用Shared Preference
219 | 5. 谨慎使用外部库
220 | 6. 业务架构设计合理
221 |
222 | #### 4-10 内存优化模拟面试
223 |
224 | 1. 你们内存优化项目的过程是怎么做的
225 | 1. 分析现状、确认问题
226 | 1. 线上OOM率
227 | 2. AS 内存监控抖动频繁
228 | 2. 针对性优化
229 | 2. 你做了内存优化最大的感受是什么
230 | 1. 磨刀不误砍柴工
231 | 2. 技术优化必须结合业务代码
232 | 3. 系统化完善解决方案
233 | 3. 如何检测所有不合理的地方
234 | 1. ARTHook
235 | 2. 重点强调区别
--------------------------------------------------------------------------------
/app/src/main/java/com/optimize/performance/adapter/NewsAdapter.java:
--------------------------------------------------------------------------------
1 | package com.optimize.performance.adapter;
2 |
3 | import android.net.Uri;
4 | import android.os.Handler;
5 | import android.support.annotation.NonNull;
6 | import android.support.constraint.ConstraintLayout;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.view.ViewTreeObserver;
12 | import android.widget.TextView;
13 |
14 | import com.facebook.drawee.view.SimpleDraweeView;
15 | import com.optimize.performance.R;
16 | import com.optimize.performance.bean.NewsItem;
17 | import com.optimize.performance.net.ConfigManager;
18 | import com.optimize.performance.utils.LaunchTimer;
19 | import com.optimize.performance.utils.LogUtils;
20 | import com.optimize.performance.wakelock.WakeLockUtils;
21 |
22 | import java.util.List;
23 |
24 | public class NewsAdapter extends RecyclerView.Adapter {
25 |
26 | private List mItems;
27 | private boolean mHasRecorded;
28 | private OnFeedShowCallBack mCallBack;
29 |
30 | public NewsAdapter(List items) {
31 | this.mItems = items;
32 | }
33 |
34 | public void setItems(List items) {
35 | this.mItems = items;
36 | notifyDataSetChanged();
37 | }
38 |
39 | public void setOnFeedShowCallBack(OnFeedShowCallBack callBack) {
40 | this.mCallBack = callBack;
41 | }
42 |
43 | @NonNull
44 | @Override
45 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
46 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item_constrainlayout, parent, false);
47 | ViewHolder holder = new ViewHolder(view);
48 | return holder;
49 | }
50 |
51 | @Override
52 | public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
53 | if (position == 0 && !mHasRecorded) {
54 | mHasRecorded = true;
55 | holder.layout.getViewTreeObserver()
56 | .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
57 | @Override
58 | public boolean onPreDraw() {
59 | holder.layout.getViewTreeObserver().removeOnPreDrawListener(this);
60 | LogUtils.i("FeedShow");
61 | LaunchTimer. endRecord("FeedShow");
62 | if (mCallBack != null) {
63 | mCallBack.onFeedShow();
64 | }
65 | return true;
66 | }
67 | });
68 | }
69 |
70 | NewsItem newsItem = mItems.get(position);
71 |
72 | // 以下代码是为了演示字符串的拼接
73 | String msgOld = newsItem.title + newsItem.targetId;// 原有方式
74 |
75 | StringBuilder builder = new StringBuilder();
76 | builder.append(newsItem.title)
77 | .append(newsItem.targetId);// 建议使用方式,不要小看这点优化
78 | String msgNew = builder.toString();
79 |
80 | holder.textView.setText(newsItem.title);
81 | Uri uri = Uri.parse(newsItem.imgurl);
82 | holder.imageView.setImageURI(uri);
83 | holder.layout.setOnClickListener(new View.OnClickListener() {
84 | @Override
85 | public void onClick(View v) {
86 |
87 | // ConfigManager.sOpenClick模拟的是功能的开关
88 | if(ConfigManager.sOpenClick){
89 | // 此处模拟的是WakeLock使用的兜底策略
90 | WakeLockUtils.acquire(holder.imageView.getContext());
91 | new Handler().postDelayed(new Runnable() {
92 | @Override
93 | public void run() {
94 | WakeLockUtils.release();
95 | }
96 | },200);
97 | }
98 | // 以下代码是为了演示Luban这个库对图片压缩对流量方面的影响
99 | // Luban.with(holder.imageView.getContext())
100 | // .load(Environment.getExternalStorageDirectory()+"/Android/1.jpg")
101 | // .setTargetDir(Environment.getExternalStorageDirectory()+"/Android")
102 | // .launch();
103 |
104 | // 以下代码是为了演示解决过度绘制问题,可以换成解决内存抖动等方面的代码
105 | // Intent intent = new Intent(holder.imageView.getContext(), SolveOverDrawActivity.class);
106 | // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
107 | // holder.imageView.getContext().startActivity(intent);
108 | }
109 | });
110 | }
111 |
112 | @Override
113 | public int getItemCount() {
114 | return mItems == null ? 0 : mItems.size();
115 | }
116 |
117 | static class ViewHolder extends RecyclerView.ViewHolder {
118 | TextView textView;
119 | SimpleDraweeView imageView;
120 | ConstraintLayout layout;
121 |
122 | public ViewHolder(View view) {
123 | super(view);
124 | textView = view.findViewById(R.id.tv_title);
125 | imageView = view.findViewById(R.id.iv_news);
126 | layout = view.findViewById(R.id.ll_out);
127 | }
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
39 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
61 |
62 |
63 |
69 |
70 |
72 |
73 |
74 |
80 |
81 |
82 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/docs/6.App卡顿优化.md:
--------------------------------------------------------------------------------
1 | ## 第6章 App卡顿优化
2 |
3 | #### 6-2 卡顿介绍及优化工具选择
4 |
5 | 1. 背景
6 |
7 | 1. 很多新能问题不易被发现,但是卡顿很容易被直观感受
8 | 2. 卡顿问题难以定位
9 |
10 | 2. 卡顿问题难在哪里?
11 |
12 | 1. 产生原因错综复杂:代码、内存、绘制、IO
13 | 2. 不易复现:当时场景强相关
14 |
15 | 3. 工具介绍
16 |
17 | 1. CPU Profiler
18 |
19 | 1. 图形化形式展示执行时间、调用栈等
20 |
21 | 2. 信息全面,包含所有线程
22 |
23 | 3. 运行时开销严重,整体都会变慢
24 |
25 | 4. 使用方式
26 |
27 | ```Java
28 | Debug.startMethodTracing("");
29 | Debug.stopMethodTracing("");
30 | // 生成文件在sd卡:Android/data/packagename/files
31 | ```
32 |
33 | 2. Systrace
34 |
35 | 1. 监控和跟踪Api调用、线程运行情况,生成html报告
36 |
37 | 2. API 18以上使用,推荐TraceCompat
38 |
39 | 3. 使用方式
40 |
41 | ```python
42 | python systrace.py -t 10 [other-options][categories]
43 | ```
44 |
45 |
46 |
47 | 4. 优点
48 |
49 | 1. 轻量级,开销小
50 | 2. 直观反映CPU利用率
51 | 3. 给出建议
52 |
53 | 3. StrictMode
54 |
55 | 1. 严苛模式,Android提供的一种运行时检测机制
56 |
57 | 2. 方便强大,容易被忽视
58 |
59 | 3. 包含:线程策略和虚拟机策略检测
60 |
61 | 1. 线程策略
62 | 1. 自定义的耗时调用,detectCustomSlowCalls()
63 | 2. 磁盘读取操作,detectDiskReads
64 | 3. 网络操作,detectNetwork
65 | 2. 虚拟机策略
66 | 1. Activity泄漏,detectActivityLeaks()
67 | 2. Sqlite对象泄漏,detectLeadedSqlLiteObjects
68 | 3. 检测实例数量,setClassInstanceLimit()
69 |
70 | 4. 使用:
71 |
72 | ```Java
73 | private void initStrictMode() {
74 | if (DEV_MODE) {
75 | StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
76 | .detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
77 | .detectDiskReads()
78 | .detectDiskWrites()
79 | .detectNetwork()// or .detectAll() for all detectable problems
80 | .penaltyLog() //在Logcat 中打印违规异常信息
81 | .build());
82 | StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
83 | .detectLeakedSqlLiteObjects()
84 | .setClassInstanceLimit(NewsItem.class, 1)
85 | .detectLeakedClosableObjects() //API等级11
86 | .penaltyLog()
87 | .build());
88 | }
89 | }
90 | ```
91 |
92 | #### 6-3 自动化卡顿检测方案及优化
93 |
94 | 1. 为什么需要自动化检测方案?
95 | 1. 系统工具适合线下针对性分析
96 | 2. 线上及测试环节需要自动化检测方案
97 | 2. 自动卡顿检测方案原理
98 | 1. 消息处理机制,一个线程只有一个Looper
99 | 2. mLogging对象在每个message处理前后被调用
100 | 3. 主线程发生卡顿,是在dispatchMessage执行耗时操作
101 | 4. 具体实现
102 | 1. Looper.getMainLooper().setMessageLogging();
103 | 2. 匹配 >> Dispatching,阈值时间后执行任务(获取堆栈)
104 | 3. 匹配 <<<< Finished,任务启动之前取消掉
105 | 3. AndroidPerformanceMonitor实战:监控msg
106 | 1. 非侵入式的性能监控组件,通知形式弹出卡顿信息
107 | 2. com.github.markzhai:blockcanary-android
108 | 3.
109 | 4. 问题及优化
110 | 1. 问题
111 | 1. 确实卡顿了,但卡顿堆栈可能不准确
112 | 2. 和OOM一样,最后的堆栈只是表象,不是真正问题
113 | 3. 高频卡顿上报量太大,服务端有压力
114 | 2. 优化
115 | 1. 获取监控周期内的多个堆栈,而不仅是最后一个
116 | 2. 对一个卡顿下堆栈进行hash排重,找出重复的堆栈
117 |
118 | #### 6-4 ANR分析与实战
119 |
120 | 1. ANR介绍及实战
121 | 1. KeyDispatchTimeout,5s
122 | 2. BroadcastTimeout,前台10s,后台60s
123 | 3. ServiceTimeout,前台20s,后台200s
124 | 2. ANR执行流程
125 | 1. 发送ANR
126 | 2. 进程接收异常终止信号,开始写入进程ANR信息
127 | 3. 弹出ANR提示框(ROM表现不一)
128 | 3. ANR解决套路
129 | 1. adb pull data/anr/traces.txt 存储路径
130 | 2. 详细分析:CPU、IO、锁
131 | 4. 线上ANR监控方案
132 | 1. 通过FileObserver监控文件变化,高版本权限问题
133 | 2. ANR-WatchDog
134 | 5. ANR-WatchDog原理及实战
135 | 1. 非侵入式的ANR监控组件,弥补高版本无权限问题
136 | 2. compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'
137 | 3.
138 | 4. start -> post消息改值 -> sleep -> 检测是否修改 -> 判断ANR发生
139 | 6. 区别
140 | 1. AndroidPerformanceMonitor:监控msg
141 | 2. ANR-WatchDog:看最终结果
142 | 3. 前者适合监控卡顿,后者适合补充ANR监控
143 |
144 | #### 6-5 卡顿单点问题检测方案
145 |
146 | 1. 背景介绍
147 | 1. 自动卡顿监测方案并不够
148 | 2. 体系化解决方案务必尽早暴露问题
149 | 3. 单点问题:主线程IPC、DB
150 | 2. IPC问题监测
151 | 1. 监测指标
152 | 1. IPC调用类型
153 | 2. 调用耗时、次数
154 | 3. 调用堆栈、发生线程
155 | 2. 常规方案
156 | 1. IPC前后加埋点
157 | 2. 不优雅、容易忘记
158 | 3. 维护成本大
159 | 3. 监测技巧
160 | 1. adb命令
161 | 1. `adb shell am trace-ipc start`
162 | 2. `adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt`
163 | 3. `adb pull /data/local/tmp/ipc-trace.txt`
164 | 2. 优雅方案
165 | 1. ARThook 还是 AspectJ ?
166 | 2. ARThook:可以Hook系统方法
167 | 3. AspectJ:非系统方法
168 | 3. 单点问题监测方案
169 | 1. 利用ARTHook完善线下工具
170 | 2. 开发阶段Hook相关操作,暴露、分析问题
171 | 3. 监控纬度
172 | 1. IPC
173 | 2. IO、DB
174 | 3. View绘制
175 |
176 | #### 6-6 如何实现界面秒开
177 |
178 | 1. 界面秒开实现
179 |
180 | 1. 界面秒开就是一个小的启动优化
181 | 2. 可以借鉴启动优化及布局优化章节
182 | 3. SysTrace,优雅异步+优雅延迟初始化
183 | 4. 异步Inflate、X2C、绘制优化
184 | 5. 提前获取页面数据
185 |
186 | 2. 界面秒开率统计
187 |
188 | 1. onCreate 到 onWindowFocusChanged
189 | 2. 特定接口
190 |
191 | 3. Lancet
192 |
193 | 1. 轻量级Android AOP框架
194 |
195 | 1. 编译速度快,支持增量编译
196 | 2. API简单,没有任何多余代码插入apk
197 | 3.
198 |
199 | 2. @Proxy 通常用与对系统API调用的Hook
200 |
201 | 3. @Insert 常用于操作App与library的类
202 |
203 | 4. ActivityHooker
204 |
205 | ```Java
206 | @Insert(value = "onCreate",mayCreateSuper = true)
207 | @TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
208 | protected void onCreate(Bundle savedInstanceState) {
209 | sActivityRecord.mOnCreateTime = System.currentTimeMillis();
210 | Origin.callVoid();
211 | }
212 |
213 | @Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
214 | @TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
215 | public void onWindowFocusChanged(boolean hasFocus) {
216 | sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
217 | LogUtils.i("onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
218 | Origin.callVoid();
219 | }
220 | ```
221 |
222 | 4. 界面秒开监控纬度
223 |
224 | 1. 总体耗时
225 | 2. 生命周期耗时
226 | 3. 生命周期间隔耗时
227 |
228 | #### 6-7 优雅监控耗时盲区
229 |
230 | #### 6-8 卡顿优化技巧总结初步
231 |
232 | #### 6-9 卡顿优化模拟面试
--------------------------------------------------------------------------------
/docs/3.App启动优化.md:
--------------------------------------------------------------------------------
1 | #### 3-2 App启动优化介绍
2 |
3 | 1. 背景介绍
4 |
5 | 1. 启动是第一体验
6 | 2. 八秒定律
7 |
8 | 2. 启动分类
9 |
10 | 1. [App startup time]()
11 |
12 | 1. 冷启动
13 |
14 | 耗时最多,衡量标准
15 |
16 | 需要了解冷启动流程
17 |
18 | 2. 热启动
19 |
20 | 最快
21 |
22 | 3. 温启动
23 |
24 | 较快
25 |
26 | 3. 相关任务
27 |
28 | 1. 冷启动之前
29 | 1. 启动App
30 | 2. 加载空白Window
31 | 3. 创建进程
32 | 2. 随后任务
33 | 1. 创建Application
34 | 2. 启动主线程
35 | 3. 创建MainActivity
36 | 4. 加载布局
37 | 5. 布置屏幕
38 | 6. 首帧绘制
39 |
40 | 4. 优化方向
41 |
42 | Application和Activity生命周期
43 |
44 | #### 3-3 启动时间测量方式
45 |
46 | 1. adb命令
47 |
48 | ```shell
49 | adb shell am start -W packagename/packagename.首屏Activity
50 | eg:adb shell am start -W com.iyueke.kids/com.iyueke.kids.ui.detail.splash.SplashActivity
51 | ```
52 |
53 | ThisTime:最后一个Activity启动耗时
54 |
55 | TotalTime:所有Activity启动耗时
56 |
57 | WaitTime:AMS启动Activity的总耗时
58 |
59 | 线下使用方便,不能带到线上,可以参照竞品
60 |
61 | 非严谨、精确时间
62 |
63 | 2. 手动打点
64 |
65 | 启动时埋点,启动结束埋点,二者差值
66 |
67 | LaunchTimer类辅助打点
68 |
69 | 开始时间:Application#attachBaseContext
70 |
71 | 结束时间误区:Activity#onWindowFocusChanged只是首帧时间
72 |
73 | 结束时间正解:真实数据展示,数据第一条展示
74 |
75 | 精确,可带到线上,推荐使用
76 |
77 | 避开误区,采用数据第一条展示
78 |
79 | #### 3-4 启动优化工具选择
80 |
81 | 1. traceview
82 |
83 | 图形的形式展示执行时间、调用栈等
84 |
85 | 信息全面,包含所有线程
86 |
87 | 使用方式:
88 |
89 | 1. `Debug.startMethodTracing("App")`
90 | 2. `Debug.stopMethodTracing("")`
91 | 3. 生成文件在sd卡:Android/data/packagename/files
92 | 4. 在Android Studio中打开生成文件
93 |
94 | 运行时开销严重,整体都会变慢
95 |
96 | 可能会带偏优化方向
97 |
98 | traceview和cpu profiler
99 |
100 | 2. systrace
101 |
102 | 结合Android内核的数据,生成Html报告
103 |
104 | API 18以上使用,推荐TraceCompat
105 |
106 | 使用方式:
107 |
108 | 1. `TraceCompat.beginSection("ApponCreate")`
109 | 2. `TraceCompat.endSection()`
110 | 3. 运行python脚本:`python systrace.py -t 10 [other-options][categories]`
111 |
112 | 轻量级,开销小
113 |
114 | 直观反映cpu利用率
115 |
116 | cputime和walltime区别
117 |
118 | 1. cputime是代码消耗cpu的时间(重点指标)
119 | 2. walltime是代码执行时间
120 | 3. cputime为什么和walltime不一样,举例:锁冲突
121 |
122 | #### 3-6 优雅获取方法耗时讲解
123 |
124 | 1. 常规方式
125 |
126 | 背景:需要知道启动阶段所有方法耗时
127 |
128 | 实现:手动埋点
129 |
130 | 1. `long time = System.currentTimeMillis()`
131 | 2. `long cost= System.currentTimeMillis() - time`
132 | 3. 或者`SystemClock.currentThreadTimeMillis()`
133 |
134 | 侵入性强、工作量大
135 |
136 | 2. AOP介绍
137 |
138 | Aspect Oriented Programming,面向切面编程
139 |
140 | 1. 针对同一类问题的统一处理
141 | 2. 无侵入添加代码
142 |
143 | AspectJ使用
144 |
145 | 1. `classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'`
146 | 2. `implementation 'org.aspectj:aspectjrt:1.8.+'`
147 | 3. `apply plugin: 'android-aspectjx'`
148 |
149 | Join Points:程序运行时的执行点,可以作为切面的地方
150 |
151 | 1. 函数调用、执行
152 | 2. 获取、设置变量
153 | 3. 类初始化
154 |
155 | PointCut:带条件的Join Points
156 |
157 | Advice:一种Hook,要插入代码的位置
158 |
159 | 1. Before:PointCut之前执行
160 | 2. After:PointCut之后执行
161 | 3. Around:PointCut之前、之后分别执行
162 |
163 | 语法简介
164 |
165 | 1. Before:Advice,具体插入位置
166 | 2. execution:处理Join Point的类型,call、execution
167 | 3. 匹配规则
168 | 4. onActivityCalled:要插入的代码
169 |
170 | #### 3-7 优雅获取方法耗时实操
171 |
172 | 具体实现见:PerformanceAop
173 |
174 | 优点:无侵入性、修改方便
175 |
176 | #### 3-8 异步优化详解
177 |
178 | 1. 优化技巧
179 |
180 | Theme切换:感觉上的快
181 |
182 | 步骤
183 |
184 | 1. lanucher.xml
185 |
186 | 2. ``
187 |
188 | 3. ```java
189 | @Override
190 | protected void onCreate(Bundle savedInstanceState) {
191 | // super.onCreate之前切换回来
192 | setTheme(R.style.AppTheme);
193 | super.onCreate(savedInstanceState);
194 | }
195 | ```
196 |
197 | 2. 异步优化
198 |
199 | 核心思想:子线程分担主线程任务,并行减少时间
200 |
201 | ```java
202 | // 线程池
203 | ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
204 | service.submit(new Runnable() {
205 | @Override
206 | public void run() {
207 | initBugly();
208 | }
209 | });
210 | // ...
211 |
212 | ```
213 |
214 | ```java
215 | // 必须被满足一次
216 | CountDownLatch mCountDownLatch = new CountDownLatch(1);
217 | service.submit(new Runnable() {
218 | @Override
219 | public void run() {
220 | // 需要在某阶段完成
221 | initBugly();
222 | mCountDownLatch.countDown();
223 | }
224 | });
225 | try {
226 | // 等待
227 | mCountDownLatch.await();
228 | } catch (InterruptedException e) {
229 | e.printStackTrace();
230 | }
231 | ```
232 |
233 | 异步优化注意事项:
234 |
235 | 1. 不符合异步要求
236 | 2. 需要在某阶段完成
237 | 3. 区分CPU密集型和IO密集型任务
238 |
239 | #### 3-9 异步初始化最优解-启动器
240 |
241 | 1. 常规异步痛点
242 |
243 | 1. 代码不优雅
244 | 2. 场景不好处理(依赖关系)
245 | 3. 维护成本高
246 |
247 | 2. 启动器介绍
248 |
249 | 核心思想:充分利用CPU多核,自动梳理任务顺序
250 |
251 | 启动器流程
252 |
253 | 1. 代码Task化,启动逻辑抽象为Task
254 | 2. 根据所有任务依赖关系排序生成一个有向无环图
255 | 3. 多线程按照排序后的优先级依次执行
256 |
257 | 3. 启动器实战
258 |
259 | ```java
260 | TaskDispatcher.init(PerformanceApp.this);
261 |
262 | TaskDispatcher dispatcher = TaskDispatcher.createInstance();
263 |
264 | dispatcher.addTask(new InitAMapTask())
265 | .addTask(new InitStethoTask())
266 | .addTask(new InitWeexTask())
267 | .addTask(new InitBuglyTask())
268 | .addTask(new InitFrescoTask())
269 | .addTask(new InitJPushTask())
270 | .addTask(new InitUmengTask())
271 | .addTask(new GetDeviceIdTask())
272 | .start();
273 |
274 | dispatcher.await();
275 | ```
276 |
277 | #### 3-11 更优秀的延迟初始化方案
278 |
279 | 1. 常规方案
280 |
281 | 1. `New Handler().postDelayed`
282 |
283 | 2. 数据展示后调用
284 |
285 | 3. ```java
286 | new DispatchRunnable(new DelayInitTaskA()).run();
287 | new DispatchRunnable(new DelayInitTaskB()).run();
288 | ```
289 |
290 | 常规初始化痛点:
291 |
292 | 1. 时机不便控制
293 | 2. 导致页面卡顿
294 |
295 | 2. 更优方案
296 |
297 | 核心思想:对延迟任务分批初始化
298 |
299 | 利用IdleHandler特性,空闲执行
300 |
301 | ```java
302 | DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
303 | delayInitDispatcher.addTask(new DelayInitTaskA())
304 | .addTask(new DelayInitTaskB())
305 | .start();
306 | ```
307 |
308 | 优点:
309 |
310 | 1. 执行时机明确
311 | 2. 缓解页面卡顿
312 |
313 | #### 3-12 启动优化其它方案
314 |
315 | 1. 优化总方针
316 | 1. 异步、延迟、懒加载
317 | 2. 技术、业务相结合
318 | 2. 注意事项
319 | 1. wall time与cpu time
320 | 1. cpu time才是优化方向
321 | 2. 按照systrace及cpu time跑满cpu
322 | 2. 监控的完善
323 | 1. 线上监控多阶段时间(App、Activity、生命周期间隔时间)
324 | 2. 处理聚合看趋势
325 | 3. 收敛启动代码修改权限
326 | 1. 结合ci修改启动代码需要Review或通知
327 | 3. 其它方案
328 | 1. 提前加载SharedPreferences
329 | 1. Multidex之前加载,利用此阶段cpu
330 | 2. 复写getApplicationContext()返回this
331 | 2. 启动阶段不启动子进程
332 | 1. 子进程会共享CPU资源,导致主进程CPU紧张
333 | 2. 注意启动顺序:App onCreate 之前是ContentProvider
334 | 3. 类加载优化:提前异步类加载
335 | 1. Class.forName() 只加载类本身及其静态变量的引用类
336 | 2. new 类实例可以额外加载类成员变量的引用类
337 | 4. 启动阶段抑制GC
338 | 5. CPU锁频
339 |
340 | #### 3-14 启动速度模拟面试
341 |
342 | 1. 你做启动优化是怎么做的?
343 | 1. 分析现状、确认问题
344 | 2. 针对性优化
345 | 3. 长期保持优化效果
346 | 2. 是怎么异步的,异步遇到问题没有
347 | 1. 体现演进过程
348 | 2. 详细介绍启动器
349 | 3. 你做了启动优化,觉得有哪些容易忽略的注意点
350 | 1. cpu time与wall time
351 | 2. 注意延迟初始化的优化
352 | 3. 介绍下黑科技
353 | 4. 版本迭代导致的启动变慢有好的解决方式吗
354 | 1. 启动器
355 | 2. 结合CI
356 | 3. 监控完善
--------------------------------------------------------------------------------
/app/src/main/java/com/optimize/performance/launchstarter/TaskDispatcher.java:
--------------------------------------------------------------------------------
1 | package com.optimize.performance.launchstarter;
2 |
3 | import android.content.Context;
4 | import android.os.Looper;
5 | import android.support.annotation.UiThread;
6 | import android.util.Log;
7 |
8 | import com.optimize.performance.launchstarter.sort.TaskSortUtil;
9 | import com.optimize.performance.launchstarter.stat.TaskStat;
10 | import com.optimize.performance.launchstarter.task.DispatchRunnable;
11 | import com.optimize.performance.launchstarter.task.Task;
12 | import com.optimize.performance.launchstarter.task.TaskCallBack;
13 | import com.optimize.performance.launchstarter.utils.DispatcherLog;
14 | import com.optimize.performance.utils.Utils;
15 |
16 | import java.util.ArrayList;
17 | import java.util.HashMap;
18 | import java.util.List;
19 | import java.util.concurrent.CountDownLatch;
20 | import java.util.concurrent.Future;
21 | import java.util.concurrent.TimeUnit;
22 | import java.util.concurrent.atomic.AtomicInteger;
23 |
24 | /**
25 | * 启动器调用类
26 | */
27 |
28 | public class TaskDispatcher {
29 | private long mStartTime;
30 | private static final int WAITTIME = 10000;
31 | private static Context sContext;
32 | private static boolean sIsMainProcess;
33 | private List mFutures = new ArrayList<>();
34 | private static volatile boolean sHasInit;
35 | private List mAllTasks = new ArrayList<>();
36 | private List> mClsAllTasks = new ArrayList<>();
37 | private volatile List mMainThreadTasks = new ArrayList<>();
38 | private CountDownLatch mCountDownLatch;
39 | private AtomicInteger mNeedWaitCount = new AtomicInteger();//保存需要Wait的Task的数量
40 | private List mNeedWaitTasks = new ArrayList<>();//调用了await的时候还没结束的且需要等待的Task
41 | private volatile List> mFinishedTasks = new ArrayList<>(100);//已经结束了的Task
42 | private HashMap, ArrayList> mDependedHashMap = new HashMap<>();
43 | private AtomicInteger mAnalyseCount = new AtomicInteger();//启动器分析的次数,统计下分析的耗时;
44 |
45 | private TaskDispatcher() {
46 | }
47 |
48 | public static void init(Context context) {
49 | if (context != null) {
50 | sContext = context;
51 | sHasInit = true;
52 | sIsMainProcess = Utils.isMainProcess(sContext);
53 | }
54 | }
55 |
56 | /**
57 | * 注意:每次获取的都是新对象
58 | *
59 | * @return
60 | */
61 | public static TaskDispatcher createInstance() {
62 | if (!sHasInit) {
63 | throw new RuntimeException("must call TaskDispatcher.init first");
64 | }
65 | return new TaskDispatcher();
66 | }
67 |
68 | public TaskDispatcher addTask(Task task) {
69 | if (task != null) {
70 | collectDepends(task);
71 | mAllTasks.add(task);
72 | mClsAllTasks.add(task.getClass());
73 | // 非主线程且需要wait的,主线程不需要CountDownLatch也是同步的
74 | if (ifNeedWait(task)) {
75 | mNeedWaitTasks.add(task);
76 | mNeedWaitCount.getAndIncrement();
77 | }
78 | }
79 | return this;
80 | }
81 |
82 | private void collectDepends(Task task) {
83 | if (task.dependsOn() != null && task.dependsOn().size() > 0) {
84 | for (Class extends Task> cls : task.dependsOn()) {
85 | if (mDependedHashMap.get(cls) == null) {
86 | mDependedHashMap.put(cls, new ArrayList());
87 | }
88 | mDependedHashMap.get(cls).add(task);
89 | if (mFinishedTasks.contains(cls)) {
90 | task.satisfy();
91 | }
92 | }
93 | }
94 | }
95 |
96 | private boolean ifNeedWait(Task task) {
97 | return !task.runOnMainThread() && task.needWait();
98 | }
99 |
100 | @UiThread
101 | public void start() {
102 | mStartTime = System.currentTimeMillis();
103 | if (Looper.getMainLooper() != Looper.myLooper()) {
104 | throw new RuntimeException("must be called from UiThread");
105 | }
106 | if (mAllTasks.size() > 0) {
107 | mAnalyseCount.getAndIncrement();
108 | printDependedMsg();
109 | mAllTasks = TaskSortUtil.getSortResult(mAllTasks, mClsAllTasks);
110 | mCountDownLatch = new CountDownLatch(mNeedWaitCount.get());
111 |
112 | sendAndExecuteAsyncTasks();
113 |
114 | DispatcherLog.i("task analyse cost " + (System.currentTimeMillis() - mStartTime) + " begin main ");
115 | executeTaskMain();
116 | }
117 | DispatcherLog.i("task analyse cost startTime cost " + (System.currentTimeMillis() - mStartTime));
118 | }
119 |
120 | public void cancel() {
121 | for (Future future : mFutures) {
122 | future.cancel(true);
123 | }
124 | }
125 |
126 | private void executeTaskMain() {
127 | mStartTime = System.currentTimeMillis();
128 | for (Task task : mMainThreadTasks) {
129 | long time = System.currentTimeMillis();
130 | new DispatchRunnable(task,this).run();
131 | DispatcherLog.i("real main " + task.getClass().getSimpleName() + " cost " +
132 | (System.currentTimeMillis() - time));
133 | }
134 | DispatcherLog.i("maintask cost " + (System.currentTimeMillis() - mStartTime));
135 | }
136 |
137 | private void sendAndExecuteAsyncTasks() {
138 | for (Task task : mAllTasks) {
139 | if (task.onlyInMainProcess() && !sIsMainProcess) {
140 | markTaskDone(task);
141 | } else {
142 | sendTaskReal(task);
143 | }
144 | task.setSend(true);
145 | }
146 | }
147 |
148 | /**
149 | * 查看被依赖的信息
150 | */
151 | private void printDependedMsg() {
152 | DispatcherLog.i("needWait size : " + (mNeedWaitCount.get()));
153 | if (false) {
154 | for (Class extends Task> cls : mDependedHashMap.keySet()) {
155 | DispatcherLog.i("cls " + cls.getSimpleName() + " " + mDependedHashMap.get(cls).size());
156 | for (Task task : mDependedHashMap.get(cls)) {
157 | DispatcherLog.i("cls " + task.getClass().getSimpleName());
158 | }
159 | }
160 | }
161 | }
162 |
163 | /**
164 | * 通知Children一个前置任务已完成
165 | *
166 | * @param launchTask
167 | */
168 | public void satisfyChildren(Task launchTask) {
169 | ArrayList arrayList = mDependedHashMap.get(launchTask.getClass());
170 | if (arrayList != null && arrayList.size() > 0) {
171 | for (Task task : arrayList) {
172 | task.satisfy();
173 | }
174 | }
175 | }
176 |
177 | public void markTaskDone(Task task) {
178 | if (ifNeedWait(task)) {
179 | mFinishedTasks.add(task.getClass());
180 | mNeedWaitTasks.remove(task);
181 | mCountDownLatch.countDown();
182 | mNeedWaitCount.getAndDecrement();
183 | }
184 | }
185 |
186 | private void sendTaskReal(final Task task) {
187 | if (task.runOnMainThread()) {
188 | mMainThreadTasks.add(task);
189 |
190 | if (task.needCall()) {
191 | task.setTaskCallBack(new TaskCallBack() {
192 | @Override
193 | public void call() {
194 | TaskStat.markTaskDone();
195 | task.setFinished(true);
196 | satisfyChildren(task);
197 | markTaskDone(task);
198 | DispatcherLog.i(task.getClass().getSimpleName() + " finish");
199 |
200 | Log.i("testLog", "call");
201 | }
202 | });
203 | }
204 | } else {
205 | // 直接发,是否执行取决于具体线程池
206 | Future future = task.runOn().submit(new DispatchRunnable(task,this));
207 | mFutures.add(future);
208 | }
209 | }
210 |
211 | public void executeTask(Task task) {
212 | if (ifNeedWait(task)) {
213 | mNeedWaitCount.getAndIncrement();
214 | }
215 | task.runOn().execute(new DispatchRunnable(task,this));
216 | }
217 |
218 | @UiThread
219 | public void await() {
220 | try {
221 | if (DispatcherLog.isDebug()) {
222 | DispatcherLog.i("still has " + mNeedWaitCount.get());
223 | for (Task task : mNeedWaitTasks) {
224 | DispatcherLog.i("needWait: " + task.getClass().getSimpleName());
225 | }
226 | }
227 |
228 | if (mNeedWaitCount.get() > 0) {
229 | mCountDownLatch.await(WAITTIME, TimeUnit.MILLISECONDS);
230 | }
231 | } catch (InterruptedException e) {
232 | }
233 | }
234 |
235 | public static Context getContext() {
236 | return sContext;
237 | }
238 |
239 | public static boolean isMainProcess() {
240 | return sIsMainProcess;
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/app/src/main/java/com/optimize/performance/PerformanceApp.java:
--------------------------------------------------------------------------------
1 | package com.optimize.performance;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.os.Handler;
7 | import android.os.Looper;
8 | import android.os.StrictMode;
9 | import android.support.multidex.MultiDex;
10 | import android.util.Log;
11 | import android.widget.ImageView;
12 |
13 | import com.amap.api.location.AMapLocation;
14 | import com.amap.api.location.AMapLocationClient;
15 | import com.amap.api.location.AMapLocationClientOption;
16 | import com.amap.api.location.AMapLocationListener;
17 | import com.facebook.drawee.backends.pipeline.Fresco;
18 | import com.facebook.stetho.Stetho;
19 | import com.optimize.performance.bean.NewsItem;
20 | import com.optimize.performance.launchstarter.TaskDispatcher;
21 | import com.optimize.performance.memory.ImageHook;
22 | import com.optimize.performance.tasks.GetDeviceIdTask;
23 | import com.optimize.performance.tasks.InitAMapTask;
24 | import com.optimize.performance.tasks.InitBuglyTask;
25 | import com.optimize.performance.tasks.InitFrescoTask;
26 | import com.optimize.performance.tasks.InitJPushTask;
27 | import com.optimize.performance.tasks.InitStethoTask;
28 | import com.optimize.performance.tasks.InitUmengTask;
29 | import com.optimize.performance.tasks.InitWeexTask;
30 | import com.optimize.performance.utils.LaunchTimer;
31 | import com.optimize.performance.utils.LogUtils;
32 | import com.taobao.android.dexposed.DexposedBridge;
33 | import com.taobao.android.dexposed.XC_MethodHook;
34 | import com.taobao.weex.InitConfig;
35 | import com.taobao.weex.WXSDKEngine;
36 | import com.tencent.bugly.crashreport.CrashReport;
37 | import com.tencent.mmkv.MMKV;
38 | import com.umeng.commonsdk.UMConfigure;
39 |
40 | import java.io.BufferedReader;
41 | import java.io.IOException;
42 | import java.io.InputStreamReader;
43 | import java.util.concurrent.CountDownLatch;
44 |
45 | import cn.jpush.android.api.JPushInterface;
46 |
47 | public class PerformanceApp extends Application {
48 |
49 | private AMapLocationClient mLocationClient = null;
50 | private AMapLocationClientOption mLocationOption = null;
51 |
52 | public void setDeviceId(String deviceId) {
53 | this.mDeviceId = deviceId;
54 | }
55 |
56 | public String getDeviceId() {
57 | return mDeviceId;
58 | }
59 |
60 | private String mDeviceId;
61 | private static Application mApplication;
62 | private boolean DEV_MODE = true;
63 |
64 | private CountDownLatch mCountDownLatch = new CountDownLatch(1);
65 |
66 | private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
67 | private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
68 |
69 | private AMapLocationListener mLocationListener = new AMapLocationListener() {
70 | @Override
71 | public void onLocationChanged(AMapLocation aMapLocation) {
72 | // 一些处理
73 | }
74 | };
75 |
76 | private int mCrashTimes;
77 |
78 | @Override
79 | protected void attachBaseContext(Context base) {
80 | super.attachBaseContext(base);
81 |
82 | if(mCrashTimes > 3){
83 | // 删除文件,恢复到重新安装的状态
84 | }
85 |
86 | if(mCrashTimes > 5){
87 | // 清除热修信息
88 | }
89 |
90 | LaunchTimer.startRecord();
91 | MultiDex.install(this);
92 | DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
93 | @Override
94 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
95 | super.afterHookedMethod(param);
96 | Thread thread = (Thread) param.thisObject;
97 | LogUtils.i(thread.getName()+" stack "+Log.getStackTraceString(new Throwable()));
98 | }
99 | });
100 | }
101 |
102 | @Override
103 | public void onCreate() {
104 | super.onCreate();
105 |
106 | MMKV.initialize(PerformanceApp.this);
107 | MMKV.defaultMMKV().encode("times",100);
108 |
109 | int times = MMKV.defaultMMKV().decodeInt("times");
110 |
111 |
112 | LaunchTimer.startRecord();
113 | mApplication = this;
114 |
115 | TaskDispatcher.init(PerformanceApp.this);
116 |
117 | TaskDispatcher dispatcher = TaskDispatcher.createInstance();
118 |
119 | dispatcher.addTask(new InitAMapTask())
120 | .addTask(new InitStethoTask())
121 | .addTask(new InitWeexTask())
122 | .addTask(new InitBuglyTask())
123 | .addTask(new InitFrescoTask())
124 | .addTask(new InitJPushTask())
125 | .addTask(new InitUmengTask())
126 | .addTask(new GetDeviceIdTask())
127 | .start();
128 |
129 | dispatcher.await();
130 |
131 | LaunchTimer.endRecord();
132 |
133 | DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
134 | @Override
135 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
136 | super.afterHookedMethod(param);
137 | DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
138 | }
139 | });
140 |
141 |
142 | // try {
143 | // DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
144 | // int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
145 | // @Override
146 | // protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
147 | // LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObjecObservablet.getClass().getSimpleName()
148 | // + "\n" + Log.getStackTraceString(new Throwable()));
149 | // super.beforeHookedMethod(param);
150 | // }
151 | // });
152 | // } catch (ClassNotFoundException e) {
153 | // e.printStackTrace();
154 | // }
155 |
156 | // BlockCanary.install(this, new AppBlockCanaryContext()).start();
157 |
158 | initStrictMode();
159 |
160 | // new ANRWatchDog().start();
161 | }
162 |
163 | private void initStrictMode() {
164 | if (DEV_MODE) {
165 | StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
166 | .detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
167 | .detectDiskReads()
168 | .detectDiskWrites()
169 | .detectNetwork()// or .detectAll() for all detectable problems
170 | .penaltyLog() //在Logcat 中打印违规异常信息
171 | .build());
172 | StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
173 | .detectLeakedSqlLiteObjects()
174 | .setClassInstanceLimit(NewsItem.class, 1)
175 | .detectLeakedClosableObjects() //API等级11
176 | .penaltyLog()
177 | .build());
178 | }
179 | }
180 |
181 |
182 | private void initStetho() {
183 | Handler handler = new Handler(Looper.getMainLooper());
184 | Stetho.initializeWithDefaults(this);
185 | }
186 |
187 | private void initWeex() {
188 | InitConfig config = new InitConfig.Builder().build();
189 | WXSDKEngine.initialize(this, config);
190 | }
191 |
192 | private void initJPush() {
193 | JPushInterface.init(this);
194 | JPushInterface.setAlias(this, 0, mDeviceId);
195 | }
196 |
197 | private void initFresco() {
198 | Fresco.initialize(this);
199 | }
200 |
201 | private void initAMap() {
202 | mLocationClient = new AMapLocationClient(getApplicationContext());
203 | mLocationClient.setLocationListener(mLocationListener);
204 | mLocationOption = new AMapLocationClientOption();
205 | mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
206 | mLocationOption.setOnceLocation(true);
207 | mLocationClient.setLocationOption(mLocationOption);
208 | mLocationClient.startLocation();
209 | }
210 |
211 | private void initUmeng() {
212 | UMConfigure.init(this, "58edcfeb310c93091c000be2", "umeng",
213 | UMConfigure.DEVICE_TYPE_PHONE, "1fe6a20054bcef865eeb0991ee84525b");
214 | }
215 |
216 | private void initBugly() {
217 | CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false);
218 | }
219 |
220 | public String getStringFromAssets(String fileName) {
221 | String Result = "";
222 | InputStreamReader inputReader = null;
223 | BufferedReader bufReader = null;
224 | try {
225 | inputReader = new InputStreamReader(getResources().getAssets().open(fileName));
226 | bufReader = new BufferedReader(inputReader);
227 | String line = "";
228 | while ((line = bufReader.readLine()) != null) {
229 | Result += line;
230 | }
231 | } catch (Exception e) {
232 | e.printStackTrace();
233 | } finally {
234 | if (inputReader != null) {
235 | try {
236 | inputReader.close();
237 | } catch (IOException e) {
238 | e.printStackTrace();
239 | }
240 | }
241 | if (bufReader != null) {
242 | try {
243 | bufReader.close();
244 | } catch (IOException e) {
245 | e.printStackTrace();
246 | }
247 | }
248 | }
249 | return Result;
250 | }
251 |
252 | public static Application getApplication() {
253 | return mApplication;
254 | }
255 |
256 | }
257 |
--------------------------------------------------------------------------------
/app/src/main/java/com/optimize/performance/ui/DroidCardsView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.optimize.performance.ui;
18 |
19 | import android.content.Context;
20 | import android.content.res.Resources;
21 | import android.graphics.Bitmap;
22 | import android.graphics.BitmapFactory;
23 | import android.graphics.Canvas;
24 | import android.graphics.Color;
25 | import android.graphics.Paint;
26 | import android.graphics.Rect;
27 | import android.os.AsyncTask;
28 | import android.view.View;
29 |
30 | import java.lang.ref.WeakReference;
31 | import java.util.ArrayList;
32 |
33 | /**
34 | * A custom view that displays a stacked collection of droid cards. See onDraw() for use of a
35 | * clipRect() to restrict the drawing area to avoid partial overdraws.
36 | */
37 | class DroidCardsView extends View {
38 | /**
39 | * The list of droids displayed in this view.
40 | */
41 | private Droid[] mDroids;
42 |
43 | /**
44 | * The width of the droid image. In this sample, we hard code an image width in
45 | * DroidCardsActivity and pass it as an argument to this view.
46 | */
47 | float mDroidImageWidth;
48 |
49 | /**
50 | * The distance between the left edges of two adjacent cards. The cards overlap horizontally.
51 | */
52 | protected float mCardSpacing;
53 |
54 | /**
55 | * Keeps track of the left coordinate for each card.
56 | */
57 | private float mCardLeft;
58 |
59 | /**
60 | * A list of DroidCards objected. We use Asynctasks to populate the contents of this list. See
61 | * the DroidCardWorkerTask class that extends AsyncTask.
62 | */
63 | private ArrayList mDroidCards = new ArrayList();
64 |
65 | /**
66 | *
67 | * @param context The app context.
68 | * @param droids The Droid objects associated with DroidCards.
69 | * @param droidImageWidth The width of each Droid image. Hardcoded in DroidCardsActivity.
70 | * @param cardSpacing The distance between the left edges of two adjacent cards.
71 | */
72 | public DroidCardsView(Context context, Droid[] droids, float droidImageWidth,
73 | float cardSpacing) {
74 | super(context);
75 |
76 | mDroids = droids;
77 | mDroidImageWidth = droidImageWidth;
78 | mCardSpacing = cardSpacing;
79 |
80 | // Fire AsyncTasks to fetch and scale the bitmaps.
81 | for (int i = 0; i < mDroids.length; i++) {
82 | new DroidCardWorkerTask().execute(mDroids[i]);
83 | }
84 | }
85 |
86 | /**
87 | * Custom implementation to do drawing in this view. Waits for the AsyncTasks to fetch
88 | * bitmaps for each Droid and populate mDroidCards, a list of DroidCard objects. Then, draws
89 | * overlapping droid cards.
90 | */
91 | protected void onDraw(Canvas canvas) {
92 | super.onDraw(canvas);
93 |
94 | // Don't draw anything until all the Asynctasks are done and all the DroidCards are ready.
95 | if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) {
96 | // Loop over all the droids, except the last one.
97 | int i;
98 | for (i = 0; i < mDroidCards.size() - 1; i++) {
99 |
100 | mCardLeft = i * mCardSpacing;
101 | canvas.save();
102 | // 指定绘制区域
103 |
104 | canvas.clipRect(mCardLeft,0,mCardLeft+mCardSpacing,mDroidCards.get(i).getHeight());
105 |
106 | // Draw the card. Only the parts of the card that lie within the bounds defined by
107 | // the clipRect() get drawn.
108 | drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);
109 |
110 | canvas.restore();
111 | }
112 |
113 | // Draw the final card. This one doesn't get clipped.
114 | drawDroidCard(canvas, mDroidCards.get(mDroidCards.size() - 1),
115 | mCardLeft + mCardSpacing, 0);
116 | }
117 |
118 | // Invalidate the whole view. Doing this calls onDraw() if the view is visible.
119 | invalidate();
120 | }
121 |
122 | /**
123 | * Draws a droid card to a canvas at the specified position.
124 | */
125 | protected void drawDroidCard(Canvas canvas, DroidCard droidCard, float left, float top) {
126 | Paint paint = new Paint();
127 | Bitmap bm = droidCard.getBitmap();
128 | Droid droid = droidCard.getDroid();
129 |
130 | // Draw outer rectangle.
131 | paint.setAntiAlias(true);
132 | Rect cardRect = new Rect(
133 | (int)left,
134 | (int)top,
135 | (int)left + (int) droidCard.getWidth(),
136 | (int)top + (int) droidCard.getHeight()
137 | );
138 | paint.setStyle(Paint.Style.STROKE);
139 | paint.setColor(Color.DKGRAY);
140 | canvas.drawRect(cardRect, paint);
141 |
142 | // Draw the bitmap centered in the card body.
143 | canvas.drawBitmap(
144 | bm,
145 | (cardRect.left + (droidCard.getWidth() / 2)) - (bm.getWidth() / 2),
146 | (cardRect.top + (int) droidCard.getHeaderHeight() + (droidCard.getBodyHeight() / 2)
147 | - (bm.getHeight() / 2)),
148 | null
149 | );
150 |
151 | // Write the droid's name in the header.
152 | paint.setTextSize(droidCard.getTitleSize());
153 | paint.setColor(getResources().getColor(droid.getColor()));
154 | paint.setStyle(Paint.Style.STROKE);
155 |
156 | // Calculate the the left and top offsets for the title.
157 | int titleLeftOffset = cardRect.left + (int) droidCard.getTitleXOffset();
158 | int titleTopOffset = cardRect.top + (int) droidCard.getTitleYOffset() +
159 | (int) droidCard.getTitleSize();
160 |
161 | // Draw the title text.
162 | canvas.drawText(droid.getName(), titleLeftOffset, titleTopOffset, paint);
163 | }
164 |
165 | /**
166 | * Creates and returns a bitmap from a drawable resource.
167 | */
168 | public Bitmap makeBitmap(Resources res, int resId, int reqWidth) {
169 | // First decode with inJustDecodeBounds=true to check dimensions
170 | final BitmapFactory.Options options = new BitmapFactory.Options();
171 | options.inJustDecodeBounds = true;
172 | BitmapFactory.decodeResource(res, resId, options);
173 |
174 | // Calculate inSampleSize.
175 | options.inSampleSize = calculateInSampleSize(options, reqWidth);
176 |
177 | // Decode bitmap with inSampleSize set.
178 | options.inJustDecodeBounds = false;
179 |
180 | // Decode bitmap.
181 | return BitmapFactory.decodeResource(res, resId, options);
182 | }
183 |
184 | /**
185 | * Returns a bitmap scaled to a specific width.
186 | */
187 | private Bitmap getScaledBitmap(Bitmap bitmap, float width) {
188 | float scale = width / bitmap.getWidth();
189 | return Bitmap.createScaledBitmap(bitmap, (int) (bitmap.getWidth() * scale),
190 | (int) (bitmap.getHeight() * scale), false);
191 | }
192 |
193 | /**
194 | * Requests the decoder to subsample the original image, possibly returning a smaller image to
195 | * save memory.
196 | */
197 | private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth) {
198 | // Get the raw width of image.
199 | final int width = options.outWidth;
200 | int inSampleSize = 1;
201 |
202 | // Calculate the best inSampleSize.
203 | if (width > reqWidth) {
204 | final int halfWidth = width / 2;
205 | while ((halfWidth / inSampleSize) > reqWidth) {
206 | inSampleSize *= 2;
207 | }
208 | }
209 | return inSampleSize;
210 | }
211 |
212 | /**
213 | * Worker that fetches bitmaps in the background and stores them in a list of DroidCards.
214 | * The order of the
215 | */
216 | class DroidCardWorkerTask extends AsyncTask {
217 | Droid droid;
218 | private final WeakReference> mDroidCardsReference;
219 |
220 | public DroidCardWorkerTask() {
221 | mDroidCardsReference = new WeakReference>(mDroidCards);
222 | }
223 |
224 | // Decode image in background.
225 | @Override
226 | protected Bitmap doInBackground(Droid... params) {
227 | droid = params[0];
228 | // Scale the bitmap.
229 | return getScaledBitmap(
230 | makeBitmap(getResources(), droid.getAvatarId(), (int) mDroidImageWidth),
231 | mDroidImageWidth
232 | );
233 | }
234 |
235 | /**
236 | * Creates a DroidCard using the retrieved bitmap and stores it in a DroidCards list.
237 | */
238 | @Override
239 | protected void onPostExecute(Bitmap bitmap) {
240 | // Check that the list and bitmap are not null.
241 | if (mDroidCardsReference != null && bitmap != null) {
242 | // Create a new DroidCard.
243 | mDroidCards.add(new DroidCard(droid, bitmap));
244 | }
245 | }
246 | }
247 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/optimize/performance/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.optimize.performance;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.job.JobInfo;
5 | import android.app.job.JobScheduler;
6 | import android.content.ComponentName;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.content.IntentFilter;
10 | import android.os.BatteryManager;
11 | import android.os.Build;
12 | import android.os.Bundle;
13 | import android.os.Handler;
14 | import android.os.Process;
15 | import android.support.annotation.NonNull;
16 | import android.support.annotation.Nullable;
17 | import android.support.v4.view.AsyncLayoutInflater;
18 | import android.support.v4.view.LayoutInflaterCompat;
19 | import android.support.v7.app.AppCompatActivity;
20 | import android.support.v7.widget.LinearLayoutManager;
21 | import android.support.v7.widget.RecyclerView;
22 | import android.text.TextUtils;
23 | import android.util.AttributeSet;
24 | import android.util.Log;
25 | import android.view.Choreographer;
26 | import android.view.LayoutInflater;
27 | import android.view.View;
28 | import android.view.ViewGroup;
29 | import android.view.animation.AlphaAnimation;
30 |
31 | import com.alibaba.fastjson.JSON;
32 | import com.optimize.performance.adapter.NewsAdapter;
33 | import com.optimize.performance.adapter.OnFeedShowCallBack;
34 | import com.optimize.performance.async.ThreadPoolUtils;
35 | import com.optimize.performance.bean.NewsItem;
36 | import com.optimize.performance.launchstarter.DelayInitDispatcher;
37 | import com.optimize.performance.net.JobSchedulerService;
38 | import com.optimize.performance.net.RetrofitNewsUtils;
39 | import com.optimize.performance.tasks.delayinittask.DelayInitTaskA;
40 | import com.optimize.performance.tasks.delayinittask.DelayInitTaskB;
41 | import com.optimize.performance.utils.ExceptionMonitor;
42 | import com.optimize.performance.utils.LaunchTimer;
43 | import com.optimize.performance.utils.LogUtils;
44 | import com.zhangyue.we.x2c.X2C;
45 | import com.zhangyue.we.x2c.ano.Xml;
46 |
47 | import org.json.JSONObject;
48 |
49 | import java.util.ArrayList;
50 | import java.util.Iterator;
51 | import java.util.List;
52 |
53 | import okhttp3.ResponseBody;
54 | import retrofit2.Call;
55 | import retrofit2.Callback;
56 | import retrofit2.Response;
57 |
58 | @Xml(layouts = "activity_main")
59 | public class MainActivity extends AppCompatActivity implements OnFeedShowCallBack {
60 |
61 | private AlphaAnimation alphaAnimation;
62 | private RecyclerView mRecyclerView;
63 | private NewsAdapter mNewsAdapter;
64 | private String mStringIds = "20190220005233,20190220005171,20190220005160,20190220005146,20190220001228,20190220001227,20190219006994,20190219006839,20190219005350,20190219005343,20190219004522,20190219004520,20190219000132,20190219000118,20190219000119,20190218009367,20190218009078,20190218009075,20190218008572,20190218008496,20190218006078,20190218006156,20190218006190,20190218006572,20190218006235,20190218006284,20190218006571,20190218006283,20190218006191,20190218005733,20190217004740,20190218001891,20190218001889,20190217004183,20190217004019,20190217004011,20190217003152,20190217002757,20190217002249,20190217000954,20190217000957,20190217000953,20190216004269,20190216003721,20190216003720,20190216003351,20190216003364,20190216002989,20190216002613,20190216000044,20190216000043,20190216000042,20190215007933,20190215008945,20190215007932,20190215007090,20190215005473,20190215005469,20190215005313,20190215004868,20190215004299,20190215001233,20190215001229,20190215001226,20190214008652,20190214008429,20190214009262,20190214008347,20190214008345,20190214007362,20190214006949,20190214006948,20190214006588,20190214006270,20190214006102,20190214005769,20190214005583,20190214005581,20190214005484,20190214005466,20190214005303,20190214004660,20190213009703,20190213009285,20190214002912,20190213007775,20190213007461,20190213007049,20190213007047,20190213006228,20190213006050,20190213005767,20190213005738,20190213005641,20190213005512,20190213004174,20190212007918,20190212007914,20190212007913,20190212007696,20190212007369,20190212007361,20190212006921,20190212006007,20190212005954,20190212005925,20190212005924,20190212005923,20190212005922,20190212005428,20190212005427,20190212005426,20190212005226,20190212004916,20190212004422,20190212004355,20190212004351,20190212000989,20190212000994,20190212000991,20190211005672,20190211004121,20190211004049,20190211003973,20190211003434,20190211003199,20190211005392,20190211003179,20190211000956,20190211000955,20190211003203,20190211003206,20190210004201,20190210003934,20190210004067,20190210003683,20190210003685,20190210003684,20190210003682,20190210003281,20190210002944,20190210002936,20190210003308,20190210002745,20190210002634,20190210002893,20190210002315,20190210001977,20190210002046,20190210001663,20190209004408,20190209003643,20190209003582,20190209003401,20190209003193,20190209002777,20190209002664,20190209002724,20190209002723,20190209002119,20190208001691,20190208004370,20190208000203,20190208004129,20190208003560,20190208002739,20190208002661,20190208000144,20190208000194,20190208002671,20190208003081,20190208002398,20190208000184,20190208001943,20190208000074,20190208000051,20190208000121,20190207003938,20190207003939,20190208002394,20190207003698,20190207001759,20190207003882,20190207003424,20190207002872,20190207003101,20190207002873,20190207002772,20190207002036,20190207001888,20190207000695,20190206004239,20190206004172,20190206002264,20190206002238,20190206002237,20190206004192,20190206004176,20190206003738,20190206003028";
65 |
66 | private long mStartFrameTime = 0;
67 | private int mFrameCount = 0;
68 | private static final long MONITOR_INTERVAL = 160L; //单次计算FPS使用160毫秒
69 | private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
70 | private static final long MAX_INTERVAL = 1000L; //设置计算fps的单位时间间隔1000ms,即fps/s;
71 |
72 |
73 | public List mItems = new ArrayList<>();
74 |
75 | @Override
76 | protected void onCreate(Bundle savedInstanceState) {
77 |
78 | // 以下代码是为了演示修改任务的名称
79 | ThreadPoolUtils.getService().execute(new Runnable() {
80 | @Override
81 | public void run() {
82 | Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
83 | String oldName = Thread.currentThread().getName();
84 | Thread.currentThread().setName("new Name");
85 | LogUtils.i("");
86 | Thread.currentThread().setName(oldName);
87 | }
88 | });
89 |
90 | // 以下代码是为了演示Msg导致的主线程卡顿
91 | new Handler().post(new Runnable() {
92 | @Override
93 | public void run() {
94 | LogUtils.i("Msg 执行");
95 | // try {
96 | // Thread.sleep(1000);
97 | // } catch (InterruptedException e) {
98 | // e.printStackTrace();
99 | // }
100 | }
101 | });
102 |
103 | LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
104 | @Override
105 | public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
106 |
107 | if (TextUtils.equals(name, "TextView")) {
108 | // 生成自定义TextView
109 | }
110 | long time = System.currentTimeMillis();
111 | View view = getDelegate().createView(parent, name, context, attrs);
112 | LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
113 | return view;
114 | }
115 |
116 | @Override
117 | public View onCreateView(String name, Context context, AttributeSet attrs) {
118 | return null;
119 | }
120 | });
121 |
122 | new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
123 | @Override
124 | public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
125 | setContentView(view);
126 | mRecyclerView = findViewById(R.id.recycler_view);
127 | mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
128 | mRecyclerView.setAdapter(mNewsAdapter);
129 | mNewsAdapter.setOnFeedShowCallBack(MainActivity.this);
130 | }
131 | });
132 |
133 | setTheme(R.style.AppTheme);
134 |
135 | super.onCreate(savedInstanceState);
136 | // setContentView(R.layout.activity_main);
137 | X2C.setContentView(MainActivity.this, R.layout.activity_main);
138 | mNewsAdapter = new NewsAdapter(mItems);
139 |
140 | IntentFilter filter = new IntentFilter();
141 | filter.addAction(Intent.ACTION_BATTERY_CHANGED);
142 | Intent intent = registerReceiver(null, filter);
143 | LogUtils.i("battery " + intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1));
144 |
145 | getNews();
146 | getFPS();
147 |
148 |
149 | // 以下代码是为了演示业务不正常场景下的监控
150 | try {
151 | // 一些业务处理
152 | Log.i("", "");
153 | } catch (Exception e) {
154 | ExceptionMonitor.monitor(Log.getStackTraceString(e));
155 | }
156 |
157 | boolean flag = true;
158 | if (flag) {
159 | // 正常,继续执行流程
160 | } else {
161 | ExceptionMonitor.monitor("");
162 | }
163 | }
164 |
165 | /**
166 | * 演示JobScheduler的使用
167 | */
168 | private void startJobScheduler() {
169 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
170 | JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
171 | JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), JobSchedulerService.class.getName()));
172 | builder.setRequiresCharging(true)
173 | .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
174 | jobScheduler.schedule(builder.build());
175 | }
176 | }
177 |
178 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
179 | private void getFPS() {
180 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
181 | return;
182 | }
183 | Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
184 | @Override
185 | public void doFrame(long frameTimeNanos) {
186 | if (mStartFrameTime == 0) {
187 | mStartFrameTime = frameTimeNanos;
188 | }
189 | long interval = frameTimeNanos - mStartFrameTime;
190 | if (interval > MONITOR_INTERVAL_NANOS) {
191 | double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
192 | mFrameCount = 0;
193 | mStartFrameTime = 0;
194 | } else {
195 | ++mFrameCount;
196 | }
197 | Choreographer.getInstance().postFrameCallback(this);
198 | }
199 | });
200 | }
201 |
202 | private void getNews() {
203 | RetrofitNewsUtils.getApiService().getNBANews("banner", mStringIds)
204 | .enqueue(new Callback() {
205 | @Override
206 | public void onResponse(Call call, Response response) {
207 | try {
208 | String json = response.body().string();
209 | JSONObject jsonObject = new JSONObject(json);
210 | JSONObject data = jsonObject.getJSONObject("data");
211 | Iterator keys = data.keys();
212 | while (keys.hasNext()) {
213 | String next = keys.next();
214 | JSONObject itemJO = data.getJSONObject(next);
215 | NewsItem newsItem = JSON.parseObject(itemJO.toString(), NewsItem.class);
216 | mItems.add(newsItem);
217 | }
218 | mNewsAdapter.setItems(mItems);
219 | } catch (Exception e) {
220 | e.printStackTrace();
221 | }
222 | }
223 |
224 | @Override
225 | public void onFailure(Call call, Throwable t) {
226 | }
227 | });
228 | }
229 |
230 | @Override
231 | protected void onResume() {
232 | super.onResume();
233 | // 以下代码是为了演示电量优化中对动画的处理
234 | // alphaAnimation.start();
235 | }
236 |
237 | @Override
238 | protected void onPause() {
239 | super.onPause();
240 | // 以下代码是为了演示电量优化中对动画的处理
241 | // alphaAnimation.cancel();
242 | }
243 |
244 | @Override
245 | public void onFeedShow() {
246 | // 以下两行是原有方式
247 | // new DispatchRunnable(new DelayInitTaskA()).run();
248 | // new DispatchRunnable(new DelayInitTaskB()).run();
249 |
250 | DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
251 | delayInitDispatcher.addTask(new DelayInitTaskA())
252 | .addTask(new DelayInitTaskB())
253 | .start();
254 |
255 | // 一系列操作 10s
256 | }
257 |
258 | @Override
259 | public void onWindowFocusChanged(boolean hasFocus) {
260 | super.onWindowFocusChanged(hasFocus);
261 | LaunchTimer.endRecord("onWindowFocusChanged");
262 | }
263 | }
264 |
--------------------------------------------------------------------------------