Android Drawable 完全解析(一):Drawable 源码分析(中)

Android Drawable完全解析(一):Drawable源码分析(上)
Android Drawable完全解析(一):Drawable源码分析(中)
Android Drawable完全解析(一):Drawable源码分析(下)

呃…我不是故意要凑篇幅写个什么上下篇,实在是因为Drawable源码有点长,一篇写不下啦O(∩_∩)O~

鉴于源码一般较长,以后所有源码分析的部分,英文注释非必要情况都不再保留!

2:Drawable源码分析/翻译

继续上Drawable源码:

package android.graphics.drawable;

public abstract class Drawable {
    ****
    略
    ****

    /** 这个方法很重要,故保留英文注释! 调用mutate(),使当前Drawable实例mutable,这个操作不可逆。 一个mutable的Drawable实例不会和其他Drawable实例共享它的状态。 当你需要修改一个从资源文件加载的Drawable实例时,mutate()方法尤其有用。 默认情况下,所有加载同一资源文件生成的Drawable实例都共享一个通用的状态, 如果你修改了其中一个Drawable实例,所有的相关Drawable实例都会发生同样的变化。 这个方法在[其实你不懂:Drawable着色(tint)的兼容方案 源码解析] 这篇文章里有过介绍,就是为了限定Drawable实例的编辑生效范围仅限于自身。 * Make this drawable mutable. This operation cannot be reversed. A mutable * drawable is guaranteed to not share its state with any other drawable. * This is especially useful when you need to modify properties of drawables * loaded from resources. By default, all drawables instances loaded from * the same resource share a common state; if you modify the state of one * instance, all the other instances will receive the same modification. * * Calling this method on a mutable Drawable will have no effect. * * @return This drawable. * @see ConstantState * @see #getConstantState() */
    public @NonNull Drawable mutate() {
        return this;
    }

    /** 被隐匿 * @hide */
    public void clearMutated() {
        // Default implementation is no-op.
    }

