Android面试

讲一下Android的生命周期:

  1. onCreate():生命周期的第一个方法,用来资源初始化、调用setContextView( )加载 布局。
  2. onStart(): Activity正在启动中,即将用。此时,Activity可见但是还是被隐藏在
    stack中。
  3. onResume():Activity已经可见,并且已经出现在前台stack而且已经开始活动了,与onStart( )的区别在于是否在前台。
  4. onRestart():Activity正在重新启用,一边是Activity从隐藏到可见时被调用。如:从Home键返回或者从上一个Activity返回。
  5. onPause(): Activity正在停止,正常情况下onStop()会接着被调用,用于存储数据。
    onPause()不能执行过长时间,否则会阻塞UI线程。
  6. onStop():可以做一些稍微耗时的工作,但也不能时间过长,否则会阻塞UI线程。
  7. onDestory():Activity的最后的一个回调方法,可以做一些资源回收的工作。

生命周期的具体调用情况:

  1. 第一次打开Activity时:onCreate()–onStart()–onResume()
  2. 打开新的Activity时:前一个Activity:onPause()-- onStop()
  3. 再次回到Activity时:onRestart() --onStart()–onResume()
  4. 用户按返回Home键时:onPause()–onStop()–onDestory()
  5. 从ActivityA启动到ActivityB时:onPause(ActivityA)–onStart()–onResume()–onStop(ActivityA)

讲一 下Android的四大组件:

活动:Activity,用来表现功能
服务:service,后台运行服务,不提供界面展示
广播接收者:BroadCast Receive,用来接收广播
内容提供者:Content Provider,支持多个应用中存储和读取数据,相当于数据库

Service的生命周期
startService():启动;当应用程序组件(如:Activity)调用startSevice时启动服务时,处于started状态,只用service调用stopSelf或者其他组件调用stopService才会停止服务。生命周期为: onCreate—onStartCommand—onDestory

bindService():绑定;当应用程序组件调用bindServive绑定启动服务时,处于bound状态,其他组件可以通过回调获取service的代理对象和service进行交互,而这两方也进行了绑定,所以启动方销毁时,service也会自动执行unbind操作,当发现所有的绑定都销毁了才会销毁service。*(与Activity进行绑定的)
生命周期为:onCreate—onBind—onUnbind—onDestory

Servce的回调函数onCreate不能执行耗时操作,否则会阻塞UI线程,可以通过线程和Handle方式执行耗时操作。
IntentService:相比较父类Service来说,其回调函数onHandleIntent中可以直接进行耗时操作,不必再开线程。其原理是IntentService的成员变量 Handler在初始化时已属于工作线程,之后handleMessage,包括onHandleIntent等函数都运行在工作线程中,多次调用onHandleIntent函数(也就是有多个耗时任务要执行),多个耗时任务会按顺序依次执行。原理是其内置的Handler关联了任务队列,Handler通过looper取任务执行是顺序执行的。这个特点就能解决多个耗时任务需要顺序依次执行的问题。而如果仅用service,开多个线程去执行耗时操作,就很难管理。

讲一讲广播:(广播有两种注册方式:静态注册、动态注册)

静态注册:AndroidManifest中进行注册后,不管改应用程序是否处于活动状态,都会进行监听,比如某个程序时监听 内存的使用情况的,当在手机上安装好后,不管改应用程序是处于什么状态,都会执行改监听方法中的内容
动态注册:代码中进行注册后,当应用程序关闭后,就不再进行监听。
BroadCastReceiver 的生命周期很短暂,当接收到广播的时候创建,当onReceive()方法结束时销毁,运行再主线程中的,不能在其中直接执行耗时任务。,
广播的类型:普通广播(自定义的Intent广播),系统广播(系统内置的广播,如:开机、网络状态变化等)、有序广播(发送出去的广播被广播接收者按照先后顺序接收,先接收道广播接受者可对广播进行修改或者截断,使用
sendOrderedBroadcast(intent))、APP内部广播(可以列为局部广播,在同一个APP内发送和接收,将exported属性设为false)

广播分为有序广播和无序广播,通过binder机制实现通信。
无序广播:完全异步,逻辑上可以被任何广播接收者接收到。优点是效率较高。缺点是一个接收者不能将处理结果传递给下一个接收者,并无法终止广播intent 的传播。
有序广播:按照被接收者的优先级顺序,在被接收者中依次传播。比如有三个广播接收者 A,B,C,优先级是 A > B > C。那这个消息先传给A,再传给 B,最后传给 C。每个接收者有权终止广播,比如 B 终止广播,C 就无法接收到。此外 A接收到广播后可以对结果对象进行操作,当广播传给 B 时, B 可以从结果对象中取得 A 存入的数据。

讲一讲Content Provider
不会直接使用ContentProvider类的对象,大多数是通过ContentResolver对象实现操作。
ContentProvider使用URI来唯一标识其数据集。

android常用的五种布局分别是:

FrameLayout、LineaLayout、AbsolutelyLayout、RelationLayout、TableLayout。

FrameLayout:是一种将空间将所有的东西就放置在窗口左上角的布局方式,会出现堆叠的情况。

LineaLayout:线性布局,分为水平 Horizontal 和竖直 vertical两种方式,水平方式表示只有一行,但是可以有N列,一次横向从左向右展开。竖直方向恰好相反,是已属相为基准,只能有一列,但是有N行,分别从上到下依次展开。

AbsolutelyLayout:绝对布局,这种方式是采用控件x,y坐标的形式实现的,这种方式由于在屏幕切换以及多个控件下操作会出错和不容易计算的原因,不经常使用。

