https://blog.csdn.net/fancylovejava/article/details/45787729

https://blog.csdn.net/dunqiangjiaodemogu/article/details/72956291

飞猪上的doraemon一直对过度绘制和布局深度有监控,不合理的布局和过深得过度绘制影响页面渲染速度。虽然发现了不少问题,多处可见如下图的红红的页面,但是一直很难推动解决,主要有两个原因。

  1. 让开发找到具体的位置需要从根布局一层层遍历分析下去,确实麻烦,所以不想改
  2. 修改后,会不会影响到其他控件的显示效果,光靠大脑想象难保万全,所以不敢改

新工具

感谢@睿牧提供的外部开源参考工具
于是doraemon里就多了一样新的工具,将当前页面的布局3D化,有点像xcode上的view ui hierarchy工具,如下图所示。新工具可以辅助分析页面布局的合理性和影响过度绘制的关键点:

  1. 在3D化的页面上将每个有名字的控件的名字(id)都写上了,便于直接看出是哪个控件(或者控件的爸爸)导致问题,以便快速定位到具体的控件;
  2. 在3D化的页面上通过拖拽和多点触摸放大来直观的看出每一个控件在整体布局里所处的位置,和对相关控件的影响,便于下结论能不能改;
  3. 在开发写布局文件时,经常用到layout嵌套layout,所以没有一个全局观,即不知当前正在写的布局在整体里的位置。在3D化的页面上,能够清晰的看出布局是否合理,是否有不合理的嵌套布局存在。不合理的布局导致过深得嵌套会引发crash

分析方法(这里以门票首页为例)

1. 打开过度绘制开关

2. 将门票首页布局3D化

按照上面的打开方式,然后进入门票首页,再点击“3D”Icon,可以看到如下图。可以看到所有控件的背景色都被涂上了标识过度绘制深度的颜色。

3. 找出影响过渡绘制的关键控件

从最外层布局向内看,导致背景色突变的控件是设置了背景色,如下图标记。其中5和6的背景色变化是因为加载了图片,这种情况可以不修改。我们主要看下1、2、3、4这4个地方。

1. 标记1位置

如下代码,在根布局里刷了一次全屏的白色。不合理性:标题栏本身也是白色,所在标题栏的像素区域内,根布局的全屏白色是多余的。

2. 标记2位置

整个页面的布局可以看成是上面一个标题栏,下面一个列表控件(listview),代码中为这个列表控件再一次刷了白色,如下代码所示:

3. 标记3位置

list的cell单元代码中再次刷了个白色底色,很显然这是多余的

4. 标记4位置

又一个list的cell单元这里也刷了个白色底色,很显然这也是多余的,前面的e6透明度更是多此一举。

4. 找到了痛点位置给出解决方案

  1. 去掉根布局的白色底色,保留listview的白色底色
  2. 去掉listview中的cell的白色底色

5. 初步优化前后对比

过度绘制数值由原先的4.04降低到2.63,提升53.6%。下图是初步优化前后颜色对比。

6. 布局合理性分析

如下黄色箭头指向的位置,4个图片控件(ImageView)并排放着,用了3层布局,对此表示质疑。

最后

3D布局工具结合过渡绘制开关可以有效地提升定位过度绘制问题,此外还容易发现多余的不合理的布局,以提升native的性能体验。

下面是源码,欢迎讨论共建。

public class ScalpelFrameLayout extends FrameLayout {