    //下面几个方法介绍了通过不同的方式创建Drawable实例:
    //流、XML、文件地址
    public static Drawable createFromStream(InputStream is, String srcName) {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
        try {
            return createFromResourceStream(null, null, is, srcName);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
    public static Drawable createFromResourceStream(Resources res, TypedValue value, InputStream is, String srcName) {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
        try {
            return createFromResourceStream(res, value, is, srcName, null);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
    public static Drawable createFromResourceStream(Resources res, TypedValue value, InputStream is, String srcName, BitmapFactory.Options opts) {
        if (is == null) {
            return null;
        }
        Rect pad = new Rect();
        if (opts == null) opts = new BitmapFactory.Options();
        opts.inScreenDensity = Drawable.resolveDensity(res, 0);
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            if (np == null || !NinePatch.isNinePatchChunk(np)) {
                np = null;
                pad = null;
            }
            final Rect opticalInsets = new Rect();
            bm.getOpticalInsets(opticalInsets);
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        }
        return null;
    }
    public static Drawable createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException {
        return createFromXml(r, parser, null);
    }
    public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme) throws XmlPullParserException, IOException {
        AttributeSet attrs = Xml.asAttributeSet(parser);
        int type;
        //noinspection StatementWithEmptyBody
        while ((type=parser.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            // Empty loop.
        }
        if (type != XmlPullParser.START_TAG) {
            throw new XmlPullParserException("No start tag found");
        }
        Drawable drawable = createFromXmlInner(r, parser, attrs, theme);
        if (drawable == null) {
            throw new RuntimeException("Unknown initial tag: " + parser.getName());
        }
        return drawable;
    }
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
        return createFromXmlInner(r, parser, attrs, null);
    }
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException {
        return r.getDrawableInflater().inflateFromXml(parser.getName(), parser, attrs, theme);
    }
    public static Drawable createFromPath(String pathName) {
        if (pathName == null) {
            return null;
        }
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, pathName);
        try {
            Bitmap bm = BitmapFactory.decodeFile(pathName);
            if (bm != null) {
                return drawableFromBitmap(null, bm, null, null, null, pathName);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
        return null;
    }
    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
        inflate(r, parser, attrs, null);
    }

    /** 从XML文件中加载Drawable实例,Drawable实例接受主题设置的风格 */
    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException {
        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.Drawable);
        mVisible = a.getBoolean(R.styleable.Drawable_visible, mVisible);
        a.recycle();
    }
    /** 从XML文件中加载Drawable实例 */
    void inflateWithAttributes(@NonNull @SuppressWarnings("unused") Resources r,
            @NonNull @SuppressWarnings("unused") XmlPullParser parser, @NonNull TypedArray attrs,
            @AttrRes int visibleAttr) throws XmlPullParserException, IOException {
        mVisible = attrs.getBoolean(visibleAttr, mVisible);
    }

    /** 这段注释很重要,故保留英文注释! ConstantState这个抽象类被用于存储 多个Drawable实例间 共享的 常量状态值及数据。 如从同一个图片资源创建的多个BitmapDrawable实例,它们将共享 同一个存储在它们的ConstantState中的Bitmap。 * This abstract class is used by {@link Drawable}s to store shared constant state and data * between Drawables. {@link BitmapDrawable}s created from the same resource will for instance * share a unique bitmap stored in their ConstantState. * newDrawable可以运用ConstantState创建一个新的Drawable实例 * <p> * {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances * from this ConstantState. * </p> * Drawable#getConstantState可以获取一个Drawable关联的ConstantState。 调用Drawable#mutate(),则将为新创建的Drawable实例单独关联一个ConstantState。 * Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling * {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that * Drawable. */
    public static abstract class ConstantState {
        /** 运用ConstantState创建一个新的Drawable实例 */
        public abstract @NonNull Drawable newDrawable();
        /** 运用ConstantState创建一个新的Drawable实例 */
        public @NonNull Drawable newDrawable(@Nullable Resources res) {
            return newDrawable();
        }
        /** 运用ConstantState创建一个新的Drawable实例 */
        public @NonNull Drawable newDrawable(@Nullable Resources res,
                @Nullable @SuppressWarnings("unused") Theme theme) {
            return newDrawable(res);
        }
        /** 返回会影响Drawable实例的一个bit掩码变化设置 */
        public abstract @Config int getChangingConfigurations();
        /** 返回所有的像素数 public int addAtlasableBitmaps(@NonNull Collection<Bitmap> atlasList) { return 0; } /** @hide */
        protected final boolean isAtlasable(@Nullable Bitmap bitmap) {
            return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;
        }
        /** 返回当前共享状态是否可以设置主题 */
        public boolean canApplyTheme() {
            return false;
        }
    }

    /** 返回当前Drawable的用于存储共享状态值的ConstantState实例 */
    public @Nullable ConstantState getConstantState() {
        return null;
    }
    //通过Bitmap实例创建Drawable实例
    private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) {
        if (np != null) {
            return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
        }
        return new BitmapDrawable(res, bm);
    }

    /** 确保色彩过滤器和当前色彩与色彩模式一致 */
    @Nullable PorterDuffColorFilter updateTintFilter(@Nullable PorterDuffColorFilter tintFilter, @Nullable ColorStateList tint, @Nullable PorterDuff.Mode tintMode) {
        if (tint == null || tintMode == null) {
            return null;
        }
        final int color = tint.getColorForState(getState(), Color.TRANSPARENT);
        if (tintFilter == null) {
            return new PorterDuffColorFilter(color, tintMode);
        }
        tintFilter.setColor(color);
        tintFilter.setMode(tintMode);
        return tintFilter;
    }

    /** 如果主题有效,则从中获取样式属性, 如果主题无效,则返回没有样式的资源。 */
    static @NonNull TypedArray obtainAttributes(@NonNull Resources res, @Nullable Theme theme, @NonNull AttributeSet set, @NonNull int[] attrs) {
        if (theme == null) {
            return res.obtainAttributes(set, attrs);
        }
        return theme.obtainStyledAttributes(set, attrs, 0, 0);
    }