RelationLayout:相对布局,这种布局方式是以已存在的某个控件作为基准,从而设置这个当前控件所在位置的方法。

TableLayout:是表格布局,每个TableLayout都有一个自己的TableRow,而每个TableRow都可以设定一个自己的控件。

Android View刷新机制?

在Android的布局体系中,父View负责刷新、布局显示子View;而当子View需要刷新时,则是通知父View来完成。

RelativeLayout和LinearLayout性能比较?

1.RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,也会调用子View2次onMeasure
2.RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
3.在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。

自定义View绘制流程

自定义view是干嘛的呢?
当我们不满足于Android提供的原生控件和布局时,就应该考虑到自定义view。
自定义View分为两大块。
自定义控件 和 自定义容器
自定义View必须重写两个构造方法
第一个是一个参数的上下文,用于在java代码中new对象使用
第二个是两个参数的一个上下文,一个AttributSet。 主要用于在xml中定义使用。
OnMesure 计算出控件的大小。
onLayout 计算出控件的位置。
onDraw 画出样式
ViewGroup\View的绘制流程:
第一步:调用ViewGroup中的onMeasure方法。
在方法中调用了measureChild方法,执行了所有子控件的onMesure方法测绘出所有的子控件的大小。
调用setMeasureDimension方法 设置测绘后的大小。
第二步:调用ViewGroup中的onLayout方法。
在方法调用getChildCount方法 获取到子条目数量。
用for循环遍历出每一个子条目的对象。 通过对象.layout方法 给子控件设置摆放位置。
第三步:首先调用ViewGroup的disPatchDraw方法绘制ViewGroup。然后调用View中的onDraw方 进行绘制。
方法详解:
onMeasure:用于设置自定义view的大小
setMeasuredDimension();
方法内部需要调用MeasureSpec类 可以获取到view的模式 和 大小;
MeasureSpec.getMode()获取模式
MeasureSpec.getSize()获取大小​
模式:
MeasureSpec.EXACTLY 精确值模式: match_parent 或者 固定一个值(。。dp)时使用。
MeasureSpec.AT_MOST 最大值模式: warp_content 当不确定大小时使用。但是不超过父控件
MeasureSpec.UNSPECIFIED 不用 就不总结了。
onDraw方法:
用于绘制自定义View。
主要使用到了Canvas 画布对象。 和Paint 画笔对象 进行的绘制。

Android的View绘制流程

View 绘制中主要流程分为measure,layout, draw 三个阶段
measure :根据父 view 传递的 MeasureSpec 进行计算大小。
layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
draw :把 View 对象绘制到屏幕上。共分为7步,列举5步进行说明:

  1. 第一步:drawBackground(canvas): 作用就是绘制 View 的背景。

  2. 第三步:onDraw(canvas) :绘制 View 的内容。View 的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。

  3. 第三步:dispatchDraw(canvas):遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup 里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View 的绘制过程,基本满足需求。

  4. 第四步:onDrawForeground(canvas):对前景色跟滚动条进行绘制。

  5. 第五步:drawDefaultFocusHighlight(canvas):绘制默认焦点高亮

View超出边界了怎么办:
View的布局继承自定义的这个布局要继承HorizontalScrollView(水平)、ScrollView(垂直)

View的事件分发机制:

事件分为三种ACTION_DOWN、ACTION_MOVE、ACTION_UP。

事件分发的三大方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent

  1. dispatchTouchEvent:用来进行事件的分发。事件传递到当前的View时,会执行此方法,此方法包含了事件分发的逻辑,返回的结果收到当前View的onTouchEvent和下一个View的dispatchTouchEvent影响。
  2. onInterceptTouchEvent:这个方法在dispatchTouchEvent中被调用,用来判断是否拦截某个,如果在同一个事件序列中拦截了某个事件,此方法不会再被调用,返回结果表示是否拦截此事件。只在ViewGroup中使用,View中没有此方法。
  3. onTouchEvent:在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

ViewGroup的分发逻辑:
requestDisallowInterceptTouchEvent方法用于影响父元素的事件拦截策略,requestDisallowInterceptTouchEvent(true),表示不允许父元素拦截事件,这样事件就会传递给子View。一般这个方法子View用的多,可以用来处理滑动冲突问题。

  1. View中没有onInterceptTouchEvent方法,所以一旦事件传递到View,那么View的dispatchTouchEvent方法就会被调用。
  2. dispatchTouchEvent方法中处理事件的逻辑顺序是onTouchListener–>onTouchEvent–>onClickListener。
  3. 也就是说如果View设置了onTouchListener,那onTouchListener的onTouch方法会被调用,如果onTouch方法返回true,那事件就被消耗了,事件分发结束,onTouchEvent不会被调用。
  4. 如果onTouch方法返回false,那么onTouchEvent就会被调用。如果View设置了onClickListener,当ACTION_UP事件到来时,onTouchEvent中的onClickListener的onClick方法也会被调用。
  5. View一般都会消耗事件,如果View没有消耗ACTION_DOWN事件,那后面ACTION_MOVE和ACTION_UP就都不会传递给View。

自定义View如何提供获取View属性的接口

  1. 在你的自定义View里创建一个接口。
  2. 类成员变量里声明一个这个接口的引用。
  3. 写一个方法获取并持有Activity实现的接口的实例
  4. 在Activity里实现这个接口
  5. Activity里绑定XML里的自定义View属性,并向XML创建的自定义View对象传递Activity实现的接口对象。

