android 网络优化

前言

随着移动网络的不断升级,客户端的网络传输由3G进化到Wifi、4G,且Wifi场景越来越多。虽然网络环境在变好,但也对网络的应用提出了更高的要求,会发现很多大厂都十分重视网络指标,如果技术人员不加以控制,在弱网、体验、包括服务器带宽、流浪方面都会造成不同程度的损失。

网络指标

流量
网络第一个影响的app指标当然是流量,在手机设置界面可以清楚查看到每个 app 耗费的流量,极端情况下会造成用户卸载。

电量

频繁的网络请求和网络时常是引发电量的主要原因,每次发送请求都会激活电信号,同样耗电也会对用户产生恶略的影响,甚至卸载。

用户体验

一些网络框架会优化处理内部线程池,但线程池毕竟有限,网络请求过多或请求数据包体大,就会造成网络阻塞,也就是一些用户所谓的卡顿,在弱网方面可能表现更差,所以网络框架的好坏直接影响着整个app的用户体验。

安全

网络安全同样是网络优化中十分重要的一部分,怎样防止被第三方窃听/篡改或冒充,防止运营商劫持,同时又不影响性能,没有安全体系的网络架构同样会对服务端或者客户端造成直接或间接的影响。

网络监控

Network Monitor
Network Monitor Android Studio自带的网络监测工具,可以看出时间段之内的网络请求数量及访问速率。

Alt text

Charles 抓包工具

本人更习惯于使用 charles 进行网络监控,这可以查看更多的数据指标,查看请求request、response 更加直观。

Alt text

网络优化

本人觉得网络优化主要从以下几个方面入手:

1.请求速度
2.链接成功率
3.流量消耗
4.安全策略

网络请求过程:

1.DNS 解析,请求DNS服务器,获取IP地址
2.建立连接,包括 tcp 三次握手、安全协议
3.发送和接收数据,解码数据

优化DNS解析

1.DNS解析在网路错误中占比较高,使用IP直连方式替代DNS服务器解析,可以减少域名解析几百毫秒的时间消耗。

连接池复用

keep-alive HTTP 协议里有个 keep-alive,HTTP1.1默认开启,一定程度上缓解了每次请求都要进行TCP三次握手建立连接的耗时。原理是请求完成后不立即释放连接,而是放入连接池中,若这时有另一个请求要发出,请求的域名和端口是一样的,就直接拿出连接池中的连接进行发送和接收数据,少了建立连接的耗时。

数据压缩

目前比较成熟的方案未 GZIP 压缩,正常情况下压缩率均值能打包 30-50之间,可以极大的提升传输速度和节省流量,必要是可以使用 Protocol Buffer 替换 JSON 。

弱网优化

弱网优化,在弱网时要是制定合适的超时时间,控制网络并发,合并打包请求,调优TCP参数,使用TCP优化算法。
对服务端的TCP协议参数进行调优,以及开启各种优化算法,使得适合业务特性和移动端网络环境,包括RTO初始值,混合慢启动,TLP,F-RTO等。
针对弱网的这些细致优化暂未成为标准,开源网络库 mars 有实现可以借鉴,若有需要可以使用。

网络安全

1.采用 Https 双向认证
2.参数加密校验,防窃听/篡改或冒充
3.加密秘钥保护,采用 so 方式进行秘钥提取

View绘制主要要讲什么

View的绘制流程是程序开发的必备知识,也是面试经常会问到知识

  • View绘制如下图
    Alt text

简单回答就是 measure,layout,draw 分别对应测量,布局,绘制三个过程。
涉及到更深的知识就会引申到Handler,同步屏障,View 的事件传递,甚至 activity 的启动过程。

什么是 ViewRootImpl

相比 Viewgroup 和 View,ViewRootImpl 可能更为陌生,实际开发中我们基本用不到它。那么

从结构上来看,ViewRootImpl 和 ViewGroup 其实是一种东西