    /**
* 传入当前顶部的Activity
*/
public static void attachActivityTo3dView(Activity activity) {
if (activity == null) {
return;
} ScalpelFrameLayout layout;
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
/**
* 在ids.xml里定义一个id
*/
if (decorView.findViewById(R.id.id_scalpel_frame_layout) == null) {
layout = new ScalpelFrameLayout(activity);
layout.setId(R.id.id_scalpel_frame_layout);
View rootView = decorView.getChildAt(0);
decorView.removeView(rootView);
layout.addView(rootView);
decorView.addView(layout);
} else {
layout = (ScalpelFrameLayout) decorView.findViewById(R.id.id_scalpel_frame_layout);
} if (!layout.isLayerInteractionEnabled()) {
layout.setLayerInteractionEnabled(true);
} else {
layout.setLayerInteractionEnabled(false);
}
} /**
* 标记位:当前多点触摸滑动方向未确定
*/
private final static int TRACKING_UNKNOWN = 0;
/**
* 标记位:当前多点触摸滑动方向是垂直方向
*/
private final static int TRACKING_VERTICALLY = 1;
/**
* 标记位:当前多点触摸滑动方向是横向方向
*/
private final static int TRACKING_HORIZONTALLY = -1;
/**
* 旋转的最大角度
*/
private final static int ROTATION_MAX = 60;
/**
* 反方向旋转的最大角度
*/
private final static int ROTATION_MIN = -ROTATION_MAX;
/**
* 默认X轴旋转角度
*/
private final static int ROTATION_DEFAULT_X = -10;
/**
* 默认Y轴旋转角度
*/
private final static int ROTATION_DEFAULT_Y = 15;
/**
* 默认缩放比例
*/
private final static float ZOOM_DEFAULT = 0.6f;
/**
* 最小缩放比例
*/
private final static float ZOOM_MIN = 0.33f;
/**
* 最大缩放比例
*/
private final static float ZOOM_MAX = 2f;
/**
* 图层默认间距
*/
private final static int SPACING_DEFAULT = 25;
/**
* 图层间最小距离
*/
private final static int SPACING_MIN = 10;
/**
* 图层间最大距离
*/
private final static int SPACING_MAX = 100;
/**
* 绘制id的文案的偏移量
*/
private final static int TEXT_OFFSET_DP = 2;
/**
* 绘制id的文案的字体大小
*/
private final static int TEXT_SIZE_DP = 10;
/**
* view缓存队列初始size
*/
private final static int CHILD_COUNT_ESTIMATION = 25;
/**
* 是否绘制view的内容,如TextView上的文字和ImageView上的图片
*/
private boolean mIsDrawingViews = true;
/**
* 是否绘制view的id
*/
private boolean mIsDrawIds = true;
/**
* 打印debug log开关
*/
private boolean mIsDebug = true;
/**
* view大小矩形
*/
private Rect mViewBoundsRect = new Rect();
/**
* 绘制view边框和id
*/
private Paint mViewBorderPaint = new Paint(ANTI_ALIAS_FLAG);
private Camera mCamera = new Camera();
private Matrix mMatrix = new Matrix();
private int[] mLocation = new int[2];
/**
* 用来记录可见view
* 可见view需要绘制
*/
private BitSet mVisibilities = new BitSet(CHILD_COUNT_ESTIMATION);
/**
* 对id转字符串的缓存
*/
private SparseArray<String> mIdNames = new SparseArray<String>();
/**
* 队列结构实现广度优先遍历
*/
private ArrayDeque<LayeredView> mLayeredViewQueue = new ArrayDeque<LayeredView>();
/**
* 复用LayeredView
*/
private Pool<LayeredView> mLayeredViewPool = new Pool<LayeredView>(
CHILD_COUNT_ESTIMATION) { @Override
protected LayeredView newObject() {
return new LayeredView();
}
};
/**
* 屏幕像素密度
*/
private float mDensity = 0f;
/**
* 对移动最小距离的合法性的判断
*/
private float mSlop = 0f;
/**
* 绘制view id的偏移量
*/
private float mTextOffset = 0f;
/**
* 绘制view id字体大小
*/
private float mTextSize = 0f;
/**
* 3D视图功能是否开启
*/
private boolean mIsLayerInteractionEnabled = false;
/**
* 第一个触摸点索引
*/
private int mPointerOne = INVALID_POINTER_ID;
/**
* 第一个触摸点的坐标X
*/
private float mLastOneX = 0f;
/**
* 第一个触摸点的坐标Y
*/
private float mLastOneY = 0f;
/**
* 当有多点触摸时的第二个触摸点索引
*/
private int mPointerTwo = INVALID_POINTER_ID;
/**
* 第二个触摸点的坐标X
*/
private float mLastTwoX = 0f;
/**
* 第二个触摸点的坐标Y
*/
private float mLastTwoY = 0f;
/**
* 当前多点触摸滑动方向
*/
private int mMultiTouchTracking = TRACKING_UNKNOWN;
/**
* Y轴旋转角度
*/
private float mRotationY = ROTATION_DEFAULT_Y;
/**
* X轴旋转角度
*/
private float mRotationX = ROTATION_DEFAULT_X;
/**
* 缩放比例,默认是0.6
*/
private float mZoom = ZOOM_DEFAULT;
/**
* 图层之间距离,默认是25单位
*/
private float mSpacing = SPACING_DEFAULT; public ScalpelFrameLayout(Context context) {
super(context, null, 0);
mDensity = getResources().getDisplayMetrics().density;
mSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
mTextSize = TEXT_SIZE_DP * mDensity;
mTextOffset = TEXT_OFFSET_DP * mDensity;
mViewBorderPaint.setStyle(STROKE);
mViewBorderPaint.setTextSize(mTextSize);
if (Build.VERSION.SDK_INT >= JELLY_BEAN) {
mViewBorderPaint.setTypeface(Typeface.create("sans-serif-condensed", NORMAL));
}
} /**
* 设置是否让当前页面布局3D化
* 使用该方法前先调用attachActivityTo3dView方法
*
* @param enabled
*/
public void setLayerInteractionEnabled(boolean enabled) {
if (mIsLayerInteractionEnabled != enabled) {
mIsLayerInteractionEnabled = enabled;
setWillNotDraw(!enabled);
invalidate();
}
} /**
* 当前页面布局是否已经3D化
*
* @return
*/
public boolean isLayerInteractionEnabled() {
return mIsLayerInteractionEnabled;
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mIsLayerInteractionEnabled || super.onInterceptTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsLayerInteractionEnabled) {
return super.onTouchEvent(event);
} int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
int index = action == ACTION_DOWN ? 0 : event.getActionIndex();
if (mPointerOne == INVALID_POINTER_ID) {
mPointerOne = event.getPointerId(index);
mLastOneX = event.getX(index);
mLastOneY = event.getY(index);
if (mIsDebug) {
log("Got pointer 1! id: %s x: %s y: %s", mPointerOne, mLastOneY, mLastOneY);
}
} else if (mPointerTwo == INVALID_POINTER_ID) {
mPointerTwo = event.getPointerId(index);
mLastTwoX = event.getX(index);
mLastTwoY = event.getY(index);
if (mIsDebug) {
log("Got pointer 2! id: %s x: %s y: %s", mPointerTwo, mLastTwoY, mLastTwoY);
}
} else {
if (mIsDebug) {
log("Ignoring additional pointer. id: %s", event.getPointerId(index));
}
} break;
case MotionEvent.ACTION_MOVE:
if (mPointerTwo == INVALID_POINTER_ID) {
/**
* 单触点滑动是控制3D布局的旋转角度
*/
int i = 0;
int count = event.getPointerCount();
while (i < count) {
if (mPointerOne == event.getPointerId(i)) {
float eventX = event.getX(i);
float eventY = event.getY(i);
float dx = eventX - mLastOneX;
float dy = eventY - mLastOneY;
float drx = 90 * (dx / getWidth());
float dry = 90 * (-dy / getHeight());
/**
* 屏幕上X的位移影响的是坐标系里Y轴的偏移角度,屏幕上Y的位移影响的是坐标系里X轴的偏移角度
* 根据实际位移结合前面定义的旋转角度区间算出应该旋转的角度
*/
mRotationY = Math.min(Math.max(mRotationY + drx, ROTATION_MIN),
ROTATION_MAX);
mRotationX = Math.min(Math.max(mRotationX + dry, ROTATION_MIN),
ROTATION_MAX);
if (mIsDebug) {
log("Single pointer moved (%s, %s) affecting rotation (%s, %s).",
dx, dy, drx, dry);
} mLastOneX = eventX;
mLastOneY = eventY;
invalidate();
} i++;
}
} else {
/**
* 多触点滑动是控制布局的缩放和图层间距
*/
int pointerOneIndex = event.findPointerIndex(mPointerOne);
int pointerTwoIndex = event.findPointerIndex(mPointerTwo);
float xOne = event.getX(pointerOneIndex);
float yOne = event.getY(pointerOneIndex);
float xTwo = event.getX(pointerTwoIndex);
float yTwo = event.getY(pointerTwoIndex);
float dxOne = xOne - mLastOneX;
float dyOne = yOne - mLastOneY;
float dxTwo = xTwo - mLastTwoX;
float dyTwo = yTwo - mLastTwoY;
/**
* 首先判断是垂直滑动还是横向滑动
*/
if (mMultiTouchTracking == TRACKING_UNKNOWN) {
float adx = Math.abs(dxOne) + Math.abs(dxTwo);
float ady = Math.abs(dyOne) + Math.abs(dyTwo);
if (adx > mSlop * 2 || ady > mSlop * 2) {
if (adx > ady) {
mMultiTouchTracking = TRACKING_HORIZONTALLY;
} else {
mMultiTouchTracking = TRACKING_VERTICALLY;
}
}
} /**
* 如果是垂直滑动调整缩放比
* 如果是横向滑动调整层之间的距离
*/
if (mMultiTouchTracking == TRACKING_VERTICALLY) {
if (yOne >= yTwo) {
mZoom += dyOne / getHeight() - dyTwo / getHeight();
} else {
mZoom += dyTwo / getHeight() - dyOne / getHeight();
} /**
* 算出调整后的缩放比例
*/
mZoom = Math.min(Math.max(mZoom, ZOOM_MIN), ZOOM_MAX);
invalidate();
} else if (mMultiTouchTracking == TRACKING_HORIZONTALLY) {
if (xOne >= xTwo) {
mSpacing += (dxOne / getWidth() * SPACING_MAX)
- (dxTwo / getWidth() * SPACING_MAX);
} else {
mSpacing += (dxTwo / getWidth() * SPACING_MAX)
- (dxOne / getWidth() * SPACING_MAX);
} /**
* 算出调整后的图层间距
*/
mSpacing = Math.min(Math.max(mSpacing, SPACING_MIN), SPACING_MAX);
invalidate();
} if (mMultiTouchTracking != TRACKING_UNKNOWN) {
mLastOneX = xOne;
mLastOneY = yOne;
mLastTwoX = xTwo;
mLastTwoY = yTwo;
}
} break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
index = action != ACTION_POINTER_UP ? 0 : event.getActionIndex();
int pointerId = event.getPointerId(index);
if (mPointerOne == pointerId) {
/**
* 多触点状态切换到单触点状态
* 即如果原先是调整缩放和图层间距的状态,放开一个手指后转为控制图层旋转状态
*/
mPointerOne = mPointerTwo;
mLastOneX = mLastTwoX;
mLastOneY = mLastTwoY;
if (mIsDebug) {
log("Promoting pointer 2 (%s) to pointer 1.", mPointerTwo);
} /**
* reset多触点状态
*/
mPointerTwo = INVALID_POINTER_ID;
mMultiTouchTracking = TRACKING_UNKNOWN;
} else if (mPointerTwo == pointerId) {
if (mIsDebug) {
log("Lost pointer 2 (%s).", mPointerTwo);
} /**
* reset多触点状态
*/
mPointerTwo = INVALID_POINTER_ID;
mMultiTouchTracking = TRACKING_UNKNOWN;
} break;
default:
break;
} return true;
} @Override
public void draw(Canvas canvas) {
if (!mIsLayerInteractionEnabled) {
super.draw(canvas);
return;
} getLocationInWindow(mLocation);
/**
* 页面左上角坐标
*/
float x = mLocation[0];
float y = mLocation[1];
int saveCount = canvas.save();
/**
* 页面中心坐标
*/
float cx = getWidth() / 2f;
float cy = getHeight() / 2f;
mCamera.save();
/**
* 先旋转
*/
mCamera.rotate(mRotationX, mRotationY, 0F);
mCamera.getMatrix(mMatrix);
mCamera.restore();
mMatrix.preTranslate(-cx, -cy);
mMatrix.postTranslate(cx, cy);
canvas.concat(mMatrix);
/**
* 再缩放
*/
canvas.scale(mZoom, mZoom, cx, cy);
if (!mLayeredViewQueue.isEmpty()) {
throw new AssertionError("View queue is not empty.");
} {
int i = 0;
int count = getChildCount();
while (i < count) {
LayeredView layeredView = mLayeredViewPool.obtain();
layeredView.set(getChildAt(i), 0);
mLayeredViewQueue.add(layeredView);
i++;
}
} /**
* 广度优先进行遍历
*/
while (!mLayeredViewQueue.isEmpty()) {
LayeredView layeredView = mLayeredViewQueue.removeFirst();
View view = layeredView.mView;
int layer = layeredView.mLayer;
/**
* 在draw期间尽量避免对象的反复创建
* 回收LayeredView一会再复用
*/
layeredView.clear();
mLayeredViewPool.restore(layeredView);
/**
* 隐藏viewgroup内可见的view
*/
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
mVisibilities.clear();
int i = 0;
int count = viewGroup.getChildCount();
while (i < count) {
View child = viewGroup.getChildAt(i);
/**
* 将可见的view记录到mVisibilities中
*/
if (child.getVisibility() == VISIBLE) {
mVisibilities.set(i);
child.setVisibility(INVISIBLE);
} i++;
}
} int viewSaveCount = canvas.save();
/**
* 移动出图层的距离
*/
float translateShowX = mRotationY / ROTATION_MAX;
float translateShowY = mRotationX / ROTATION_MAX;
float tx = layer * mSpacing * mDensity * translateShowX;
float ty = layer * mSpacing * mDensity * translateShowY;
canvas.translate(tx, -ty);
/**
* 画view的边框
*/
view.getLocationInWindow(mLocation);
canvas.translate(mLocation[0] - x, mLocation[1] - y);
mViewBoundsRect.set(0, 0, view.getWidth(), view.getHeight());
canvas.drawRect(mViewBoundsRect, mViewBorderPaint); /**
* 画view的内容
*/
if (mIsDrawingViews) {
view.draw(canvas);
} /**
* 画view的id
*/
if (mIsDrawIds) {
int id = view.getId();
if (id != NO_ID) {
canvas.drawText(nameForId(id), mTextOffset, mTextSize, mViewBorderPaint);
}
} canvas.restoreToCount(viewSaveCount);
/**
* 把刚刚应该显示但又设置了不可见的view从队列里取出来,后面再绘制
*/
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int i = 0;
int count = viewGroup.getChildCount();
while (i < count) {
if (mVisibilities.get(i)) {
View child = viewGroup.getChildAt(i);
child.setVisibility(VISIBLE);
LayeredView childLayeredView = mLayeredViewPool.obtain();
childLayeredView.set(child, layer + 1);
mLayeredViewQueue.add(childLayeredView);
} i++;
}
}
} canvas.restoreToCount(saveCount);
} /**
* 根据id值反算出在布局文件中定义的id名字
*
* @param id
* @return
*/
private String nameForId(int id) {
String name = mIdNames.get(id);
if (name == null) {
try {
name = getResources().getResourceEntryName(id);
} catch (NotFoundException e) {
name = String.format("0x%8x", id);
} mIdNames.put(id, name);
} return name;
} private static void log(String message, Object... object) {
TLog.i("Scalpel", String.format(message, object));
} private static class LayeredView {
private View mView = null;
/**
* mView所处的层级
*/
private int mLayer = 0; void set(View view, int layer) {
mView = view;
mLayer = layer;
} void clear() {
mView = null;
mLayer = -1;
}
} private static abstract class Pool<T> {
private Deque<T> mPool; Pool(int initialSize) {
mPool = new ArrayDeque<T>(initialSize);
for (int i = 0; i < initialSize; i++) {
mPool.addLast(newObject());
}
} T obtain() {
return mPool.isEmpty() ? newObject() : mPool.removeLast();
} void restore(T instance) {
mPool.addLast(instance);
} protected abstract T newObject();
}
}