Activity的四种启动模式对比

  1. Standard:标准的启动模式,如果需要启动一个activity就会创建该activity的实例。也是activity的默认启动模式。
  2. SingeTop:如果启动的activity已经位于栈顶,那么就不会重新创建一个新的activity实例。而是复用位于栈顶的activity实例对象。如果不位于栈顶仍旧会重新创建activity的实例对象。 SingleTask:设置了singleTask启动模式的activity在启动时,如果位于activity栈中,就会复用该activity,这样的话,在该实例之上的所有activity都依次进行出栈操作,即执行对应的onDestroy()方法,直到当前要启动的activity位于栈顶。一般应用在网页的图集,一键退出当前的应用程序,接收推送通知的内容显示页面。
  3. singleInstance:如果使用singleInstance启动模式的activity在启动的时候会复用已经存在的activity实例。不管这个activity的实例是位于哪一个应用当中,都会共享已经启动的activity的实例对象。使用了singlestance的启动模式的activity会单独的开启一个共享栈,这个栈中只存在当前的activity实例对象。适合需要与程序分离开的页面。例如闹铃提醒,将闹铃提醒与闹铃设置分离。
  4. singleTask:
    当活动的启动模式指定为 singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。singleTask适合作为程序入口点

Handler(实现线程之间的通信Android 消息传递机制 / 异步通信机制)

用于解决多线程并发的问题。
handler整个流程中,主要有四个对象,handler,Message,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建handler对象, 我们通过要传送的消息保存到Message中,handler。post,handler通过调用sendMessage方法将Message发送到MessageQueue中,Looper对象就会不断的调用loop()方法 不断的从MessageQueue中取出Message交给handler进行处理。

Looper、MessageQueue、Message、Handler的关系
相当于一家餐厅的整体的运作关系
Handler好比点餐员,
Looper好比后厨厨师长,只有一个
MessageQueue好比订单打单机,只有一个
Message好比一桌一桌的订单

  1. 标准流程
  1. 首先进入一家店,通过点餐员点餐把数据提交到后厨打单机。
  2. 然后厨师长一张一张的拿起订单,按照点餐的先后顺序,交代后厨的厨师开始制作。
  3. 制作好后上菜,并标记已做好的订单。
  1. 特殊流程
    订单为延迟订单,比如客人要求30分钟后人齐了再制作,这时会把该订单按时间排序放到订单队列的合适位置,并通过SystemClock.uptimeMillis()定好闹铃。至于为什么用uptimeMillis是因为该时间是系统启动开始计算的毫秒时间,不受手动调控时间的影响。
    如果打单机中全是延迟订单,则下令给后厨厨师休息,并在门口贴上免打扰的牌子(needWake),等待闹铃提醒,如有新的即时订单进来并且发现有免打扰的牌子,则通过nativeWake()唤醒厨师再开始制作上菜。
    但是为了提升店铺菜品覆盖,很多相邻的店铺都选择合作经营,就是你可以混搭旁边店的餐到本店吃,此时只需点餐员提交旁边店的订单即可,这样旁边店的厨师长就可以通过打单机取出订单并进行制作和上菜。

总结:映射到以上场景中,一家店就好比一个Thread,而一个Thread中可以有多个Handler(点餐员),但只能有一个Looper(厨师长),一个MessageQueue(打单机),和多个Message(订单)。
1、通过Message.obtain()从消息池中获取一个消息对象,给相关属性赋值(what等)
2、Handler对象将消息发送出去(延迟消息是立即发送,延迟处理)
3、发送出去的消息会在MessageQueue里进行排序(按照when属性也就是处理时间排序)
4、Looper会从MessageQueue里取出消息,可能会出现阻塞(消息队列没有消息或者是最近一个消息还没到处理时间),出现阻塞时,Looper陷入沉睡(不浪费内存),到时间了,他会自己唤醒自己
5、Looper取出消息后就会调用Handler对象的dispatchMessage()方法分发消息(处理消息的方式有三种:Message的callback(为Runnable对象),Handler的callback,和Handler的handleMessage,前两种方式优先级比较高,但是很少用,一般为null)
6、Handler对象调用handleMessage方法操作 UI(在主线程)
7、消息处理完后会从消息队列中移除,Looper会负责将消息清理干净(所有属性回归原值)再放入消息池中以备复用
8、发送消息和处理消息是同一个对象

异步消息处理机制-handlerThread

  1. HandlerThread本质上是一个线程类,它继承了Thread
  2. ·HandlerThread有自己的内部Looper对象,可以进行looper循环
  3. ·通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务
  4. ·优点是不会有堵塞,减少了对性能的消耗,缺点是不能同时进行多任务的处理,需要等待进行处理。处理效率较低。
  5. ·与线程池注重并发不同,HandlerThread是一个串行队列,HandlerThread背后只有一个线程。

Handler内存泄漏原因及其解决方案

  1. 原因:
    在Activity中使用非静态内部类初始化了一个Handler,此Handler就会持有当前Activity的引用,要一个对象被回收,那么前提它不被任何其它对象持有引用,所以 当我们Activity页面关闭之后,如果 此时Handler 并没有释放Activity的引用,那么Activity不会被回收,当内存不足时,就会导致内存泄露。
    处。
  2. 解决方案:
  1. 尽可能避免使用Handler做延迟操作
  2. 使用静态内部类继承Hanlder(静态内部类不会持有外部对象的引用),如果我们需要在Handler中 使用外部的Activity时,可以定义一个Activity弱引用(WeakReference)对象,弱引用在第二次GC回收时,可以被回收
  3. 在onDestory 时,清除Handler消息队列中的消息removeCallbacksAndMessages(null)

