LinearLayout是安卓开发中最常用布局之一,此文依托分析其源码,了解其性能特色,目标使以后开发使用它更得心应手
/**
* LinearLayout是安卓开发中最常用布局之一,此文依托分析其源码,了解其性能特色,目标使以后开发使用它更得心应手
* 重点查看onMeasure()
* 默认方向是横向
*/
public class LinearLayout extends ViewGroup {
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
//divider的几种类型
public static final int SHOW_DIVIDER_NONE = 0;
public static final int SHOW_DIVIDER_BEGINNING = 1;
public static final int SHOW_DIVIDER_MIDDLE = 2;
public static final int SHOW_DIVIDER_END = 4;
private boolean mBaselineAligned = true;//默认是 子view按Baseline对齐
//默认为0,当前LinearLayout作为其他layout的子View时,以该LinearLayout中哪个TextView(Butt//on、EditView)的基准线为对外基准线
private int mBaselineAlignedChildIndex = -1;
private int mBaselineChildTop = 0;
private int mOrientation;
private int mGravity = Gravity.START | Gravity.TOP;
private int mTotalLength;//测量的所有子view总高度
private float mWeightSum;//权重总和
//是否用最大个的子view,如果用的话,所有可见的子view都跟这个一样大
private boolean mUseLargestChild;
private Drawable mDivider;//分割线的Drawable,会在重写的onDraw()中绘制
private int mDividerWidth;
private int mDividerHeight;
private int mShowDividers;//对应上面divider的几种类型
private int mDividerPadding;
private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;//RTL相关
//最终调用的构造方法
public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//构造方法中 各个参数的初始化
final TypedArray a = ...
}
public void setShowDividers(@DividerMode int showDividers) {
if (showDividers != mShowDividers) {//如果这个类型发生改变
requestLayout();//重新布局
}
mShowDividers = showDividers;
}
public void setDividerDrawable(Drawable divider) {
if (divider == mDivider) {
return;
}
mDivider = divider;
if (divider != null) {
mDividerWidth = divider.getIntrinsicWidth();//获取DividerWidth自己的宽高
mDividerHeight = divider.getIntrinsicHeight();
} else {
mDividerWidth = 0;
mDividerHeight = 0;
}
setWillNotDraw(divider == null);//如果没有divider就添加一个不需要draw的flag
requestLayout();
}
public void setDividerPadding(int padding) {
mDividerPadding = padding;
}
//此方法会根据自身布局方向,分开绘制,内部逻辑类似
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {//默认divider是宽高为0的
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);//竖向布局绘制divider
} else {
drawDividersHorizontal(canvas);//横向~
}
}
//竖向布局绘制divider
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();//获取子的个数
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {//此子view可见且在其前有divider
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;//计算得到divider的top位置
drawHorizontalDivider(canvas, top);//draw横向的divider(整体布局是竖向)
}
}
}
if (hasDividerBeforeChildAt(count)) {//计算是否需要在尾部绘制divider
final View child = getLastNonGoneChild();//最后一个没有gone的子view
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;//高度-底部内边距-divider高度
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;//最后一个view到父布局顶部距离加上其到底部的外边距
}
drawHorizontalDivider(canvas, bottom);//普通的通过计算Rect 绘制divider
}
}
//最后一个没有gone的子view
private View getLastNonGoneChild() {
for (int i = getVirtualChildCount() - 1; i >= 0; i--) {
View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
return child;
}
}
return null;
}
//横向布局绘制divider,大部分类似于竖向布局
void drawDividersHorizontal(Canvas canvas) {
final int count = getVirtualChildCount();
final boolean isLayoutRtl = isLayoutRtl();//判断是否为RTL布局
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int position;
if (isLayoutRtl) {
position = child.getRight() + lp.rightMargin;
} else {
position = child.getLeft() - lp.leftMargin - mDividerWidth;
}
drawVerticalDivider(canvas, position);//绘制竖向的divider
}
}
}
if (hasDividerBeforeChildAt(count)) {
final View child = getLastNonGoneChild();
int position;
if (child == null) {
if (isLayoutRtl) {
position = getPaddingLeft();
} else {
position = getWidth() - getPaddingRight() - mDividerWidth;
}
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (isLayoutRtl) {
position = child.getLeft() - lp.leftMargin - mDividerWidth;
} else {
position = child.getRight() + lp.rightMargin;
}
}
drawVerticalDivider(canvas, position);
}
}
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}
void drawVerticalDivider(Canvas canvas, int left) {
mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
mDivider.draw(canvas);
}
public boolean isMeasureWithLargestChildEnabled() {//是不是依赖最大的字view
return mUseLargestChild;
}
@Override
public int getBaseline() {//获取ll对外的baseline
...
}
View getVirtualChildAt(int index) {
return getChildAt(index);
}
int getVirtualChildCount() {
return getChildCount();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
protected boolean hasDividerBeforeChildAt(int childIndex) {
if (childIndex == getVirtualChildCount()) {
// 位运算符计算是不是需要在尾部drawDivider
return (mShowDividers & SHOW_DIVIDER_END) != 0;
}
boolean allViewsAreGoneBefore = allViewsAreGoneBefore(childIndex);//判断index前面所有view是不是皆为GONE
if (allViewsAreGoneBefore) {
// 这是第一个没有gone掉的子view,且是SHOW_DIVIDER_BEGINNING(invisible时依然会显示)
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
} else {// 非第一个子view,且是SHOW_DIVIDER_MIDDLE
return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0;
}
}
/**
* Checks whether all (virtual) child views before the given index are gone.
*/
private boolean allViewsAreGoneBefore(int childIndex) {
for (int i = childIndex - 1; i >= 0; i--) {
View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
return false;
}
}
return true;
}
/**
* @param widthMeasureSpec 父控件传进来的width的要求
* @param heightMeasureSpec
* 分析可以得出:
* 如果是一个非权重,非useLargest的LinearLayout,只走第一层for循环(测量一次就够)
* 一个但useLargestChild,高度没定死的LinearLayout会for两次
* 一个使用权重的布局会for三次
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;//记录最大宽度
int childState = 0;/经过所有子viewmeasure后的state状态
int alternativeMaxWidth = 0;//权重布局下计算的MaxWidth
int weightedMaxWidth = 0;//临时存的每个子view的权重最大宽度
boolean allFillParent = true;//子view全部是MATCH_PARENT
float totalWeight = 0;//全部权重
final int count = getVirtualChildCount();//总数
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);//宽度模式
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);//高度模式
boolean matchWidth = false;//至少有一个子view需要测量宽度(为MATCH_PARENT)
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {//一层for循环
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {//如果该子view前有divider
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;//该子view权重
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {//确切高度,且height=0 权重>0
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;//后面跳过Measure
} else {//高度不是确定可能是AT_MOST/UNSPECIFIED
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);//调用viewgroup中方法测量子view
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +//此子view高度和外边距加到总高度中
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);//记录最大子view高度
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;//记录baseline位置
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;//该子view是否需要测量宽度
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());//获取子viewmeasure后的state状态
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);//这有点看不懂,为何MATCH_PARENT时要用margin
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}//for循环结束
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {//尾部divider高度也加上
mTotalLength += mDividerHeight;
}
if (useLargestChild &&//如果是用最大的子view乘以个数
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {//高度没限定
mTotalLength = 0;//计算的都是废物,清零吧
//重新计算总高度:每个非gone的view的高度都按 上次循环记录的最大子view的高度计算,再加上margin
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// 再加上自己的上下内边距
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 再跟自己的minheight对比下
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
//resolveSizeAndState除了返回最终尺寸信息还会有可能返回量算的state标志位信息。
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
//如果是权重,前面的又计算的都是废物,清零吧
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);//给他分享点多出来的高度
weightSum -= childExtra;//减掉分享出去的
delta -= share;//减掉分享出去的
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
//将重新修整好的高度传到子view的measure中
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth, weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(//调用child的measure方法
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;//最大宽度用记录到的最大宽度替代
}
maxWidth += mPaddingLeft + mPaddingRight;//加上自己的左右内边距
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());//再跟自己的最小宽度对比
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);//最终,设置宽高给自己了
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
private void forceUniformWidth(int count, int heightMeasureSpec) {
// Pretend that the linear layout has an exact size.
int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
MeasureSpec.EXACTLY);//自己的宽度
for (int i = 0; i< count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() != GONE) {
LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
if (lp.width == LayoutParams.MATCH_PARENT) {
// Temporarily force children to reuse their old measured height
// FIXME: this may not be right for something like wrapping text?
int oldHeight = lp.height;
lp.height = child.getMeasuredHeight();
// Remeasue with new dimensions
measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);//viewgroup的方法
lp.height = oldHeight;
}
}
}
}//measure结束了
void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
...
}
int getChildrenSkipCount(View child, int index) {
return 0;
}
int measureNullChild(int childIndex) {
return 0;
}
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
int getLocationOffset(View child) {
return 0;
}
int getNextLocationOffset(View child) {
return 0;
}
//也根据自身布局方向分别layout()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;//左内边距
int childTop;//每次临时记录的子view的top
int childLeft;
// Where right end of child should go
final int width = right - left;//自己的宽度
int childRight = width - mPaddingRight;//子view的右边
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;//子view宽度
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;//初始化第一个childTop的值,上内边距+高度-measure后高度
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);//0
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();//测量宽高
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();//方向:RTL相关
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {//解析横向的位置
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;//这个子view前的divider高度要加进去
}
childTop += lp.topMargin;//再加上其上外边距
setChildFrame(child, childLeft, childTop + getLocationOffset(child),//调用子view的layout()方法
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);//再加上自己的下内边距 去下次循环
i += getChildrenSkipCount(child, i);
}
}
}
@Override
public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
if (layoutDirection != mLayoutDirection) {
mLayoutDirection = layoutDirection;
if (mOrientation == HORIZONTAL) {
requestLayout();
}
}
}
void layoutHorizontal(int left, int top, int right, int bottom) {
...
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
//(MarginLayoutParams extends ViewGroup.LayoutParams)
//ViewGroup.LayoutParams中是height和weight
//MarginLayoutParams中是上下左右的一个margin值
//此处的LayoutParams拓展了weight 和gravity属性
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public float weight;
public int gravity = -1;
}
...
}
}
复制代码
zyt