    /** 根据 原始像素值,资源单位密度和目标设备单位密度 获得一个float像素值 */
    static float scaleFromDensity(float pixels, int sourceDensity, int targetDensity) {
        return pixels * targetDensity / sourceDensity;
    }
    static int scaleFromDensity( int pixels, int sourceDensity, int targetDensity, boolean isSize) {
        if (pixels == 0 || sourceDensity == targetDensity) {
            return pixels;
        }
        final float result = pixels * targetDensity / (float) sourceDensity;
        if (!isSize) {
            return (int) result;
        }
        final int rounded = Math.round(result);
        if (rounded != 0) {
            return rounded;
        } else if (pixels > 0) {
            return 1;
        } else {
            return -1;
        }
    }

    //获取单位密度
    static int resolveDensity(@Nullable Resources r, int parentDensity) {
        final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi;
        return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
    }
    static void rethrowAsRuntimeException(@NonNull Exception cause) throws RuntimeException {
        final RuntimeException e = new RuntimeException(cause);
        e.setStackTrace(new StackTraceElement[0]);
        throw e;
    }

    /** 通过解析tintMode属性枚举值获得一个PorterDuff.Mode 被隐匿 * @hide */
    public static PorterDuff.Mode parseTintMode(int value, Mode defaultMode) {
        switch (value) {
            case 3: return Mode.SRC_OVER;
            case 5: return Mode.SRC_IN;
            case 9: return Mode.SRC_ATOP;
            case 14: return Mode.MULTIPLY;
            case 15: return Mode.SCREEN;
            case 16: return Mode.ADD;
            default: return defaultMode;
        }
    }
}

Drawable类本身源码先写到这儿,接着往下看。

3:Drawable绘制流程

看过Drawable源码,其实我们还是不清楚:
Drawable实例到底是如何被绘制到屏幕上面?
Drawable源码中的那些方法又是什么时候被谁调用的?

我们回想一下,使用Drawable最通常的步骤:
通过Resource获取Drawable实例
将获取的Drawable实例当做背景设置给View或者作为ImageView的src进行显示:

下面就逐步分析理解Drawable的绘制流程。

3.1:通过Resource获取Drawable实例

最常用写法:getResources().getDrawable(int id),看下关键代码:

public class Resources {
    ****
    public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
        final Drawable d = getDrawable(id, null);
        *****
        return d;
    }
    public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            //
            impl.getValue(id, value, true);
            //将获取到的Drawable实例返回
            return impl.loadDrawable(this, value, id, theme, true);
        } ****
    }
}
一路追踪下去:
public class ResourcesImpl {
    //Resource实例,TypedValue,资源ID,Theme实例,true
    @Nullable Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws NotFoundException {
        try {
            ********
            //是否属于ColorDrawable
            final boolean isColorDrawable;
            //Drawable缓存
            final DrawableCache caches;
            final long key;
            //判断资源是否属于颜色资源
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                //如果是加载一张普通的图片,不属于颜色资源
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    return cachedDrawable;
                }
            }
            //如果在Drawable缓存里面未找到资源ID对应的Drawable实例,继续
            final Drawable.ConstantState cs;
            if (isColorDrawable) {
                cs = sPreloadedColorDrawables.get(key);
            } else {
                //如果不属于颜色资源,则从sPreloadedDrawables中查询
                //sPreloadedDrawables只有在执行cacheDrawable方法时
                //才会进行数据添加:而第一次加载图片时候还未执行cacheDrawable
                //所以此时cs = null.
                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
            }
            Drawable dr;
            if (cs != null) {
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            } else {
                //当第一次加载图片资源时候,cs=null且不属于颜色资源,
                //实际是通过loadDrawableForCookie来获取Drawable实例
                dr = loadDrawableForCookie(wrapper, value, id, null);
            }
            *********
    }
    private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id, Resources.Theme theme) {
        ****
        final String file = value.string.toString();
        ****
        final Drawable dr;
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        try {
            if (file.endsWith(".xml")) {
                //如果是从xml文件加载Drawable
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXml(wrapper, rp, theme);
                rp.close();
            } else {
                //从图片资源加载Drawable,执行Drawable.createFromResourceStream
                //获取Drawable实例
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
                is.close();
            }
        } catch (Exception e) {
            ****
        }
        ****
        return dr;
    }
}
一路追踪下去:
public abstract class Drawable {
    public static Drawable createFromResourceStream(Resources res, TypedValue value, InputStream is, String srcName, BitmapFactory.Options opts) {
        ****
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        ****
    }
    private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) {
        if (np != null) {
            //如果加载的图片资源是.9 PNG,返回NinePatchDrawable实例
            return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
        }
        //对于普通图片资源,返回BitmapDrawable
        return new BitmapDrawable(res, bm);
    }
}