Post—handler
Handler.post(Runnable)是直接调用了Runable里的run方法、是在主线程运行,并不是在子线程运行。

Handler的post/send()的原理
通过一系列的sendMessageXXX()方法将msg通过消息队列的enqueueMessage()加入到队列中
Handler的post方法发送的是同步消息吗?可以发送异步消息吗?
用户层面发送的都是同步消息 不能发送异步消息 异步消息只能由系统发送

Handler的post()和postDelayed()方法的异同?
底层都是调用的sendMessageDelayed() post()传入的时间参数为0 postDelayed()传入的时间参数是需要的时间间隔。

Handler中Message 消息队列对于延迟消息是如何处理的?
在分发消息(sendMessageDelayed)时,会根据延迟消息整理链表,最终构建出一个时间从小到大的序列,然后接受消息(dispatchMessage)。延迟消息的发送是通过循环遍历,不停的获取当前时间戳来与 msg.when 比较,直到小于当前时间戳为止。

View中的post和handler的post有什么区别?
当项目很小,MainActivity的逻辑也很简单时是看不出啥区别的,但当act的onCreate到onResume之间耗时比较久时(比如2s以上),就能明显感受到这2者的区别了,而且本身它们的实际含义也是很不同的,前者的Runnable真正执行时,可能act的整个view层次都还没完整的measure、layout完成,但后者的Runnable执行时,则一定能保证act的view层次结构已经measure、layout并且至少绘制完成了一次。

性能优化

布局,异步,过渡绘制
如何避免过度绘制

  1. 移除window的背景
  2. 移除控件中不需要的背景
  3. 减少透明度的使用
  4. 使用ConstraintLayout减少布局层级
  5. 使用merge标签减少布局层级
  6. 使用ViewStub标签延迟加载
  7. 少自定义View的过度绘制,使用clipRect()
    布局加载使用异步加载

Fragment的生命周期

Fragment完整的生命周期,相比于Activity的生命周期多了如下几个方法

  1. onAttach():当Fragment与Activity发生关联时,该方法被调用
  2. onCreateView():创建Fragment的视图
  3. onActivityCreate():当Activity的onCreate()返回时被调用
  4. onDestoryView():与onCreateView()是对应的,当Fragment的视图被销毁时,该方法被调用
  5. onDetach(): 与onAttach() 方法相对应,当Fragment与Activity的关联被取消时,该方法被调用
  1. Activity中调用replace()方法时的生命周期
    新替换的Fragment:onAttach > onCreate > onCreateView > onViewCreated > onActivityCreated > onStart > onResume
    被替换的Fragment:onPause > onStop > onDestroyView > onDestroy > onDetach
  2. Activity中调用replace()方法和addToBackStack()方法时的生命周期
    新替换的Fragment(没有在BackStack中):onAttach > onCreate > onCreateView > onViewCreated > onActivityCreated > onStart > onResume
    新替换的Fragment(已经在BackStack中):onCreateView > onViewCreated > onActivityCreated > onStart > onResume
    被替换的Fragment:onPause > onStop > onDestroyView
  3. Fragment在运行状态后跟随Activity的生命周期
    Fragment在上述的各种情况下进入了onResume后,则进入了运行状态,以下4个生命周期方法将跟随所属的Activity一起被调用:
    onPause > onStop > onStart > onResume

为什么要使用Binder?

Linux内核拥有着非常多的跨进程通信机制,如:管道、socket等,较之这些,主要考虑到安全和性能两个方便,才使用了Binder。
移动设备中如果广泛的使用跨进程通信机制肯定会对通信机制提出严格的要求,而Binder相比较传统的进程通信方式更加的高效;由于传统进程通信方式没有对通信的双方和身方做出严格的验证,只有上层协议才会去架构,如socket连接的IP地址可以人为的伪造。而Binder身份校验也是android权限模式的基础。

binder通信模式:
电话基站:binder驱动
通信录:serviceManager

通信流程:
1)在Remote Service端,定义一个TimeBinder实例。TimeBinder继承自ITime.Stub类,而ITime.Stub类继承自Binder同时实现了ITime接口(关于这些类之间的继承关系后面会说)。这个TimeBinder类就是服务端要返回给客户端的东西。
2)TimeBinder被包装成Parcel对象在底层传输(Binder通信在底层依赖Binder驱动);当客户端收到传过来的Parcel对象后将其解包,恢复成IBinder对象。为什么恢复成IBinder对象而不是TimeBinder对象呢?如果熟悉Java泛型的话,就会知道,泛型编程会丢失对象原来的类型信息。在Parcel中处理的都是IBinder类型的对象,所以从Parcel中读出来的也是IBinder类型的对象。
3)调用ITime.Stub的静态方法asInterface(android.os.IBinder obj),将IBinder对象转换成ITime接口对象。asInterface方法原型为
public static ITime asInterface(android.os.IBinder obj)
4)通过转换得到的ITime接口对象,就可以调用ITime接口中定义的方法了。这样就完成了一次远程调用。

MVP 模式

  1. 各部分之间的通信,都是双向的。

  2. View 与 Model 不发生联系,都通过 Presenter 传递。

  3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

MVP的优点
1.模型与视图完全分离

2.P层非常容易(适合)做单元测试,Presenter被抽象成接口,可以有多种具体的实现,所以方便进行单元测试

3.(Presener的复用)一个Presener可以用于多个视图(View),而不需要改变Presenter的逻辑。视图(View)的变化比模型(Model)的变化更频繁的多 ,所以这样超级方便。