安卓动态分析工具【Android】3D布局分析工具的更多相关文章

  1. 教你使用Android SDK布局优化工具layoutopt

    创建好看的Android布局是个不小的挑战,当你花了数小时调整好它们适应多种设备后,你通常不想再重新调整,但笨重的嵌套布局效率往往非常低下,幸运的是,在Android SDK中有一个工具可以帮助你优化 ...

  2. Android布局分析工具HierarchyView的使用方法

    本文是从这里看到的:http://www.2cto.com/kf/201404/296960.html 如果我们想宏观的看看自己的布局,Android SDK中有一个工具HierarchyView.b ...

  3. Android动态逆向分析工具ZjDroid--脱壳神器

    项目地址:https://github.com/BaiduSecurityLabs/ZjDroid 前提条件: 1.Root手机一部 2.须要通过Xposed installer( http://dl ...

  4. Android 静态代码分析工具

    简评: 作者在文中提到的三个静态代码分析工具不是互相替代的关系,各有各的侧重点,如果有需要完全可以同时使用. 静态代码分析是指无需运行被测代码,仅通过分析或检查源程序的语法.结构.过程.接口等来检查程 ...

  5. Android手机流量分析工具介绍

    一.20 Best Android Hacking Apps And Tools Of 2018 首先罗列常见的Android手机hacking的工具 #1The Android Network Ha ...

  6. Android优化—— 内存分析工具 MAT 的使用

    1 内存泄漏的排查方法 Dalvik Debug Monitor Server (DDMS) 是 ADT插件的一部分,其中有两项功能可用于内存检查 : ·    heap 查看堆的分配情况 ·     ...

  7. [Android Memory] 内存分析工具 MAT 的使用

    转载自: http://blog.csdn.net/aaa2832/article/details/19419679 1 内存泄漏的排查方法 Dalvik Debug Monitor Server ( ...

  8. 网站运维工具使用iis日志分析工具分析iis日志(iis日志的配置)

    我们只能通过各种系统日志来分析网站的运行状况,对于部署在IIS上的网站来说,IIS日志提供了最有价值的信息,我们可以通过它来分析网站的响应情况,来判断网站是否有性能问题,或者存在哪些需要改进的地方 对 ...

  9. 测试工具-慢sql日志分析工具pt-query-digest

    pt-query-digest分析来自慢速日志文件,常规日志文件和二进制日志文件的MySQL查询.它还可以分析来自tcpdump的查询和MySQL协议数据. 开启慢日志 set global slow ...