由此可见,通过Resource实例加载一张资源图片:
.9图返回1个NinePatchDrawable实例;
普通图片返回1个BitmapDrawable实例。

3.2:将获取的Drawable实例当做背景设置给View

最常用写法:targetView.setBackgroundDrawable(Drawable bg),
同样看一下关键代码

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ****
    public void setBackgroundDrawable(Drawable background) {
        ****
        if (background == mBackground) {
            //如果当前背景和background相同,直接return
            return;
        }
        boolean requestLayout = false;
        mBackgroundResource = 0;
        if (mBackground != null) {
            if (isAttachedToWindow()) {
                //如果当前View实例已经被绘制到屏幕上,则首先取消
                //该View实例原始背景Drawable的动画
                mBackground.setVisible(false, false);
            }
            //移除该View实例原始背景Drawable的动画监听接口
            mBackground.setCallback(null);
            //取消该View实例原始背景Drawable的所有事件
            unscheduleDrawable(mBackground);
        }
        if (background != null) {
            ****
            //设置background的布局方向和View实例一致,
            //Drawable.setLayoutDirection见上一篇文章
            background.setLayoutDirection(getLayoutDirection());
            if (background.getPadding(padding)) {
                //如果Drawable实例background有padding
                resetResolvedPaddingInternal();
                switch (background.getLayoutDirection()) {
                    case LAYOUT_DIRECTION_RTL:
                        //布局方向从右至左
                        mUserPaddingLeftInitial = padding.right;
                        mUserPaddingRightInitial = padding.left;
                        internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
                        break;
                    case LAYOUT_DIRECTION_LTR:
                    default:
                        //布局方向从左至右
                        mUserPaddingLeftInitial = padding.left;
                        mUserPaddingRightInitial = padding.right;
                        //internalSetPadding会将四个参数值和View实例的padding进行比对,若不同则会重新布局+重建View的外部轮廓
                        internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
                }
                mLeftPaddingDefined = false;
                mRightPaddingDefined = false;
            }
            if (mBackground == null
                    || mBackground.getMinimumHeight() != background.getMinimumHeight()
                    || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
                requestLayout = true;
            }
            //设置当前View实例的背景为传入的Drawable实例 background
            mBackground = background;
            if (background.isStateful()) {
                //如果background会根据状态值变更外观,则设置其状态为
                //当前View实例的state
                background.setState(getDrawableState());
            }
            if (isAttachedToWindow()) {
                //如果当前View实例已经被绘制到屏幕上
                //且实例和实例的父控件及递归获得的根布局都处于可见状态,
                //则设置background开启动画效果
                background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
            }
            applyBackgroundTint();
            //设置background动画接口监听为View实例本身(View实现了 Drawable.Callback):
            //public class View implements Drawable.Callback
            background.setCallback(this);
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                //需要重新布局
                requestLayout = true;
            }
        } else {
            mBackground = null;
            if ((mViewFlags & WILL_NOT_DRAW) != 0
                    && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
                mPrivateFlags |= PFLAG_SKIP_DRAW;
            }
            requestLayout = true;
        }
        computeOpaqueFlags();
        if (requestLayout) {
            //重新布局
            requestLayout();
        }
        mBackgroundSizeChanged = true;
        //重绘View实例
        invalidate(true);
        //重建View实例的外部轮廓
        invalidateOutline();
    }
}