4.(View的复用)View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务逻辑完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做高度可复用的View组件。Activity只处理生命周期的任务,代码变得更加简洁

5.可以更高效地使用Model,因为所有的交互都发生在一个地方——Presenter内部

6.视图逻辑和业务逻辑分别抽象到了View和Presenter的接口中去,提高代码的可阅读性

MVP的缺点
1.Presenter层要处理的业务逻辑过多,复杂的业务逻辑会使P层非常庞大和臃肿。

2.Presenter中除了业务逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑(没有做到像MVVM数据同步那样一劳永逸),造成Presenter比较笨重,维护起来会比较困难。

3.接口及接口中声明的方法粒度不好把控。MVP的架构不是固定的,可能会随着实际需求的不同而有不同的改动。P层就是一个变化比较多的地方,P层的意义是使V与M层解耦。如果粒度太小,那么一旦业务多起来,我们的P层会非常臃肿。而如果粒度太大,那么我们一个P层确实可以达到复用,可却导致了我们不同需求的V层复用同一个P层接口时,要实现好多我们不需要的方法,这就是非常典型的违背了接口隔离,接口的实现类不应该实现没有的方法。而其中有些方法是否会用到以及是否会增加或删减还需要后续进一步确认。

4.Activity中需要声明大量跟UI相关的方法,而相应的事件通过Presenter调用相关方法来实现。两者互相引用和调用,存在耦合。一旦View层的UI视图发生改变,接口中的方法就需要改变,View层和P层同时都需要修改。

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。Angular 和 Ember 都采用这种模式。

面试官:Binder有什么优势 小王: 性能方面

共享内存 0次数据拷贝
Binder 1次数据拷贝
Socket/管道/消息队列 2次数据拷贝
稳定性方面

Binder:基于C/S架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好
共享内存:虽然无需拷贝,但是控制复杂,难以使用
从稳定性的角度讲,Binder机制是优于内存共享的。
安全性方面

传统的IPC没有任何安全措施,安全依赖上层协议来确保。
传统的IPC方法无法获得对方可靠的进程用户ID/进程UI(UID/PID),从而无法鉴别对方身份。
传统的IPC只能由用户在数据包中填入UID/PID,容易被恶意程序利用。
传统的IPC访问接入点是开放的,无法阻止恶意程序通过猜测接收方地址获得连接。
Binder既支持实名Binder,又支持匿名Binder,安全性高。
面试官:Binder是如何做到一次拷贝的 小王: 主要是因为Linux是使用的虚拟内存寻址方式,它有如下特性:

用户空间的虚拟内存地址是映射到物理内存中的
对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射
这个内存映射过程是通过系统调用mmap()来实现的 Binder借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝

