专栏首页Android 进阶Android 开发艺术探索笔记一

Android 开发艺术探索笔记一

IntentFilter

  1. IntentFilter 为了匹配过滤列表,需要同时匹配actiondatacategory,否则匹配失败,actingdatacategory可以有多个,只有一个intent同时匹配actioncategorydata类别才算完全匹配。
  2. data匹配规则:
    • scheme:URI模式,比如http、file、content,如果URI没有指定scheme那么整个URI其他参数无效。
  3. Host:URI主机名,比如www.google.com,如果host未指定,整个URI无效
  4. Port:端口号,比如80 只有当指定scheme与host,端口号才有意义。

Android 多进程模式

Android多进程模式下会造成以下几方面问题:

  1. 单例与静态成员完全失效
  2. 线程同步机制失效
  3. sharedprefrences的可靠性降低
  4. Application多次创建,当一个组件跑在一个新的进程中,系统由于要创建新的进程同时分配独立的虚拟机,这个过程就是启动一个新的应用过程,重新启动一遍,自然会创建新的Application

进程通信

Binder进程通信

生成的Binder

  1. DESCRIPETORBinder唯一标识,一般用当前binder类名
  2. asInterface 将服务端binder对象转换成客户端所需的AIDL接口
  • 如果客户端与服务端运行在同一个进程,那么返回的就是服务端Stub对象本身,否则返回的是系统封装后的stub.proxy对象
  1. asBinder 用于返回当前binder对象
  2. onTransact 此方法运行在binder线程池中,当客户端发起跨进程请求时,远程请求经由系统底层封装后交由此方法进行处理,服务端通过code确定客户端请求目标是什么,接着从data中取出目标所需的参数,当目标方法执行完毕后,就向reply中写入返回值。
  • 如果onTransact 方法返回false,那么客户端请求失败。
  1. Binder 提供两个配对方法,linkDeath和unlinkDeath,通知linkDeath来设置死亡代理,当binder死亡时,重新发起连接从而恢复连接。

Android IPC方式

  1. 使用Bundle 传递数据
  2. 使用文件共享。需要避免并发写,通过使用同步限制多个线程写操作,适用于对数据同步要求不高的进程通信,需要妥善处理并发写的问题
  3. 使用Messenger 是一个轻量级的IPC方案,它的底层实现了AIDL。支持一对多串行通信。支持实时通信。只能传输Bundle支持的数据类型。不支持RPC。
  4. 使用AIDL,处理大量请求:
  • 服务端首先创建一个service监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在service中实现接口即可
  • 首先需要绑定服务端service,绑定成功后,将服务端返回的binder对象转换成AIDL接口所属类型就可以调用AIDL方法了
  • 对象不能跨进程进行传输,需要实现Parcelable接口,通过RemoteCallbackList,它是系统专门提供删除跨进程listener接口的
  • 客户端调用远程服务时,被调用的方法运行在服务端binder线程池中,同时客户端线程会被挂起,如果服务端方法比较耗时,就会造成客户端阻塞,如果客户端运行在UI线程,就会出现ANR。放在非UI线程即可
  • ADIL中使用权限验证功能:
    • onBind中进行验证,验证不通过直接返回null
    • onTransact方法中进行验证,验证失败直接返回false
  1. 使用ContentProvide,是用来不同应用之间可以数据共享。底层一样是binder
  • onCreate()代表ContenProvider创建,一般做初始化操作,getType返回Uri请求所对应的MIME类型,如图片、视频等
  • contentProvider通过Uri来区分外界访问的数据集合
  • update、insert、delete方法会引起数据源改变,需要通过contentProvidernotifyChange通知外界数据发生改变,要观察contentProvider外界数据源已经发生改变,可以通过registerContentProvider注册观察者,与unregisterContentProvider解除观察者
  • query、insert、delete、update四大方法存在多线程并发访问,需要进行线程同步,存在多个SQLiteDatabase需要进行同步因为对象之间无法进行线程同步,一个不需要线程同步,由于其内存对数据库操作有同步处理
  1. 使用Socket实现进程间通信。不能在主线程中访问网络。可以通过网络传输字节流,支持一对多并发实时通信。不支持直接的RPC