1
2
3
4
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {}
public abstract class ViewGroup extends View
implements ViewParent, ViewManager {}

它们都继承了 ViewParent。ViewParent 是一个接口,定义了一些父 View 的基本行为,比如 requestlayout,getparent 等。不同的是,ViewRootImpl 并不会像 ViewGroup 一样被真正绘制在屏幕上。在 activity 中,它是专门用来绘制 DecorView 的,核心方法是 setView

  • “activity,window,View 三者之间的关系是什么?”

Alt text

如图所示,window 是 activity 里的一个实例变量,本质是一个接口,唯一的实现类是 PhoneWindow。

activity 的 setContentView 方法实际上是就是交给 phonewindow 去做的。window 和 View 的关系可以类比为显示器和显示的内容。

每个 activity 都有一个“显示器” window,“显示的内容”就是 DecorView。这个“显示器”定义了一些方法来决定如何显示内容。比如 setTitleColor setTitle 是设置导航栏的颜色和 title , setAllowReturnTransitionOverlap 设置进/出场动画等等。

所以 window 是 activity 的一个成员变量,window 和 View 是“显示器”和“显示内容”的关系。

这就是他们的关系

在整个 activity 的生命周期中,setContentView 是在 onCreate 中调用的,它实现了对资源文件的解析,完成了 xml 文件到 View 的转化。那么 View 真正开始绘制是在哪个生命周期呢?

  • 答案是 onResume 结束后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;

// 1. 执行Activity的onResume方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
if (r == null) {
// We didn't actually resume the activity, so skipping any follow-up actions.
return;
}
if (mActivitiesToBeDestroyed.containsKey(token)) {
// Although the activity is resumed, it is going to be destroyed. So the following
// UI operations are unnecessary and also prevents exception because its token may
// be gone that window manager cannot recognize it. All necessary cleanup actions
// performed below will be done while handling destruction.
return;
}

final Activity a = r.activity;

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

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

// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
// 2. 获取window
r.window = r.activity.getWindow();
// 3. 获取DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
// 4.获取Activity的WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 5.调用WindowManager的addView方法
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}

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

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

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

r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}

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


public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}

ViewRootImpl root;
View panelParentView = null;

synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}

int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}

// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
// 6.调用viewRootImpl的setView方法
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}

从源码中可以看到,onResume 之后,ActivityThread 通过调用 activity 中 windowmanager 的 addView 方法,将 decorView 传入到 ViewRootImpl 的 setView 方法中,通过 setView 来完成 View 的绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//接收的参数ViewRootImpl#setView 这里入参是DecorView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
....
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
//检查绘制的的线程是不是创建View的线程 绘制View
requestLayout();
....




@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//线程检查
checkThread();
mLayoutRequested = true;
//开始绘制
scheduleTraversals();
}
}


@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//获取内存屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//执行绘制任务 mTraversalBarrier
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除内存屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//开始绘制 依次调用performMeasure performLayout performDraw
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
  • 简单来说 setView 做了三件事
  1. 检查绘制的线程是不是创建 View 的线程。这里可以引申出一个问题,View 的绘制必须在主线程吗?

  2. 通过内存屏障保证绘制 View 的任务是最优先的

  3. 调用 performTraversals 完成 measure,layout,draw 的绘制

看到这里,ViewRootImpl 的绘制基本就完成了。其实这也是面试官希望听到的内容。考察的是面试者对 View 绘制体系的理解。

后续 ViewGroup 和 View 的绘制其实是 performTraversals 对整个 ViewTree 的绘制。他们的关系可以用下面这张图表示

Alt text

那为什么我在 onCreate 中调用 View.post 方法可以得到 View 的宽高呢?

1
2
3
4
5
6
7
8
9
10
11
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}

// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

View.post 会判断当前 View 是否已经被添加到 window 上。如果添加了则立即执行 runnable,如果没有被添加则先放到一个队列中存储起来,等添加到 window 上时再执行。

