美文网首页
Android面试题(下)

Android面试题(下)

作者: kjy_112233 | 来源:发表于2017-08-24 10:30 被阅读0次

    一、View

    (1)MotionEvent是什么?包含几种事件?什么条件下会产生?

    • ACTION_DOWN:第一个手指触摸屏幕
    • ACTION_MOVE:手指在屏幕上移动
    • ACTION_UP:最后一个手指离开屏幕
    • ACTION_CANCEL:手势被取消,不再接受后续事件;从当前控件转移到外层控件时会触发
    • ACTION_OUTSIDE:标志着用户触碰到了正常的UI边界
    • ACTION_POINTER_DOWN:出现一个新的触摸点
    • ACTION_POINTER_UP:非最后一个手指抬起

    (2)scrollTo()和scrollBy()的区别?

    • scrollBy内部调用了scrollTo,它是基于当前位置的相对滑动;而scrollTo是绝对滑动
    • 两者都只能对view内容进行滑动,而不能使view本身滑动。

    (3)Scroller中最重要的两个方法是什么?主要目的是?

    • 在MotionEvent.ACTION_UP事件触发时调用startScroll()方法,该方法并没有进行实际的滑动操作,而是记录滑动相关量
    • 马上调用invalidate/postInvalidate()方法,请求View重绘,导致View.draw方法被执行
    • 紧接着会调用View.computeScroll()方法,此方法是空实现,需要自己处理逻辑。具体逻辑是:先判断computeScrollOffset(),若为true(表示滚动未结束),则执行scrollTo()方法,它会再次调用postInvalidate(),如此反复执行,直到返回值为false。

    (4)谈一谈View的事件分发机制?

    • 事件传递顺序:Activity(Window) -> ViewGroup -> View
    • dispatchTouchEvent:进行事件的分发(传递)。返回值是 boolean 类型,受当前onTouchEvent和下级view的dispatchTouchEvent影响
    • onInterceptTouchEvent:对事件进行拦截。该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。一旦拦截,则执行ViewGroup的onTouchEvent,在ViewGroup中处理事件,而不接着分发给View。且只调用一次,所以后面的事件都会交给ViewGroup处理。
    • onTouchEvent:进行事件处理。

    (5)如何解决View的滑动冲突?

    • 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。onInterceptTouchEvent方法
    • 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。子View的dispatchTouchEvent方法并设置requestDisallowInterceptTouchEvent方法,父View需要重写onInterceptTouchEvent方法

    (6)谈一谈View的工作原理?

    • View工作流程简单来说就是,先measure测量,用于确定View的测量宽高,再 layout布局,用于确定View的最终宽高和四个顶点的位置,最后 draw绘制,用于将View 绘制到屏幕上

    (7)MeasureSpec是什么?有什么作用?

    • UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。
    • EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。
    • AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。
    • 作用:通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小

    (8)自定义View/ViewGroup需要注意什么?

    • 设置View支持wrap_content
    • 设置View支持padding
    • 尽量不要在View中使用Handler
    • View中有线程或动画需要及时停止
    • 处理滑动嵌套

    (9)onTouch()、onTouchEvent()和onClick()关系?

    • 优先度onTouch()>onTouchEvent()>onClick()。因此onTouchListener的onTouch()方法会先触发;如果onTouch()返回false才会接着触发onTouchEvent(),同样的,内置诸如onClick()事件的实现等等都基于onTouchEvent();如果onTouch()返回true,这些事件将不会被触发。

    (10)SurfaceView和View的区别?

    • View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新
    • View适用于主动更新的情况,而SurfaceView适用于被动更新,如频繁刷新,这是因为如果使用View频繁刷新会阻塞主线程,导致界面卡顿
    • SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于需要频繁刷新、刷新时数据处理量很大的页面

    (11)invalidate()和postInvalidate()的区别?

    • invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。

    (12)Activity、View、Window三者之间的关系?

    • 在Activity启动过程其中的attach()方法中初始化了PhoneWindow,而PhoneWindow是Window的唯一实现类,然后Activity通过setContentView将View设置到了PhoneWindow上,而View通过WindowManager的addView()、removeView()、updateViewLayout()对View进行管理。

    (13)Window的内部机制

    • ViewManager接口中定义三个对Window操作方法:添加、更新和删除。
    • WindowManager也是一个接口,它继承了ViewManager接口
    • WindowManager的具体实现类是WindowManagerImpl,并没有直接实现Window的三大操作,而是交给了WindowManagerGlobal。
    • WindowManagerGlobal以单例模式向外提供自己的实例,因此通过WindowManagerGlobal的addView()、updateViewLayout()、removeView()实现WindowManager对Window的添加、删除和修改。
    • Windows的三大操作最终都会通过一个IPC过程移交给WindowManagerService。
    • Window和View通过ViewRootImpl来联系,ViewRootImpl可控制View的测量、布局和重绘。

    (14)Window有哪几种类型?

    • 应用Window:对应一个Activity。
    • 子Window:不能单独存在,需附属特定的父Window。如Dialog。
    • 系统Window: 需申明权限才能创建。如Toast。

    (15)View中getRowX和getX的区别

    • getRowX:触摸点相对于屏幕的坐标
      +getX:触摸点相对于按钮的坐标

    二、Animation

    (1)Android中有哪几种类型的动画?

    • ViewAnimation补间动画:容易设置和能满足许多应用程序的需要。AlphaAnimation(透明度动画)、RotateAnimation(旋转动画)、ScaleAnimation(缩放动画)、TranslateAnimation(平移动画)四种类型的补间动画。并且View动画框架还提供了动画集合类(AnimationSet),通过动画集合类可以将多个补间动画以组合的形式显示出来,不能真正的改变view的位置。
    • PropertyAnimation属性动画:这种动画可以设置给任何Object,包括那些还没有渲染到屏幕上的对象。这种动画是可扩展的,可以让你自定义任何类型和属性的动画。对该类对象进行动画操作,真正改变了对象的属性。
    • DrawableAnimation:专门用来一个一个的显示Drawable的resources,就像放幻灯片一样。

    (2)帧动画在使用时需要注意什么?

    • 使用祯动画要注意不能使用尺寸过大的图片,否则容易造成OOM

    (3)View动画和属性动画的区别?

    • View动画:通过不断图形变换实现动画效果,不能真正的改变view的位置;只能作用在View上。
    • 属性动画:通过动态改变对象的属性实现动画效果,真正改变View的位置;能作用在任何对象上。

    (4)View动画为何不能真正改变View的位置?而属性动画为何可以?

    • View动画改变的只是View的显示,而没有改变View的响应区域;而属性动画会通过反射技术来获取和执行属性的get、set方法,从而改变了对象位置的属性值。

    (5)属性动画插值器和估值器的作用?

    • 插值器(Interpolator):根据时间流逝的百分比计算出当前属性值改变的百分比。确定了动画效果变化的模式,如匀速变化、加速变化等等。
    • 类型估值器(TypeEvaluator):根据当前属性改变的百分比计算出改变后的属性值。针对于属性动画,View动画不需要类型估值器。

    三、Drawable、Bitmap

    (1)了解哪些Drawable?适用场景?

    • BitmapDrawable 表示一张图片
    • NinePatchDrawable 可自动地根据所需的宽/高对图片进行相应的缩放并保证不失真(表示一张.9格式的图片)
    • ShapeDrawable 表示纯色、有渐变效果的基础几何图形(矩形,圆形,线条等)
    • LayerDrawable 可通过将不同的Drawable放置在不同的层上面从而达到一种叠加后的效果
    • StateListDrawable 表示一个Drawable的集合且每个Drawable对应着View的一种状态
    • TransitionDrawable LayerDrawable的子类,实现两层 Drawable之间的淡入淡出效果。
    • InsetDrawable 表示把一个Drawable嵌入到另外一个Drawable的内部,并在四周留一些间距。
    • ScaleDrawable 表示将Drawable缩放到一定比例。
    • ClipDrawable 表示裁剪一个Drawable。

    (2)mipmap系列中xxxhdpi、xxhdpi、xhdpi、hdpi、mdpi和ldpi存在怎样的关系?

    • 表示不同密度的图片资源,像素从高到低依次排序为xxxhdpi>xxhdpi>xhdpi>hdpi>mdpi>ldpi,根据手机的dpi不同加载不同密度的图片

    (3)dp、dpi、px的区别?

    • px:像素,如分辨率1920x1080表示高为1920个像素、宽为1080个像素
    • dpi:每英寸的像素点
    • dp:密度无关像素,是个相对值

    (4)res目录和assets目录的区别?

    • res/raw中的文件会被映射到R.java文件中,访问时可直接使用资源ID,不可以有目录结构
    • assets文件夹下的文件不会被映射到R.java中,访问时需要AssetManager类,可以创建子文件夹

    (5)加载图片的时候需要注意什么?

    • 直接加载大容量的高清Bitmap很容易出现显示不完整、内存溢出OOM的问题,所以最好按一定的采样率将图片缩小后再加载进来
    • 为减少流量消耗,可对图片采用内存缓存策略,又为了避免图片占用过多内存导致内存溢出,最好以软引用方式持有图片
    • 如果还需要网上下载图片,注意要开子线程去做下载的耗时操作

    (6)LRU算法的原理?

    • LruCache(内存缓存):LruCache类是一个线程安全的泛型类:内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。
    • DiskLruCache(磁盘缓存): 通过将缓存对象写入文件系统从而实现缓存效果。

    四、IPC(跨进程通信)

    (1)为何需要进行IPC?多进程通信可能会出现什么问题?

    • 所有运行在不同进程的四大组件,只要它们之间需要通过内存共享数据,都会共享失败。由于Android为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间
    • 静态变量和单例模式失效:由独立的虚拟机造成。
    • 线程同步机制失效:由独立的虚拟机造成
    • SharedPreference的不可靠下降: SharedPreferences不支持两个进程同时进行读写操作,即不支持并发读写,有一定几率导致数据丢失。
    • Application多次创建:Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次

    (2)Android中为何新增Binder来作为主要的IPC方式?

    • 传输效率高、可操作性强:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。
    • 实现C/S架构方便:Linux的众IPC方式除了Socket以外都不是基于C/S架构,而Socket主要用于网络间的通信且传输效率较低。Binder基于C/S 架构 ,Server端与Client端相对独立,稳定性较好。
    • 安全性高:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Binder机制为每个进程分配了UID/PID且在Binder通信时会根据UID/PID进行有效性检测。

    (3)Binder框架中ServiceManager的作用?

    • 在Binder框架定义了四个角色:Server,Client,ServiceManager和Binder驱动。其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间。
    • ServiceManager:服务的管理者,将Binder名字转换为Client中对该Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用。
    • Server&Client:服务器&客户端。在Binder驱动和ServiceManager提供的基础设施上,进行Client-Server之间的通信。

    (4)Android中有哪些基于Binder的IPC方式?简单对比下?

    • Bundle:Bundle实现了Parcelable接口,方便在不同的进程中传输数据。支持在activity、service、receiver之间通过intent.putExtra()传递Bundle数据。Bundle内部是通过ArrayMap来存取数据。Bundle不支持的数据类型无法在进程中被传递
    • Messager:底层实现是AIDL,即对AIDL进行了封装,更便于进行进程间通信。支持一对多串行通信,支持实时通信。
    • 文件共享:两个进程通过读/写同一个文件来交换数据。比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。
    • ContentProvider:专门用来进行不同应用间数据共享的方式。
    • AIDL:支持一对多并发通信,支持实时通信。
    • Socket:不仅可跨进程,还可以跨设备通信。网络数据交换

    (5)是否了解AIDL?原理是什么?如何优化多模块都使用AIDL的情况?

    • 如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端实现间接调用服务端对象的方法。
    • 当有多个业务模块都需要AIDL来进行IPC,此时需要为每个模块创建特定的aidl文件,那么相应的Service就会很多。必然会出现系统资源耗费严重、应用过度重量级的问题。解决办法是建立Binder连接池,即将每个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复创建Service。
    • 每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个Service,服务器提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对像,不同的业务模块拿到所需的Binder对象后就可进行远程方法的调用了

    五、ListView、RecyclerView

    **(1)两者的缓存机制上的区别

    • ListView与RecyclerView缓存机制原理大致一样,滑动的时候,离开屏幕的ItemView被回收到缓存,新的itemView加载优先获取的缓存中的。
    • 两者的缓存层级不同,ListView只有两层,RecycleView有四级缓存。

    (2)RecyclerView的优缺点

    • ViewHolder的编写规范化,ListView是需要自己定义的,而RecyclerView是规范好的;
    • RecyclerView复用item全部搞定,不需要想ListView那样setTag()与getTag();
    • RecyclerView多了一些LayoutManager工作,但实现了布局效果多样化;
    • RecyclerView 的布局效果丰富,可以在LayoutMananger中设置:线性布局(纵向,横向),表格布局,瀑布流布局
    • 在ListView中有个setEmptyView() 用来处理Adapter中数据为空的情况;但是在RecyclerView中没有这个API,所以在RecyclerView中需要进行一些数据判断来实现数据为空的情况。
    • 在ListView中可以通过addHeaderView() 与 addFooterView()来添加头部item与底部item,来当我们需要实现下拉刷新或者上拉加载的情况。
    • 在RecyclerView添加头部item或者底部item的时候,需要在Adapter中自己编写,根据ViewHolder的Type与View来实现自己的Header,Footter与普通的item,但是这样就会影响到Adapter的数据,比如position,添加了Header与Footter后,实际的position将大于数据的position;

    (3)局部刷新的区别

    • 在ListView中通常刷新数据是用notifyDataSetChanged() ,但是这种刷新数据是全局刷新的,这样一来就会非常消耗资源。
    • 在RecyclerView中可以实现局部刷新,例如:notifyItemChanged();
    • getFirstVisiblePosition(),该方法获取当前状态下list的第一个可见item的position。
    • getLastVisiblePosition(),该方法获取当前状态下list的最后一个可见item的position。
    • getItemAtPosition(),该方法返回当前状态下position位置上listView的convertView
    • 然后调用getView()方法来刷新这个item的数据;

    (4)ListView,RecyclerView,ScrollView滑动到底部监听

    • 监听ListView滑到底部只要实现setOnScrollListener()方法
    • 监听RecyclerView滑到底部只要实现addOnScrollListener()方法
    • 监听ScrollView滑到底部只要实现setOnScrollToBottomLintener()方法

    (5)listView错乱原因

    • item的缓存机制:为了使用性能更优,listView会缓存行item。listView通过adapter的getView函数获得每行item,如果某行item已经滑出屏幕,若该item不在缓存内,则put进缓存,否则更新缓存;获取滑入屏幕的行item之前会先判断缓存中是否有可用的item,如果有做为convertView参数传递给adapter的getView
    • item图片显示重复:异步加载图片滑动快加载慢,造成显示重复
    • 行item图片显示错乱和行item图片显示闪烁
    • 出现错乱的原因是异步加载对象被复用造成的,如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行item的标识是否一致,一致则显示,否则不做处理即可。
    • listView中加载不同的item使用geiItemViewType和使用getViewTypeCount设置item样式数

    六、Android常见异常与优化

    (1)布局优化类

    • 尽量减少布局层级和复杂度,使用合适的布局三种常见的ViewGroup的绘制速度:FrameLayout > LinerLayout > RelativeLayout
    • 使用include标签可以指定插入一段布局文件到当前布局。这样的话既提高了布局复用,也减少了代码书写。
    • merge标签可以和include的标签一起使用从而减少布局层级。
    • ViewStub延时加载,显示时才去加载出来。
    • ListView中contentView复用,引入holder来避免重复的findViewById,分页加载。
    • 避免过于复杂的布局:如果我们的UI布局层次太深,或是自定义控件的onDraw中有复杂运算,CPU的相关运算就可能大于16ms,导致卡顿。
    • 避免过度绘制:绘制了多重背景,绘制了不可见的UI元素。
    • 移除Activity默认背景,在不需要Activity的默认背景减少Activity启动时的渲染时间,提升启动效率。

    (2)响应优化类

    • 主线程阻塞:开辟单独的子线程来处理耗时阻塞事务。
    • CPU满负荷, I/O阻塞:I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行。

    (3)内存优化类

    • 执行GC操作的时候,任何线程的任何操作都会需要暂停,自然会导致界面卡顿。
    • 避免频繁的GC:大量的对象被创建又在短时间内马上被释放导致频繁GC。

    (4)网络优化类

    • 减少网络数据获取的频次:减少了radio的电量消耗,控制电量使用。
    • 减少获取数据包的大小:可以减少流量消耗,也可以让每次请求更快。
    • 控制图片的大小:图片相对于接口请求来说,数据量要大得多。
    • 网络缓存:适当的缓存,既可以让我们的应用看起来更快,也能避免一些不必要的流量消耗。

    **(5)Bitmap优化

    • 主动释放Bitmap资源
    • 主动释放ImageView的图片资源
    • 对加载图片进行压缩,避免加载图片多大导致OOM出现

    (6)内存泄漏是什么?为什么会发生?内存泄漏优化?

    • 内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。
    • 发生内存泄漏是由于长周期对象持有对短周期对象的引用,使得短周期对象不能被及时回收。
    • 动画:无限循环动画,导致动画一直播放。当播放的Activity退出,在onDestroy中停止动画。
    • 匿名内部类:匿名内部类隐式持有对所在Activity的引用。 当该对象传入到异步线程中可能导致外部类无法被回收。
    • 非静态内部类:内部类持有外部类的引用,非静态内部类创建静态的实例对象,导致外部类无法被回收。修改为静态内部类。
    • 单例模式导致的内存泄漏:由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。
    • 尽量避免使用static成员变量:静态成员生命周期与应用的生命周期一样。
    • Handler造成的内存泄漏:Message持有对Handler的引用,而非静态内部类的Handler又隐式持有对外部类Activity的引用,使得引用关系会保持至消息得到处理,从而阻止了Activity的回收。
    • 资源未关闭造成的内存泄漏:BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用。在Activity销毁的时候要及时关闭或者注销
    • 静态集合类引起内存泄漏:主要是HashMap,Vector等。如果是静态集合这些集合没有及时set null的话。短周期对象就无法及时释放。
    • 内存泄露可能导致APP占用内存过高,影响效率,严重的话会导致OOM。查找内存泄露可以用LeakCanary等工具。

    (7)内存泄漏和内存溢出的区别

    • 内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。
    • 内存溢出(out of memory)是指程序在申请内存时,没有足够的内存空间供其使用。

    (8)什么情况会导致内存溢出?如何避免OOM异常?

    • 内存泄漏是导致内存溢出的主要原因
    • 直接加载大图片也易造成内存溢出
    • 减少内存对象的占用:减少bitmap的内存占用,减少资源图片的大小,避免在Android里面使用Enum,优化布局层次,减少内存消耗。
    • 内存对象的重复利用:listView/GridView/RecycleView中对contentView的复用,避免在onDraw方法里面new对象,stringBuilder代替+,复用系统自带的资源,Bitmap对象的复用。

    (9)ANR是什么?怎么避免和解决ANR

    • 在规定的时间内,没有响应按键或触摸事件在特定时间内无响应
    • ANR的关键是处理超时所以应该避免在UI线程中做耗时操作,BroadcastReceiver还是Service都是运行在主线程中,避免处理复杂的逻辑和计算。

    **(10)异常处理机制知道哪些?

    • 捕捉异常:由系统自动抛出异常,即try捕获异常->catch处理异常->finally 最终处理
    • 抛出异常:在方法中将异常对象显性地抛出,之后异常会沿着调用层次向上抛出,交由调用它的方法来处理。配合throws声明抛出的异常和throw抛出异常。
    • 自定义异常:继承Execption类或其子类

    七、Android基础

    (1)访问修饰符public private protected 以及不写时的区别?

    • public:公共的修饰符
    • private:当前类的修饰符
    • protected:当前类,子类,同包修饰符
    • default:当前类和同包修饰符

    (2)解释下Android程序运行时权限和文件系统权限的区别

    • 文件的系统权限是由Linux系统规定的,只读,读写等
    • 运行时权限,是对于某个系统上的APP来访问权限,允许,拒绝,询问,该功能可以防止非法的程序访问敏感信息。

    (3)FrameWork工作方式及原理

    • Framework是Android系统对Linux,lib库等封装,提供WMS,AMS,bind机制,handler-message机制等方式,供APP使用简单来说framework就是提供APP生存的环境。

    (4)Android内存优化

    • 内存优化的作用:避免因不正确使用内存、缺乏管理,从而出现内存泄露、内存溢出、内存空间占用过大等问题,最终导致应用程序崩溃。

    针对进程的内存策略

    • 内存分配策略:ActivityManagerService集中管理所有进程的内存分配
    • 内存回收策略:Application Framework决定回收的进程类型,Android中的进程是托管,当进程空间紧张是按照优先级从低到高回收进程;Linux内核真正回收具体进程

    针对对象、变量的内存策略

    • 内存分配策略
      静态分配(静态变量):存储已被虚拟机加载的类信息、常量、静态变量;在程序编译时已分配好,存在程序整个运行期间,不需要被回收。
      栈式分配(局部变量):存储方法执行时的局部变量、对象的引用;方法执行时定义局部变量自动分配内存,方法执行结束自动释放内存。
      堆式分配(对象实例):存储对象的实例、实例成员变量;创建对象时由程序分配,由Java垃圾回收管理器自动管理回收。
    • 内存释放策略:对象、变量的内存释放由Java垃圾回收器(GC)、帧栈负责

    常见的内存问题、优化方案

    • 内存泄漏:程序在申请内存后,当该内存不需再使用,但却无法被释放回收的现象。
    • 图片资源Bitmap:Android系统分配给每个应用程序的内存有限,图片资源非常的消耗内存;使用后释放图片资源、根据分辨率适配图片、设置图片缓存。
    • 内存抖动:程序频繁分配内存、频繁回收内存;频繁的回收内存会导致卡顿甚至内存溢出。

    相关文章

      网友评论

          本文标题:Android面试题(下)

          本文链接:https://www.haomeiwen.com/subject/mhuldxtx.html