RPC 是什么

  • RPC 即 Remote Procedure Call (远程过程调 用) 是一种计算机通讯协议,它为我们定义了计算机 C 中的程序如何调用另外一台计算机 S 的程序,让程序员不需要操心底层网络协议,使得开发包括网络分布式多程序在内的应用程序更加容易。**
  • RPC 是典型的 Client/Server 模式,由客户端对服务器发出若干请求,服务器收到后根据客户端提供的参数进行操作,然后将执行结果返回给客户端。**
  • RPC 位于 OSI 模型中的会话层:
    • OSI模型由低到高分别是:物理层,数据链路层,网络层,传输层,会话层,表示层,应用层。

View的事件体系

View的点击

  1. TouchSlop:是系统所能识别出的被认为最小滑动距离,手指在屏幕上滑动,如果小于这个常量,就不认为进行滑动操作
  2. VelocityTracker:用于追踪手指在滑动过程中的速度
  3. GestureDecteor:手势检测,onDown手指轻轻触摸屏幕瞬间完成
  4. onShowPress 手指轻轻触摸屏幕,尚未松开或拖动
  5. onSingleTabUp 手指触摸屏幕后松开,这是单击行为
  6. onDoubleTap:双击,由两次连续的单击组成,不能与onSingleTabConfirmed共存
  7. onSingleTabConfirmed严格的单击行为,如果触发onSingleTabConfirmed,那么后面不能再跟一个单击行为

View的滑动

  1. Scroller:实现view的弹性滑动,实现过度效果。工作原理是:通过computeScrollview不断进行重绘,根据重绘的时间间隔,得出view的当前滑动位置,根据位置通过scrollTo完成滑动,多次小幅度滑动就组成了弹性滑动了。
  2. 使用scrollBy/scrollTo 实现view的滑动,只能将view的内容移动,不能将view的本身进行移动。

滑动对比:

  1. scrollBy/scrollTo 操作简单,适合view内容的滑动
  2. 动画 操作简单,主要适用于没有交互的view和实现复杂的动画效果
  3. 改变布局参数 操作复杂,适用于有交互的view

View的滑动冲突

  1. 场景1:外部滑动与内部滑动方向不一致
    • viewpage与listview的嵌套,因为viewpage内部中处理了这种滑动冲突,所以无需考虑。如果外面是scrollview就需要考虑了
  2. 场景2:外部滑动与内部滑动方向一致
  3. 场景3:上面两种滑动的嵌套

解决滑动冲突方法

  1. 外部拦截:重写父容器onInterceptTouchEvent,在内部做相应的拦截,首先ACTION_DOWN事件必须返回false,否则后续的ACTION_MOVE与ACTION_UP事件会直接交由父容器处理,无法传递给子元素。如果父容器ACTION_UP返回true,那么子元素的onclick事件无法触发。
  2. 内部拦截:父容器不拦截事件,所有事件都交由子元素进行处理。
  • 调用requestDisallowInterceptTouchEvent方法,当子元素调用parent.requestDisallowInterceptTouchEvent并设置为false,父容器才能拦截所需的事件,否则为true交由子控件处理。

View的工作原理

ViewRoot对应于ViewRootImpl类,它是连接windowmanager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的,在ActivityThread中,当activity创建完毕后,会将DecorView添加到window中,同时创建viewrootImpl对象,并将viewrootImplDecorView关联。View的绘制流程从viewRootperformTraversals方法开始,经过三个过程将view绘制出来

onMeasure

View的measure

measure过程决定了view的宽、高,measure完成后可以通过getMeasureWidth来获取view的测量后的宽与高,在几乎所有情况都是这样,特殊情况,Layout决定view的四个顶点,可以通过getTop,getBottom来获取view的四个顶点位置,通过getWidth获取view的最终宽高,只有draw方法完成后,view的内容才会显示在屏幕上

由源码可知,DecorView其实就是一个FrameLayout,view层事件都先经过DecoreView,然后在传递给view.