而 View 被测量完成后才会 attachToWindow。所以当 post 的 runnable 执行时,View 已经绘制完成了。

MeasureSpec 的理解

  • 说说什么是 MeasureSpec?为什么测量宽高要用它作为参数呢?

View 的大小不仅仅取决于自身的宽高,还取决于父 View 的大小和测量模式。一个 200200 的父 View 是不可能容纳一个 300300 的子 View 的,父 View 的 wrap_content 和 match_content 也会影响子 View 的大小。

所以 View 的 measure 函数其实应该有 4 个参数:父 View 的宽,父 View 的高,宽的测量模式,高的测量模式。

Android 这里用了一个巧妙的设计,用一个 Int 值来表示宽/高的测量模式和大小。一个 int 有 32 位,前 2 位表示测量 MODE,后 30 位表示 SIZE。

为什么要用 2 位表示 MODE 呢?因为 MODE 只有 3 种呀,UNSPECIFIED,EXACTLY,AT_MOST 。

  • 使用这个 View 时宽高传入 wrap_content,结果会怎么样?

当我们自定义一个 View 时,如果继承的是 View,measure 方法走的就是 View 默认的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

所以当我们自定义 View 时,如果没有对 MODE 做处理,设置 wrap_content 和 match_content 结果其实是一样的,View 的宽高都是取父 View 的宽高。

  • invaliate 和 requestlayout 方法的区别?

ViewRootImpl 作为顶级 View 负责 View 的绘制。所以简单来说,requestlayout 和 invaliate 最终都会向上回溯调用到 ViewRootImpl 的 postTranversals 方法来绘制 View。

不同的是 requestlayout 会绘制 View 的 measure,layout 和 draw 过程。invaliate 因为只添加了绘制 draw 的标志位,只会绘制 draw 过程。

  • 实现一下 findViewbyid 的过程?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
View findViewById(int id){
if(mId == id)return this;
if(mVIew instanceof ViewGroup){
int childrenCount = getChildrenCount();
for(int i = 0; i<childrenCount;i++){
View v = getchildAt(i){
v = v.findViewById(id);
if(v !=null){
return v;
}
}
}
}
return null;
}

粘性事件的EventBus

下午赶去公司解决了电台业务首次语音搜台后(用到服务,但只出一个独立的Activity,主界面并没有打开)不能听歌识曲的问题.

排查到最后,去识别的消息确实是发出去了,但是却没有收到,没有收到消息当然不会响应.最后,消息是通过EventBus.getDefault.post(xx)发出的,一定是发送和接收出现问题.

推测该问题是由于主界面还未创建,用于接收的EventBus还未注册,即发布者发了消息,但订阅者还未产生(一般消息的处理逻辑是先注册订阅,后接收),这样没有收到消息当然无法响应操作.

了解到,EventBus是支持发送黏性事件的。

粘性事件?

何为黏性事件呢?简单讲,就是在发送事件之后再订阅该事件也能收到该事件。Android中就有这样的实例,也就是Sticky Broadcast,即粘性广播。正常情况下如果发送者发送了某个广播,而接收者在这个广播发送后才注册自己的Receiver,这时接收者便无法接收到 刚才的广播,为此Android引入了StickyBroadcast,在广播发送结束后会保存刚刚发送的广播(Intent),这样当接收者注册完 Receiver后就可以接收到刚才已经发布的广播。这就使得我们可以预先处理一些事件,让有消费者时再把这些事件投递给消费者.

EventBus也提供了这样的功能,有所不同是EventBus会存储所有的Sticky事件,如果某个事件在不需要再存储则需要手动进行移除。用户通过Sticky的形式发布事件,而消费者也需要通过Sticky的形式进行注册,当然这种注册除了可以接收 Sticky事件之外和常规的注册功能是一样的,其他类型的事件也会被正常处理。

基本使用

发布和接收粘性事件一般有如下几步:

1、粘性事件的发布:

1
EventBus.getDefault().postSticky("nearby");
1
2
3
4
5
6
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveSoundRecongnizedmsg(String insType) {
if ("nearby".equals(insType)) {
soundRecognizeCtrl();
}
}

剩下的操作就和普通事件一样注册和反注册即可.

手动获取和移除粘性事件(Getting and Removing sticky Events manually)

正如你之前看到的,最近发布的粘性事件在其新订阅者注册后将会自动传递给新订阅者。但有时可能更方便手动检查粘性事件。有时我们也需要移除粘性事件,以免它在传递下去。

1
2
3
4
5
MessageEvent stickyEvent = EventBus.getDefault().getStickyEvent(MessageEvent.class);
if(stickyEvent != null) {
EventBus.getDefault().removeStickyEvent(stickyEvent);
  //TODO
}

removeStickyEvent 会返回之前持有的粘性事件。

于是,

1
2
3
4
MessageEvent stickyEvent = EventBus.getDefault().removeStickyEvent(MessageEvent.class);
if(stickyEvent != null) {
  //TODO
}

使用场景

我们要把一个Event发送到一个还没有初始化的Activity/Fragment,即尚未订阅事件。那么如果只是简单的post一个事件,那么是无法收到的,这时候,你需要用到粘性事件,它可以帮你解决这类问题.

小结

对于EventBus3.0来说,我还只是知道如何简单的使用,是知其然,不知其所以然,它是一个比较强大的事件总线库,后续会看下源码,慢慢分析一下,消息是如何发送和接收的.

Android 查看手机设备CPU架构信息

手机要是root的,连上数据线
打开终端输入

1
2
3
adb shell
##cpu信息在/proc/cpuinfo文件中
cat /proc/cpuinfo

信息如下,
Processor表示aarch64架构
processor表示第几个核,0表示第1个核,1表示第二个核,以此类推。
Features表示cpu支持的指令集,asimd 就是neon
CPU architecture表示arm架构,7表示arm-v7架构,8表示arm-v8架构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
applewangdeMacBook-Pro:nearby lr$ adb shell
bullhead:/ $ cat /proc/cpuinfo
Processor : AArch64 Processor rev 12 (aarch64)
processor : 0
BogoMIPS : 38.40
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x51
CPU architecture: 8
CPU variant : 0x7
CPU part : 0x803
CPU revision : 12

processor : 1
BogoMIPS : 38.40
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x51
CPU architecture: 8
CPU variant : 0x7
CPU part : 0x803
CPU revision : 12

processor : 2
BogoMIPS : 38.40
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x51
CPU architecture: 8
CPU variant : 0x7
CPU part : 0x803
CPU revision : 12

processor : 3
BogoMIPS : 38.40
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x51
CPU architecture: 8
CPU variant : 0x7
CPU part : 0x803
CPU revision : 12

processor : 4
BogoMIPS : 38.40
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x51
CPU architecture: 8
CPU variant : 0x6
CPU part : 0x802
CPU revision : 13

processor : 5
BogoMIPS : 38.40
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x51
CPU architecture: 8
CPU variant : 0x6
CPU part : 0x802
CPU revision : 13
...
...
...

Charles如何设置本地映射和取消本地映射

适用情况:

遇到接口返回特别快的时候非常郁闷,还得求着接口开发改数据很浪费时间,于是在这种情况下可以使用charles做本地映射。

环境:

charles版本:3.11.2

一:如何设置本地映射

1、选中你要进行断点的接口,右键选择Save Respons,把文件保存到本地

2,保存格式是bat

3、返回到接口处,再点右键,选择Map Local,映射到本地你保存的文件上面

4、选择保存成bat格式的文件

5、改变你要改变的参数,保存成功后,再进入时就是按照你改变的参数进行数据请求

二:如何取消本地映射

1、点击tools->Map local

2、把红色指向位置对勾去掉就可以了