面试官:MMAP的内存映射原理了解吗 小王: MMAP内存映射的实现过程,总的来说可以分为三个阶段: (一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝 注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

面试官:Binder机制是如何跨进程的 小王: 1.Binder驱动

在内核空间创建一块接收缓存区,
实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区
2.发送进程通过系统调用(copy_from_user)将数据发送到内核缓存区。由于内核缓存区和接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信。

面试官:说说四大组件的通信机制 小王: 1.activity

(1)一个Activity通常就是一个单独的屏幕(窗口)。

(2)Activity之间通过Intent进行通信。

(3)android应用中每一个Activity都必须要在AndroidManifest.xml配置文件中声明,否则系统将不识别也不执行该Activity。

2.service

(1)service用于在后台完成用户指定的操作。service分为两种:

started(启动):当应用程序组件(如activity)调用startService()方法启动服务时,服务处于started状态。
bound(绑定):当应用程序组件调用bindService()方法绑定到服务时,服务处于bound状态。
(2)startService()与bindService()区别:

started service(启动服务)是由其他组件调用startService()方法启动的,这导致服务的onStartCommand()方法被调用。当服务是started状态时,其生命周期与启动它的组件无关,并且可以在后台无限期运行,即使启动服务的组件已经被销毁。因此,服务需要在完成任务后调用stopSelf()方法停止,或者由其他组件调用stopService()方法停止。
使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。
(3)开发人员需要在应用程序配置文件中声明全部的service,使用标签。 (4)Service通常位于后台运行,它一般不需要与用户交互,因此Service组件没有图形用户界面。Service组件需要继承Service基类。Service组件通常用于为其他组件提供后台服务或监控其他组件的运行状态。

3.content provider

(1)android平台提供了Content Provider使一个应用程序的指定数据集提供给其他应用程序。其他应用可以通过ContentResolver类从该内容提供者中获取或存入数据。

(2)只有需要在多个应用程序间共享数据是才需要内容提供者。例如,通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处是统一数据访问方式。

(3)ContentProvider实现数据共享。ContentProvider用于保存和获取数据,并使其对所有应用程序可见。这是不同应用程序间共享数据的唯一方式,因为android没有提供所有应用共同访问的公共存储区。

(4)开发人员不会直接使用ContentProvider类的对象,大多数是通过ContentResolver对象实现对ContentProvider的操作。

(5)ContentProvider使用URI来唯一标识其数据集,这里的URI以content://作为前缀,表示该数据由ContentProvider来管理。

4.broadcast receiver

(1)你的应用可以使用它对外部事件进行过滤,只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力,例如闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

(2)广播接收者的注册有两种方法,分别是程序动态注册和AndroidManifest文件中进行静态注册。

(3)动态注册广播接收器特点是当用来注册的Activity关掉后,广播也就失效了。静态注册无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器也是打开着的。也就是说哪怕app本身未启动,该app订阅的广播在触发时也会对它起作用。

面试官:为什么Intent不能传递大数据

小王: Intent携带信息的大小其实是受Binder限制。数据以Parcel对象的形式存放在Binder传递缓存中。如果数据或返回值比传递buffer大,则此次传递调用失败并抛出TransactionTooLargeException异常。 Binder传递缓存有一个限定大小,通常是1Mb。但同一个进程中所有的传输共享缓存空间。多个地方在进行传输时,即时它们各自传输的数据不超出大小限制,TransactionTooLargeException异常也可能会被抛出。在使用Intent传递数据时,1Mb并不是安全上限。因为Binder中可能正在处理其它的传输工作。不同的机型和系统版本,这个上限值也可能会不同。

Android的三种动画

帧动画(FrameAnimotion)
通过顺序播放一系列预先定义好的图像从而产生动画效果,图片过多时会造成OOM(内存用完)异常。系统提供了AnimotionDrawable类来使用帧动画。

  1. 首先在drawable里创建一个animation-list类型的xml文件;
  2. 设置布局文件activity_frame,定义开始和停止界面的布局
  3. 在Activity中进行布局加载、帧动画资源绑定、事件触发

补间动画又叫View动画(TweenAnimotion)
通过对场景里的对象不断做图像变换(透明度、缩放、平移、旋转)从而产生动画效果,是一种渐进式动画,并且View动画支持自定义;四种变换效果对应Animation的四个子类:TranslateAnimation(位移)、ScaleAnimation(缩放)、RotateAnimation(旋转)、AlphaAnimation(透明),这些动画可以通过XML来定义,也可以通过代码动态定义。

  1. 在res下创建一个anim文件夹,创建alpha类型xml文件用来实现动画透明度;创建scal类型xml文件用来实现缩放动画;创建scal类型xml文件用来实现位移动画;创建rotate类型xml文件用来实现旋转动画;创建set类型xml文件用来实现组合动画(包含上述四个标签)
  2. 创建布局文件
  3. 再Activity中使用AnimationUtils.loadAnimation()方法使用动画效果

属性动画(AccributeAnimotion)
对作用对象进行了扩展,属性动画可以对任何对象做动画,动画的效果也也得到了加强,可以实现更加绚丽的动画效果。

  1. 设置布局文件
  2. 在Activity中引用ObjectAnimator.ofFloat()方法动态设置动画属性

补间动画和属性动画的区别
补间动画只是改变了View的显示效果,并不会真正的改变View的属性,而属性动画可以改变View的显示效果和属性

1.性能优化

1.如何对 Android 应用进行性能分析
android 性能主要之响应速度 和UI刷新速度。

可以参考博客:Android系统性能调优工具介绍

首先从函数的耗时来说,有一个工具TraceView 这是androidsdk自带的工作,用于测量函数耗时的。

UI布局的分析,可以有2块,一块就是Hierarchy Viewer 可以看到View的布局层次,以及每个View刷新加载的时间。

这样可以很快定位到那块layout & View 耗时最长。

还有就是通过自定义View来减少view的层次。

2.什么情况下会导致内存泄露

内存泄露是个折腾的问题。

什么时候会发生内存泄露?内存泄露的根本原因:长生命周期的对象持有短生命周期的对象。短周期对象就无法及时释放。

  1. 静态集合类引起内存泄露

主要是hashmap,Vector等,如果是静态集合 这些集合没有及时setnull的话,就会一直持有这些对象。

2.remove 方法无法删除set集 Objects.hash(firstName, lastName);

经过测试,hashcode修改后,就没有办法remove了。

3.observer 我们在使用监听器的时候,往往是addxxxlistener,但是当我们不需要的时候,忘记removexxxlistener,就容易内存leak。

广播没有unregisterrecevier

4.各种数据链接没有关闭,数据库contentprovider,io,sokect等。cursor

5.内部类:

java中的内部类(匿名内部类),会持有宿主类的强引用this。

所以如果是new Thread这种,后台线程的操作,当线程没有执行结束时,activity不会被回收。

Context的引用,当TextView 等等都会持有上下文的引用。如果有static drawable,就会导致该内存无法释放。

6.单例

单例 是一个全局的静态对象,当持有某个复制的类A是,A无法被释放,内存leak。

3.如何避免 OOM 异常

首先OOM是什么?

当程序需要申请一段“大”内存,但是虚拟机没有办法及时的给到,即使做了GC操作以后

这就会抛出 OutOfMemoryException 也就是OOM

Android的OOM怎么样?

为了减少单个APP对整个系统的影响,android为每个app设置了一个内存上限。

public void getMemoryLimited(Activity context)
{
ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
System.out.println(activityManager.getMemoryClass());
System.out.println(activityManager.getLargeMemoryClass());
System.out.println(Runtime.getRuntime().maxMemory()/(1024*1024));
}
HTC M7实测,192M上限。512M 一般情况下,192M就是上限,但是由于某些特殊情况,android允许使用一个更大的RAM。

如何避免OOM

减少内存对象的占用

1.ArrayMap/SparseArray代替hashmap

2.避免在android里面使用Enum

3.减少bitmap的内存占用

inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。

decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。

4.减少资源图片的大小,过大的图片可以考虑分段加载

内存对象的重复利用

大多数对象的复用,都是利用对象池的技术。

1.listview/gridview/recycleview contentview的复用

2.inBitmap 属性对于内存对象的复用ARGB_8888/RBG_565/ARGB_4444/ALPHA_8

这个方法在某些条件下非常有用,比如要加载上千张图片的时候。

3.避免在ondraw方法里面 new对象

4.StringBuilder 代替+

4.Android 中如何捕获未捕获的异常

CrashHandler

关键是实现Thread.UncaughtExceptionHandler

然后是在application的oncreate里面注册。

5.ANR 是什么?怎样避免和解决 ANR(重要)

ANR->Application Not Responding

也就是在规定的时间内,没有响应。

三种类型:

1). KeyDispatchTimeout(5 seconds) —主要类型按键或触摸事件在特定时间内无响应