MeasureSpec 是view的内部类,他封装了view的规格尺寸,包括view的宽高信息,代表一个32位int值,高2位代表测量模式,低30位代表测量大小。

测量模式有三种:

  1. UNSPECIFIED:父容器对view没有任何限制,view要多大有多大
  2. EXACTIY:父容器已经检测出view所需的精确大小,对应match_parent与具体数值
  3. AT_MOST:父容器指定了一个可用大小的specsize,view大小不能大于这个值,对应wrap_content.

对于view其measurespec由父容器的measurespec与自身的LayoutParams共同决定的。measurespec一旦确定,就可以确定view的宽高。

如果父容器的measurespecwrap_content,子元素的layoutparamswrap_contentmatch_parent显示效果一样,需要在layoutparamswrap_content指定默认的宽与高即可.

ViewGroup的measure

ViewGroup的measure过程:

  • 对于viewgroup来说,除了完成自己的measure,还需遍历所有子元素的measure,和view不同,viewgroup是一个抽象类,没有重写view的onMeasure方法,提供了measureChildren方法。

在某些极端得情况下,在onMeasure方法中拿到的宽与高可能不准确,在onLayout中获取宽与高才是最终的宽与高。

无法获取view的宽高解决方法

在activity启动时,获取view的宽高,在activity的生命周期中无法准确获取宽高,无法保证view测量完毕,获取宽高只能是0.

  1. 可以在onWindowFocusChanged方法中获取,表示view已经初始化,onWindowFocusChanged会被调用多次,在activity窗口得到与失去焦点时都会被调用,继续执行,暂停执行也会,频繁进行onPause与onResume也会频繁调用。
  2. 通过post将一个runnable投递到消息队列尾部 view.post(new Runnable(){ @Overribe public void run(){ int width = view.getMeasureWidth(); } })
  3. 使用viewTreeObserver众多回调可以获取宽高。使用onGlobalLayoutListener接口,当view树状态发生改变或者view内部可见性发生改变,它回调。
  4. 手动对view进行measure得到view的宽高,比较复杂.

Draw过程

  1. 绘制背景
  2. 绘制自己
  3. 绘制childern
  4. 绘制装饰

自定义view须知

  1. 让view支持wrap_content,必须对wrap_content做特殊处理,否则使用wrap_content就相当于使用match_parent
  2. 让view支持padding,直接继承自viewgroup控件需要在onMeasure与onLayout中考虑paddingmargin对其造成的影响,不然导致padding与子元素的margin失效
  3. 避免在view中使用handler,使用post替代
  4. view中有线程与动画需要及时停止,在onDetachFromWindow中,不及时处理,可能会造成内存泄漏
  5. view带有嵌套,需要处理好滑动冲突。

**直接继承view或viewgroup的控件,padding默认是不会生效的,需要自行处理。

Android View 的requestLayout、invalidate与postInvalidate

requestLayout

view的requestLayout()绘制方式:

从源码注释可以看出,如果当前View在请求布局的时候,View树正在进行布局流程的话,该请求会延迟到布局流程完成后或者绘制流程完成且下一次布局发现的时候再执行。

子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

invalidate

该方法的调用会引起View树的重绘,常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候,需要在主线程(即UI线程)中调用该方法。那么我们来分析一下它的实现

当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。

postInvalidate

这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用

一张图反映不同

总结

一般来说,如果View确定自身不再适合当前区域,比如说它的LayoutParams发生了改变,需要父布局对其进行重新测量、布局、绘制这三个流程,往往使用requestLayout。而invalidate则是刷新当前View,使当前View进行重绘,不会进行测量、布局流程,因此如果View只需要重绘而不需要测量,布局的时候,使用invalidate方法往往比requestLayout方法更高效

理解RemoteViews

它表示view的结构,可以在其他进程显示,提供一组跨进程更新界面。在Android中主要用来:通知栏与桌面小部件

它无法直接访问里面的view,必须通过所提供的方法来更新view,比如textview的setTextView方法。两个参数要设置的ID与提供的文本。

remoteviews使用了AppWidgetProvider类实现桌面小部件,本质是一个广播。