由此可见,setBackgroundDrawable方法,调用了Drawable实例的一系列方法,最终引发了View实例的重新布局(requestLayout())重绘(invalidate(true))及重建View实例的外部轮廓(invalidateOutline())。
invalidate会触发draw方法,我们继续看View.draw方法的关键代码:

    public void draw(Canvas canvas) {
        ****
        /* 翻译可能不甚准确,欢迎英语好的同学留言指正O(∩_∩)O~ Draw方法会执行以下几个步骤,且必须按顺序执行: 1:绘制View实例的背景 2:如有必要,保存画布图层以备褪色 3:绘制View实例的内容 4:绘制View实例的中的子控件 5:如有必要,绘制边缘并恢复图层 6:绘制滚动条 * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */
        //从步骤顺序上看,和Drawable相关的就是第1步,只看第1步代码
        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            //绘制背景
            drawBackground(canvas);
        }
        ****
    }

    一路追踪下去:

    private void drawBackground(Canvas canvas) {
        //mBackground就是之前setBackgroundDrawable传入的Drawable实例
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //设置background绘制范围为View实例的所在范围
        setBackgroundBounds();
        ****
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            //最终调用Drawable.draw(Canvas canvas)将Drawable实例
            //绘制到屏幕上
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            //最终调用Drawable.draw(Canvas canvas)将Drawable实例
            //绘制到屏幕上
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

由此可见,View实例的背景Drawable实例最终还是调用自身的Drawable.draw(@NonNull Canvas canvas)方法绘制到屏幕上。

继续查看Drawable.draw方法:

public abstract class Drawable {
    //Drawable中的draw是一个抽象方法,应该是为了众多的
    //子类Drawable拥有自定义的绘制逻辑进行重写
    public abstract void draw(@NonNull Canvas canvas);
}

在分析Resource.getDrawable时候已经知道,
对于普通的图片资源,获取到的是一个BitmapDrawable实例,
我们就来看看BitmapDrawable的draw具体的绘制逻辑:

public class BitmapDrawable extends Drawable {
    @Override
    public void draw(Canvas canvas) {
        ****
        if (shader == null) {
            ****
            //最终调用了Canvas.drawBitmap方法,将Drawable实例中的bitmap绘制到View实例关联的画布上
            canvas.drawBitmap(bitmap, null, mDstRect, paint);
            if (needMirroring) {
                canvas.restore();
            }
        } ****
    }
}

至此,将获取的Drawable实例当做背景设置给View,和Drawable相关的一系列逻辑就分析完了,大致如下:

  • 1:setBackgroundDrawable方法,调用了Drawable的一系列方法,设置了Drawable实例一系列属性值,最终引发了View实例的重新布局(requestLayout()),重绘(invalidate(true))及重建View实例的外部轮廓(invalidateOutline())
  • 2:在View实例重绘过程的第一步,将得到的Drawable实例(View实例的背景)绘制到屏幕上,实质是调用了Drawable.draw(@NonNull Canvas canvas)
  • 3:Drawable.draw本身是个抽象方法,绘制具体逻辑由其子类实现。
    我们以之前获得的BitmapDrawable为例进行分析:
    最终调用了Canvas.drawBitmap方法,将Drawable实例中的bitmap绘制到View实例关联的画布上

Drawable绘制流程今天先写到这儿,现在是2017/03/07 20:41,加班码字到现在有点累了,明后天继续把ImageView和Drawable关联的部分写完吧!

未完待续…

以上就是个人分析的一点结果,若有错误,请各位同学留言告知!

That’s all !

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/58c21174128fe100602598e8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