2). BroadcastTimeout(10 seconds) —BroadcastReceiver在特定时间内无法处理完成

3). ServiceTimeout(20 seconds) —小概率类型 Service在特定的时间内无法处理完成

为什么会超时:事件没有机会处理 & 事件处理超时

怎么避免ANR

ANR的关键

是处理超时,所以应该避免在UI线程,BroadcastReceiver 还有service主线程中,处理复杂的逻辑和计算

而交给work thread操作。

1)避免在activity里面做耗时操作,oncreate & onresume

2)避免在onReceiver里面做过多操作

3)避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。

4)尽量使用handler来处理UI thread & workthread的交互。

如何解决ANR

首先定位ANR发生的log:

04-01 13:12:11.572 I/InputDispatcher( 220): Application is not responding:Window{2b263310com.android.email/com.android.email.activity.SplitScreenActivitypaused=false}. 5009.8ms since event, 5009.5ms since waitstarted
CPUusage from 4361ms to 699ms ago ----CPU在ANR发生前的使用情况
04-0113:12:15.872 E/ActivityManager( 220): 100%TOTAL: 4.8% user + 7.6% kernel + 87% iowait

04-0113:12:15.872 E/ActivityManager( 220): CPUusage from 3697ms to 4223ms later:-- ANR后CPU的使用量
从log可以看出,cpu在做大量的io操作。

所以可以查看io操作的地方。

当然,也有可能cpu占用不高,那就是 主线程被block住了。

6.Android 线程间通信有哪几种方式

1)共享变量(内存)

2)管道

3)handle机制

runOnUiThread(Runnable)

view.post(Runnable)

7.Devik 进程,linux 进程,线程的区别

Dalvik进程。

每一个android app都会独立占用一个dvm虚拟机,运行在linux系统中。

所以dalvik进程和linux进程是可以理解为一个概念。

8.描述一下 android 的系统架构

从小到上就是:

linux kernel,lib dalvik vm ,application framework, app

9.android 应用对内存是如何限制的?我们应该如何合理使用内存?

activitymanager.getMemoryClass() 获取内存限制。

关于合理使用内存,其实就是避免OOM & 内存泄露中已经说明。

10. 简述 android 应用程序结构是哪些

1)main code

unit test
3)mianifest

4)res->drawable,drawable-xxhdpi,layout,value,mipmap

mipmap 是一种很早就有的技术了,翻译过来就是纹理映射技术.

google建议只把启动图片放入。

5)lib

6)color

11.请解释下 Android 程序运行时权限与文件系统权限的区别
文件的系统权限是由linux系统规定的,只读,读写等。

运行时权限,是对于某个系统上的app的访问权限,允许,拒绝,询问。该功能可以防止非法的程序访问敏感的信息。

12.Framework 工作方式及原理,Activity 是如何生成一个 view 的,机制是什么

Framework是android 系统对 linux kernel,lib库等封装,提供WMS,AMS,bind机制,handler-message机制等方式,供app使用。

简单来说framework就是提供app生存的环境。

1)Activity在attch方法的时候,会创建一个phonewindow(window的子类)

2)onCreate中的setContentView方法,会创建DecorView

3)DecorView 的addview方法,会把layout中的布局加载进来。

13.多线程间通信和多进程之间通信有什么不同,分别怎么实现

线程间的通信可以参考第6点。

进程间的通信:bind机制(IPC->AIDL),linux级共享内存,boradcast,

Activity 之间,activity & serview之间的通信,无论他们是否在一个进程内。

14.Android 屏幕适配

屏幕适配的方式:xxxdpi, wrap_content,match_parent. 获取屏幕大小,做处理。

dp来适配屏幕,sp来确定字体大小

drawable-xxdpi, values-1280*1920等 这些就是资源的适配。

wrap_content,match_parent, 这些是view的自适应

weight,这是权重的适配。

15.什么是 AIDL 以及如何使用

Android Interface Definition Language

AIDL是使用bind机制来工作。

参数:

java原生参数

String

parcelable

list & map 元素 需要支持AIDL

一、anr

1、什么是anr
在 Android 系统中,如果应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应用程序无响应(ANR:ApplicationNotResponding)对话框。 用户可以选择让程序继续运行,但是,他们在使用你的应用程序时,并不希望每次都要处理这个对话框。因此 ,在程序里对响应性能的设计很重要,这样系统就不会显示 ANR 给用户。

2、ANR的触发

1、Activity、BroadCastReceiver、Service触发ANR的时间
Android 系统会监控程序的响应状况,不同的组件发生 ANR 的时间不一样:

2、引起ANR的原因
主线程被 IO 操作(从 4.0 之后网络 IO 不允许在主线程中)阻塞;
主线程中存在耗时的计算;
主线程中错误的操作,比如 Thread.wait 或者 Thread.sleep 等。
3、ANR信息查看
如果开发机器上出现问题,我们可以查看/data/anr/traces.txt,最新的 ANR 信息在最开始部分。