AppWidgetProvider 方法

  • onEnable:当窗口小部件第一次添加到桌面时,调用这个方法,多次添加只会调用一次
  • onUpdate:当小部件添加或每次更新都会调用方法,设置updatePeriodMillis指定更新周期
  • onDelete:每删除一次都会调用
  • onDisabled:最后一个该类型的桌面小部件被删除调用
  • onReceive:广播内置方法,分发具体事件

PendingIntent概述

表示一种待定,等待,即将发生的意思。而Intent是立刻发生

**flags常见类型

  • FLAG_ONE_SHOT:只能使用一次,它会自动cancel,后续有,那么send会调用失败
  • FLAG_NO_CREATE:当前描述的pendingintent不会主动创建,如果当前的pendingintent之前不存在,那么getActivity方法直接返回false
  • FLAG_CANCEL_CURRENT:如果当前的pendingintent已经存在,那么他们都会被cancel,系统会创建一个新的pendingintent
  • FLAG_UPDATE_CURRENT:如果pendingintent已经存在,那么都会被更新。intent的Extras会被替换最新

如果manager.notify(1,notification)第一个参数是常量,那么就会弹一个通知,后续通知会把前面完全替换掉,如果每次都不同,多次调用notify就会弹出多个通知.

系统没有通过Binder直接支持view的跨进程访问,而是提供了一个action概念,action代表一个view的操作,实现了Parcelable,首先将view的操作封装到这个action中,然后将对象跨进程传输传到远程中,接着在远程进程中执行action操作。应用每调用一次set方法,remoteViews就会添加一个对应的action,它的apply进行view的更新操作。remoteviews的apply方法内部会遍历所有action对象,并调用他们的apply方法,进行view的更新操作。

AppWidgetProvider的updateAppwidget内部通过apply与reapply加载更新界面

apply:加载布局并更新界面,而reapply只会更新界面。初始化会调用apply,后续调用reapply更新界面

remoteviews中的setOnclickPendingIntent只能给普通的view设置单击事件,不能给listview与stackview设置单击事件。要给它们设置单击事件,必须将setPendingIntentTemplate与setOnclickFillInIntent组合使用才行

Drawable

它表示一种图像的概念,在开发中,被当做view的背景使用

一张图片所形成的的drawable,它的内部宽高就是图片的宽高,但一个颜色形成的drawable没有宽高,drawable内部宽高不等同于它的大小,drawable实际区域大小可以通过他的getBounds方法获取,一般与view的尺寸相同。它没有大小概念,当它被当做view的背景时,会被拉伸至view的同等大小。

BitmapDrawable

BitmapDrawable:表示一张图片,通过xml方式描述它。

  • android:src 图片资源id
  • android:antialias 抗锯齿
  • android:dither 抖动效果 开启这个选项,让高质量图片在低质量的屏幕上还能保持较好的显示效果
  • android:filther 开启过滤 当图片拉伸时,也能保持很好的显示效果
  • android:mipMap 图像相关的处理技术 纹理映射 默认设置为false
  • android:tileMode 平铺模式 disable 表示关闭平铺模式repeat重复显示
  • mirror 镜面显示 clamp图片四周元素会扩展到周围区域

ShapeDrawable

ShapeDrawable 通过颜色来构造图形,可以纯白,也可以渐变

  • android:shape:表示图片形状 rectangle oval(椭圆) ring 圆环其中linering要通过stroke标签指定颜色与宽度,否则无法显示
  • corners 表示角度
  • gradient表示渐变,与solid冲突
  • solid 纯色填充
  • stroke shape的描边
  • padding 表示不是shape的空白,而是包含它的view的空白
  • size标签设置的宽高就是shapedrawable的固有宽高
    • size 大小
      • android:width 整型 宽度
      • android:height 整型 高度

LayerDrawable

LayerDrawable 表示一种层次化的drawable xml标签<layer-list>,将不同的drawable放置在不同的层上面达到一种叠加的效果

StateListDrawable

StateListDrawable:对应selector标签,表示Drawable集合,每个drawable对应一个状态。

  • android:constantSize:表示StateLIstDrawable固定大小是否不随着状态改变而改变,fasle表示改变**
  • android:variablePadding:表示它的padding是否随着状态改变而改变,true表示改变**