随机推荐

  1. 【hdu 5632】Rikka with Array

    Description As we know, Rikka is poor at math. Yuta is worrying about this situation, so he gives Ri ...

  2. linux 远程 telnet

    linux 远程 telnet [ root@test1 ~]# yum install telnet* (安装的三个包  xinetd . telnet-server.telnet)   [ roo ...

  3. Notepad++ --v7.5.8 (64bit) 安装目录显示插件(Explorer)

    https://blog.csdn.net/qq_24153697/article/details/83036761 最近想自己做一个小项目,用Notepad做IDE,但是发现已安装的Notepad没 ...

  4. DensePose: Dense Human Pose Estimation In The Wild(理解)

    0 - 背景 Facebook AI Research(FAIR)开源了一项将2D的RGB图像的所有人体像素实时映射到3D模型的技术(DensePose).支持户外和穿着宽松衣服的对象识别,支持多人同 ...

  5. 关于stm32的数据类型

    常见的uint16_t.uint32_t.u8.u16等 定义在stm32f10x.h文件中,这个文件是定义相关数据类型和结构体的头文件.

  6. OpenCV3编程入门-读书笔记1

    一.OpenCV概述 1.OpenCV全程Open Source Computer Vision Library,即开源计算机视觉库.它是一个跨平台的开源计算机视觉库,可以运行在windows.lin ...

  7. 【原创】大叔经验分享(45)kibana添加index pattern卡住 返回403 Forbidden

    kibana添加index pattern卡住,通过浏览器查看请求返回状态为403 Forbidden,返回消息为: {"message":"blocked by: [F ...

  8. [C]关于函数指针参数的赋值

    问题 在有一次尝试用stat()函数获取文件属性的时候,发现如果直接声明一个指针,然后把这个指针作为参数传给函数,会导致函数执行失败,原代码: #include <sys/stat.h> ...

  9. 两个c语言结构体复制的问题

    以前一直以为结构体要通过memcpy来复制,现在才明白可直接用“=”复制 C语言中,结构体是一篇连续的内存空间,使用=赋值操作,底层用到的就是memcpy:如果结构体中有指针变量:操作后.两个指针指向 ...

  10. js中setTimeout() 时间参数为0

    当看到下面 这种setTimeout 设置为0 写法的时候一脸懵逼,完全没用过. var fuc = [1,2,3]; for(var i in fuc){ setTimeout(function() ...