3、如何解决
1、使用 AsyncTask 处理耗时 IO 操作。
2、使用 Thread 或者 HandlerThread 时,要设置线程优先级。未调Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,仍然会降低程序响应,因为默认 Thread 的优先级和主线程相同。
3、使用 Handler 处理工作线程结果,而不是使用 Thread.wait()或者 Thread.sleep() 来阻塞主线程。
4、Activity 的 onCreate 和 onResume 回调中尽量避免耗时的代码。
5、BroadcastReceiver 中 onReceive 代码也要尽量减少耗时,建议使用 IntentService 处理。

二、oom

1、什么是OOM
移动端内存有限,手机给每个应用分配大小有限(Google 源生OS分配的内存为16M或者24M,但是不同厂家的ROM会修改)。当你使用的内存空间接近阀值,实例化新对象,需要分配新的内存空间是。就会报Out of Memory。

2、OOM解决方法
加载大量图片

Bitmap压缩。
缓存。
软引用&弱引用。
替换.png图片。
内存泄露

1.静态变量导致内存泄露。
如一个静态变量持有当前Activity对象(但是很少有人会这么干吧)。

2.单例模式导致的内存泄露。

单例模式的特点是它的生命周期与Application一致。所以单例模式实例化对象时,要用Application.context。

3.非静态内部类和匿名内部类导致的内存泄露。
非静态内部类和匿名内部类隐式持有外部类的引用。Handler经常会不注意的时候写成匿名内部类,就造成内存泄露。

4.资源未关闭导致的内存泄露。
资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,不使用的时候,应该及时关闭它们,否则会造成内存泄漏。

5.属性动画导致的内存泄露。
属性动画中的无限循环动画,如果没有在onDestroy中停止,尽管界面上看不到动画效果,但是Activity还是被View持有,会导致内存泄露。

6.集合容器导致内存泄露。
当不需要对象时,并没有把它的引用从集合中清理掉,也是一种内存泄露。

一、简述一下OkHttp

OkHttp是一个非常优秀的网络请求框架,已被谷歌加入到Android的源码中。目前比较流行的Retrofit也是默认使用OkHttp的。

1、支持http2,对一台机器的所有请求共享同一个socket
2、内置连接池,支持连接复用,减少延迟
3、支持透明的gzip压缩响应体
GZIP是网站压缩加速的一种技术,对于开启后可以加快我们网站的打开速度,原理是经过服务器压缩,客户端浏览器快速解压的原理,可以大大减少了网站的流量

开GZIP有什么好处?Gzip开启以后会将输出到用户浏览器的数据进行压缩的处理,这样就会减小通过网络传输的数据量,提高浏览器启动页面的速度。

4、通过缓存避免重复的请求
强制缓存

客户端第一次请求数据时,服务端返回缓存的过期时间(通过字段 Expires 与 Cache-Control 标识),后续如果缓存没有过期就直接使用缓存,无需请求服务端;否则向服务端请求数据。

对比缓存
对比缓存时,每次请求都需要与服务器交互,由服务端判断是否可以使用缓存。

5、请求失败时自动重试主机的其他ip,自动重定向
6、好用的API

二、看过OkHttp的源码吗,简单说一下

第一,通过一个构建者模式(Request.Builder)构建所有的request,然后分发到Dispatcher(分发器);
第二,Dispatcher再把request分发到HttpEngine(真正干活的类)中,HttpEngine首先要看一下本次请求有没有cache(缓存),如果有缓存,就从缓存中拿到信息,然后返回给response;如果没有缓存,HttpEngine就把request分发到ConnectionPool(连接池)中;
第三,在ConnectionPool(连接池)中,通过Connection发送请求,首先选择Route(路由)和Platfrom(平台),然后到达Server(Socket),获取到Data,然后返回response。

三、OkHttp的使用

1、创建OkHttpClient对象:OkHttpClient client = new OkHttpClient();
2、创建网络请求:Request request = new Request.Builder() .url(“http://sethfeng.github.io/index.html”) .build();
3、得到Call对象:Call call = client.newCall(request); //实际创建的是一个RealCall对象,RealCall中有一个对client对象的引用
4、发送请求,获取返回的数据
发送同步请求:Response response = call.excute();
发送异步请求:
call.enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {

}
@Override
public void onResponse(Response response) throws IOException {

}
});

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
面试是评估一个人技能和能力的重要环节。在Android面试中,除了基础知识的掌握外,还需要注意交流和语言组织能力。基础知识的牢固掌握是面试的基础,但有些人可能在实际表达中存在困难。因此,在准备面试时,可以使用思维导图等工具来梳理知识点,帮助自己更好地组织语言。从简到繁、从外到内的方式可以帮助我们更好地展开回答。生成思维导图后,可以按照这个体系一条条地进行讲解,并在每个点上进行延伸,这样可以延长自己的说话时间,提高通过面试的概率。\[1\]\[2\] 在Android中,进程之间是不能互相访问的,因此需要使用多进程通信技术。Android中特有的多进程通信技术是Binder。通常情况下,一个应用是一个进程,但是Android中一个应用也可以有多个进程,可以通过指定android:process属性来给四大组件指定进程。其中以“:”开头的为私有进程,不以“:”开头的为共有进程。\[3\] 在Android面试中,除了基础知识和多进程通信,还有其他重要的话题,如Android架构、UI设计、性能优化等。准备面试时,建议全面了解这些话题,并能够清晰地表达自己的观点和经验。 #### 引用[.reference_title] - *1* *2* *3* [android面试实用篇](https://blog.csdn.net/wang_yong_hui_1234/article/details/105579401)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值