view的常见状态

  • android:state_pressed 表示按下
  • android:state_focused表示获取焦点
  • android:state_selected表示用户选择了view

系统会根据view的当前状态从selector中选择对应的item,每个item对应一种drawable,从上往下查找,直至查找第一条匹配的item,将默认的item放在最后,不带任何状态。

LevelListDrawable

LevelListDrawable:表示drawable集合,集合中每个drawable都有一个等级,最小等级0 最大等级10000 如果被当做Imageview,可以调用setImageLevel来切换。 等级范围 由maxLevel与minlevel

TransitionDrawable

TransitionDrawable:对应transition标签,实现两个drawable之间淡入淡出效果

  • 通过startTransitionreverseTransition实现淡入淡出已经逆过程

InsertDrawable

InsertDrawable:对应insert标签,将其他drawable内嵌到自己中,当一个view希望自己的背景比自己实际区域小时,可以用这个样式。LayerDrawable也可以实现。

ScaleDrawable

ScaleDrawable:对应标签<scale> ,根据等级来指定缩放比例

  • 等级为0表示不可见,如果等级为10000,那么就没有缩放效果。
  • 如果scaledrawable等级越大,那么内部drawable就看起来越大
  • 如果scaledrawable xml定义的缩放比例越大,那么内部drawable就看起来越小

ClipDrawable

ClipDrawable:对应<clip>标签,根据当前等级来裁剪另一个drawable

  • clipOrientation表示裁剪方向,水平与竖直,gravity需要与clipOrientation一起才能有作用。等级为0表示裁剪全部区域,等级为10000表示不裁剪。

Drawable 类中的几个重要的方法

Drawable 类有四个抽象方法子类必须实现:

public abstract void draw(Canvas canvas); 
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha); 
public abstract void setColorFilter(ColorFilter colorFilter); 
public abstract @PixelFormat.Opacity int getOpacity(); 

- draw:在 setBounds 方法设置的区域的 Canvas 中进行Drawable 的绘制,要绘制状态效果的话,可以由 setAlpha,setColorFilter 等方法控制;

  • setAlpha :给 Drawable 指定一个 alpha 值,在 0 - 255 之间;
  • setColorFilter:设置滤镜效果,有时我们会在 Drawable 内部定义一个 Paint 对象,所以该方法的实现可以为 mPaint.setColorFilter(colorFilter)
  • getOpacity :返回 Drawable 的透明度,取值为 PixelFormat.UNKNOWN,PixelFormat.TRANSLUCENT,PixelFormat.TRANSPARENT,PixelFormat.OPAQUE 中的一个; public void setBounds(int left, int top, int right, int bottom); public void setBounds(@NonNull Rect bounds); protected void onBoundsChange(Rect bounds)
  • setBounds 设置绘制区域矩形边界,draw 方法调用时会用到其设置的值,不设置默认边界均为 0,所以自定义 Drawable 时要重写该方法
  • onBoundsChange setBounds 方法中新旧 bounds 发生变化时回调,默认为空方法; public int getIntrinsicWidth(); public int getIntrinsicHeight();
  • 获取 Drawable 的内部宽高,包含 padding,一张图片形成的 Drawable 内部宽高就是图片的宽高,不同的 Drawable 子类是有不同的实现的,而一个颜色所形成的 Drawable 就没有内部宽高的概念,在用作 View 的 background 时自动拉伸为 View 大小。 public int getMinimumWidth()   public int getMinimumHeight()  
  • 返回 Drawable 建议的最小宽高,View 用作背景时要大于该最小宽高,默认的返回为内部宽高或 0;

动画深入浅析

  1. android:interpolator:插值器,会影响动画速度。
  2. android:shareInterpolator:集合中的动画是否和集合共享一个插值器
  3. android:fillAfter:表示动画结束后,是否停留在结束为止,true表示停留

旋转动画放在位移动画之前,否则位移动画无法执行,组合动画执行顺序最好按照,缩放、位移、旋转与透明。

自定义view的方法并在需要的时候参考矩阵的变换细节,就可以写出特定的自定义view动画

帧动画使用简单,但较容易引起OOM,所以尽量避免使用过多尺寸较大的图片。

view的特殊使用场景:

LayoutAnimation:作用于viewgroup,为viewgroup指定一个动画

属性:

  • android:delay 设置动画时间延迟
  • android:animationOrder:动画顺序:normal ,reverse表示排在后面先开始,逆向,random随机动画**

android:animation 具体的入场动画

Activity切换效果

  • overridePendingTransition(int enterAnim,int exitAnim)
  • enterAnim被打开,所需动画资源
  • exitAnim activity被暂停,动画资源

属性动画要求动画作用的对象提供get和set方法,以动画效果多次调用set方法,每次传递set值不一样,随着时间推移,传递的值越接近于最终值。

  1. object必须提供set方法,如果没有传递值,还需提供get方法,让系统取abc属性初始值(这条件不满足,直接崩溃)
  2. set对abc的改变必须通过某种方式反映出来,比如UI(这条件不满足,动画无效果,不会崩溃)

针对上述问题:

  • 给对象增加get与set方法,系统sdk无权限,不可行
  • 用类包装原始对象,间接提供get与set
  • 采用valueAnimator,监听动画过程,自己实现属性改变

使用动画注意事项

  1. OOM,避免使用帧动画,图片过多就会出现
  2. 内存泄漏,属性动画无限循坏时,需要在activity退出时及时停止
  3. 兼容性问题,3.0以下有兼容问题
  4. view动画问题,view动画是对view的影像做动画,不是真正改变view状态,有时会出现无法隐藏,调用view.clearAnimation清除动画
  5. 不使用px,否则在不同的设备出现不同的效果
  6. 动画元素交互,3.0系统之前,新位置无法触发单击事件,3.0之后,单击触发为移动后的位置,但是view的动画仍在原位置
  7. 开启硬件加速,提高动画流畅性 &lt;activity android:name=&quot;.view.activity.LeadActivity&quot; android:hardwareAccelerated=&quot;true&quot; android:configChanges=&quot;keyboardHidden|orientation|screenSize&quot;              android:theme=&quot;@style/MyTheme&quot;&gt; &lt;/activity&gt; View.isHardwareAccelerated()如果返回的是true,表示使用了硬件加速 Canvas.isHardwareAccelerated(),如果返回true表示这个图层开启了硬件加速

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Window 机制源码分析

    Window 是一个抽象的基类,表示一个窗口,包含一个View tree和layout参数。

    Yif
  • 列表、存储以及WebView 相关优化

    不要在你的getView()中写过多的逻辑代码,我们能够将这些代码放在别的地方。比如:

    Yif
  • View 绘制源码分析

    这里的measure方法为final 所以不可重写,该方法主要是用来计算出view自身的实际大小,并设置宽高。

    Yif
  • View官方文档

    nimomeng
  • 小程序url传参如何写变量

    蓓蕾心晴
  • Android中View研究自学之路 Android6.0源码分析之View(一)Android6.0源码分析之View(二)

    Android中View研究自学之路 http://blog.csdn.net/zrf1335348191/article/details/54171263 ...

    fanfan
  • Android6.0源码分析之View(一)

    目前对于view还处于学习阶段,本来打算学习结束之后再写一篇进行总结,但是发现自己自制力太差,学习效率太低,所以在此,边学边写博客,不仅督促自己完成对view的...

    fanfan
  • Android中View研究自学之路

    写这篇博客呢是在研究了view将近一个月之后,算是对自己的学习做一个总结,进而反思一下学习方法,本博文不涉及代码分析。

    fanfan
  • Android "巧"仿蚂蚁森林水滴动效

    本文重在思路和性能,就不介绍自定义view和handler避免内存泄漏或是导致空指针这些了,喜欢请clone项目并star、fork一下,感谢各位。

    CCCruch
  • 更轻量的 View Controllers

    View controllers 通常是 iOS 项目中最大的文件,并且它们包含了许多不必要的代码。所以 View controllers 中的代码几乎总是复用...

    用户5290428

扫码关注云+社区

领取腾讯云代金券