安卓面试题(持续更新)

二、Android

Activity

1.说下Activity的生命周期?

正常情况下创建一个Activity的生命周期:onCreate onStart onResume onPause onStop onDestroy

当使用SingleTask启动一个已经存在的Activity不会再次调用onCreate方法,而是调用onNewIntent方法,新传递的参数会携带在这个回调方法的Intent中,需要使用setIntent()给当前Activity设置新的Intent,否则后续通过getIntent获取的Intent将是原来的Intent。

2.Activity A启动另一个Activity B会回调哪些方法?如果Activity B是完全透明呢?如果启动的是一个Dialog呢?再从B跳回A呢?再按Home键呢?

启动一个透明的Activity会回调原来Activity的onPause方法,但不会回调onStop。启动Dialog并不会改变Activity的生命周期。如果是以dialog的形式启动Activity则和启动透明的Activity一样

3.谈谈onSaveInstanceState()方法?何时会调用?

1、当用户按下HOME键时。

这是显而易见的,系统不知道你按下HOME后要运行多少其他的程序,自然也不知道activity A是否会被销毁,故系统会调用onSaveInstanceState,让用户有机会保存某些非永久性的数据。以下几种情况的分析都遵循该原则

2、长按HOME键,选择运行其他的程序时。

3、按下电源按键(关闭屏幕显示)时。

4、从activity A中启动一个新的activity时。

5、屏幕方向切换时,例如从竖屏切换到横屏时。

在屏幕切换之前,系统会销毁activity A,在屏幕切换之后系统又会自动地创建activity A,所以onSaveInstanceState一定会被执行

总而言之,onSaveInstanceState的调用遵循一个重要原则,即当系统“未经你许可”时销毁了你的activity,则onSaveInstanceState会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据(当然你不保存那就随便你了)。 ---------重点在于这句

4.onSaveInstanceState()与onPause()的区别?

onPause():Activity的生命周期中的一个阶段,当Activity退出前台不再和用户进行交互时调用,可以用来保存一些持久化的数据。

onSaveInstanceState():帮助Activity保存临时的数据,比如各种UI状态。

联系与区别:两个方法都能够保存数据,一般onPause()用于保存持久化数据,比如向数据库写入数据,而onSaveInstanceState()用于保存临时数据。

注:当某个Activity正常结束生命周期,比如用户按下Back键调用onDestroy()方法时,onSaveInstanceState()方法不会被调用。其他情况下,比如按下Home键或者跳转到其他Activity,或者当某个Activity由于内存不足等原因被系统杀死时,就会调用onSaveInstanceState()方法。该方法是在onStop()方法之前被调用(与onPause()方法没有前后顺序),也就是说在Activity调用onStop()方法变得不可见之前就会调用onSaveInstanceState()保存Activity的UI信息,当该Activity被系统杀死后重新启动时,就会在onCreate()方法中取出onSaveInstanceState()保存的数据重新加载到UI上。

5.如何避免配置改变时Activity重建?

主要考虑屏幕方向发生改变时,当屏幕发生改变时,例如:手机旋转屏幕时 Activity不被创建

在AndroidManifest.xml文件中添加 android:configChanges="orientation|screenSize"

6.优先级低的Activity在内存不足被回收后怎样做可以恢复到销毁前状态?

先在onSaveInstanceState()方法中对需要保存的数据进行保存,再在onRestoreInstanceState()或者在onCreate()中取bundle数据,onCreate()中取数据记得非空判断

7.说下Activity的四种启动模式?(有时会出个实际问题来分析返回栈中Activity的情况)

standard 标准模式,不写默认这个,不管栈中没有当前页面都重新创建新的页面;

singleTop 栈顶模式, 栈顶不重复,可以防止重复点击相同按钮打开两个相同页面;

singleTask 单例模式,堆栈不重复,栈内始终只有当前一个页面;

singleInstance 开新栈,添加一个页面就要开一个新栈;

8.onNewIntent()调用时机?

当打开一个页面,这个页面存在并正在运行时就会执行onNewIntent()方法接收数据。

9.了解哪些Activity启动模式的标记位?

FLAG_ACTIVITY_NEW_TASK在一个新的task中开启一个activity。如果包含该activity的task已经运行,该task就回到前台,activity通过onNewIntent()接受处理该intent。

这是与"singleTask"登录模式相同的行为。

FLAG_ACTIVITY_SINGLE_TOP果要被开启的activity是当前的activity(在返回栈的顶部),已经存在的实例通过onNewIntent()接收一个调用,然后处理该intent,而非重新创建一个新的实例。

这与"singleTop"登录模式有相同的行为。

FLAG_ACTIVITY_CLEAR_TOP如果要被开启的activity已经在当前的task中运行,系统不会生成该activity的一个新的实例,在该栈顶部的所有其他的activity会被销毁,这个intent通过 onNewIntent()被传递给该重新运行的activity的实例(现在在栈顶部)。

10.如何修改Activity的出现动画

可以通过两种方式,一是通过定义 Activity 的主题,二是通过覆写 Activity 的 overridePendingTransition 方法。

1:通过设置主题样式

在 styles.xml 中编辑如下代码:

@anim/slide_in_left

@anim/slide_out_left

@anim/slide_in_right

@anim/slide_out_right

2:添加 themes.xml 文件:

@style/AnimationActivity

true

在 AndroidManifest.xml 中给指定的 Activity 指定 theme。

覆写 overridePendingTransition 方法

overridePendingTransition(R.anim.fade, R.anim.hold);

fragment

1.fragment的生命周期

image

onAttach:onAttach()在fragment与Activity关联之后调调查用。需要注意的是,初始化fragment参数可以从getArguments()获得,但是,当Fragment附加到Activity之后,就无法再调用setArguments()。所以除了在最开始时,其它时间都无法向初始化参数添加内容。

onCreate:fragment初次创建时调用。尽管它看起来像是Activity的OnCreate()函数,但这个只是用来创建Fragment的。此时的Activity还没有创建完成,因为我们的Fragment也是Activity创建的一部分。所以如果你想在这里使用Activity中的一些资源,将会获取不到。比如:获取同一个Activity中其它Frament的控件实例。(代码如下:),如果想要获得Activity相关联的资源,必须在onActivityCreated中获取。

onCreateView:在这个fragment构造它的用户接口视图(即布局)时调用。

onActivityCreated:在Activity的OnCreate()结束后,会调用此方法。所以到这里的时候,Activity已经创建完成!在这个函数中才可以使用Activity的所有资源。如果把下面的代码放在这里,获取到的btn_Try的值将不会再是空的!

onStart:当到OnStart()时,Fragment对用户就是可见的了。但用户还未开始与Fragment交互。在生命周期中也可以看到Fragment的OnStart()过程与Activity的OnStart()过程是绑定的。意义即是一样的。以前你写在Activity的OnStart()中来处理的代码,用Fragment来实现时,依然可以放在OnStart()中来处理。

onResume:当这个fragment对用户可见并且正在运行时调用。这是Fragment与用户交互之前的最后一个回调。从生命周期对比中,可以看到,Fragment的OnResume与Activity的OnResume是相互绑定的,意义是一样的。它依赖于包含它的activity的Activity.onResume。当OnResume()结束后,就可以正式与用户交互了。

onPause:此回调与Activity的OnPause()相绑定,与Activity的OnPause()意义一样。

onStop:这个回调与Activity的OnStop()相绑定,意义一样。已停止的Fragment可以直接返回到OnStart()回调,然后调用OnResume()。

onDestroyView:如果Fragment即将被结束或保存,那么撤销方向上的下一个回调将是onDestoryView()。会将在onCreateView创建的视图与这个fragment分离。下次这个fragment若要显示,那么将会创建新视图。这会在onStop之后和onDestroy之前调用。这个方法的调用同onCreateView是否返回非null视图无关。它会潜在的在这个视图状态被保存之后以及它被它的父视图回收之前调用。

onDestroy:当这个fragment不再使用时调用。需要注意的是,它即使经过了onDestroy()阶段,但仍然能从Activity中找到,因为它还没有Detach。

onDetach:Fragment生命周期中最后一个回调是onDetach()。调用它以后,Fragment就不再与Activity相绑定,它也不再拥有视图层次结构,它的所有资源都将被释放。

推荐文章https://www.jianshu.com/p/9f538c3a1918

2.Activity和fragment的通信

1. 接口回调。

2. 通过设置fragment的argument。

3. 通过getActivity()调用activity的公共方法。

4. 通过Intent启动activity 附加信息。

fragment之间的通信

1. 通过寄宿的activity获取另一个fragment的实例并调用其方法。

2. 接口回调

3. 使用广播

Service

1.谈一谈Service的生命周期?

image

这里我们总结一下:

1). 被启动的服务的生命周期:如果一个Service被某个Activity 调用 Context.startService 方法启动,那么不管是否有Activity使用bindService绑定或unbindService解除绑定到该Service,该Service都在后台运行。如果一个Service被startService 方法多次启动,那么onCreate方法只会调用一次,onStart将会被调用多次(对应调用startService的次数),并且系统只会创建Service的一个实例(因此你应该知道只需要一次stopService调用)。该Service将会一直在后台运行,而不管对应程序的Activity是否在运行,直到被调用stopService,或自身的stopSelf方法。当然如果系统资源不足,android系统也可能结束服务。

2). 被绑定的服务的生命周期:如果一个Service被某个Activity 调用 Context.bindService 方法绑定启动,不管调用 bindService 调用几次,onCreate方法都只会调用一次,同时onStart方法始终不会被调用。当连接建立之后,Service将会一直运行,除非调用Context.unbindService 断开连接或者之前调用bindService 的 Context 不存在了(如Activity被finish的时候),系统将会自动停止Service,对应onDestroy将被调用。

3). 被启动又被绑定的服务的生命周期:如果一个Service又被启动又被绑定,则该Service将会一直在后台运行。并且不管如何调用,onCreate始终只会调用一次,对应startService调用多少次,Service的onStart便会调用多少次。调用unbindService将不会停止Service,而必须调用 stopService 或 Service的 stopSelf 来停止服务。

4). 当服务被停止时清除服务:当一个Service被终止(1、调用stopService;2、调用stopSelf;3、不再有绑定的连接(没有被启动))时,onDestroy方法将会被调用,在这里你应当做一些清除工作,如停止在Service中创建并运行的线程。

特别注意:

1、你应当知道在调用 bindService 绑定到Service的时候,你就应当保证在某处调用 unbindService 解除绑定(尽管 Activity 被 finish 的时候绑定会自      动解除,并且Service会自动停止);

2、你应当注意 使用 startService 启动服务之后,一定要使用 stopService停止服务,不管你是否使用bindService;

3、同时使用 startService 与 bindService 要注意到,Service 的终止,需要unbindService与stopService同时调用,才能终止 Service,不管 startService 与 bindService 的调用顺序,如果先调用 unbindService 此时服务不会自动终止,再调用 stopService 之后服务才会停止,如果先调用 stopService 此时服务也不会终止,而再调用 unbindService 或者 之前调用 bindService 的 Context 不存在了(如Activity 被 finish 的时候)之后服务才会自动停止;

4、当在旋转手机屏幕的时候,当手机屏幕在“横”“竖”变换时,此时如果你的 Activity 如果会自动旋转的话,旋转其实是 Activity 的重新创建,因此旋转之前的使用 bindService 建立的连接便会断开(Context 不存在了),对应服务的生命周期与上述相同。

5、在 sdk 2.0 及其以后的版本中,对应的 onStart 已经被否决变为了 onStartCommand,不过之前的 onStart 任然有效。这意味着,如果你开发的应用程序用的 sdk 为 2.0 及其以后的版本,那么你应当使用 onStartCommand 而不是 onStart。

生命周期方法说明

onStartCommand()

当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果您实现此方法,则在服务工作完成后,需要由您通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。)

onBind()

当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,供客户端用来与服务进行通信。请务必实现此方法,但如果您并不希望允许绑定,则应返回 null。

onCreate()

首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已在运行,则不会调用此方法。

onDestroy()

当服务不再使用且将被销毁时,系统将调用此方法。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等。 这是服务接收的最后一个调用。

2.Service的两种启动方式?区别在哪?

一、Service第一种启动方式startService

生命周期:oncreate——>onstartCommand——>onDestroy;多次通过该方法启动Service,oncreate函数只会被调用一次,onStartCommand函数会被多次调用,但只需要调用一次stopService就可以销毁该Service;

特点说明:通过该启动方式启动Service,一旦Service启动成功那么该Service就会和他所创建的Activity脱离关系,也就是说Service的不会随着Activity的销毁而销毁,并且在应用的任何Activiy中都可以对该Service进行操作。

二、Service第二种启动方式bindService

生命周期:oncreate——>onBind——>unBind——>OnDestroy;多次bindService,oncreate和onBind方法都只会调用一次,而且只需要一次调用unBindService用于取消bind;

特点说明:它与startService正好相反,一个Activity通过bind的方式启动Service,那么该Service是依赖于该Activity存在的,也就是Activity的销毁也会导致bind所启动的Service的销毁(会经历unbind——>onDestroy的生命周期)

3.一个Activty先start一个Service后,再bind时会回调什么方法?此时如何做才能回调Service的destory()方法?

会回调onBind()绑定服务的方法,如果想要回调Service的destory()方法,需要同时调用stopService()方法和unBindService()方法

4.Service如何和Activity进行通信?

1、通过bindService(),并通过onbind()获取相应的Ibinder对象实例。

2、通过广播,service发送广播,在activit一种进行接收广播数据。

5.用过哪些系统Service?

ActivityManagerService: 系统管理服务

WindowManager: 窗口管理服务

LayoutInflater: 获取xml里面定义的view

AlarmManager:时钟服务

PowerManager: 电源服务

KeyboadManager: 键盘管理服务

6.是否能在Service进行耗时操作?如果非要可以怎么做?

Service 服务是运行在主线程中的,因此不能执行耗时操作;如果非要执行耗时操作,可以在服务中新开线程,如IntentService就是专门处理耗时的服务的,里面通过一个队列来管理每条任务,每条任务都在自己的子线程中执行,使用IntentService时,只需要继承IntentService,重写onHandleIntent()就行了

7.AlarmManager能实现定时的原理?

通过调用AlarmManager的set()方法就可以设置定时任务,里面又几个参数,PendingIntent 比较重要,这个就是我们需要广播的内容封装。然后通过广播接收器onRecive()接收,这样不断的启动服务和广播接收实现定时操作

8.前台服务是什么?和普通服务的不同?如何去开启一个前台服务?

前台服务是用户看得见的服务,而普通服务一半默默的在后台运行,比如通知栏的通知Notification就是一个前台服务,通过startForeground() 开启一个前台服务

9.是否了解ActivityManagerService,谈谈它发挥什么作用?

ActivityManagerService是一个核心系统服务,负责四大组件的、启动、调度、切换以及进程间管理和调度工作。

关于AMS详细解析https://blog.csdn.net/xiangzhihong8/article/details/79986612

BroadcastReceicer

1.广播有几种形式?什么特点?

在Android自定义的广播中分为无序广播有序广播。

无序广播:

 发送方式:通过sendBroadcast(intent)发送

 无序广播类似于电视台播放新闻联播,不管你当时有没有准时收看,都会按时播放新闻联播

 特点:

      1、无法终止广播 

      2、无法修改数据

有序广播:

 发送方式:通过sendOrderedBroadcast()发送

 有序广播就类似于中央发送的红头文件,比如说会首先发送到哪个省,然后发送到哪个市等等,按照优先级一级一级的进行接收。

 特点:

    1、可以终止广播

    2、可以修改数据

2.广播的两种注册形式?区别在哪?

在Android手机应用程序中开发中,需要用到BroadcastReceiver来监听广播的消息。在自定义好BroadcastReceiver,需要对其进行注册,注册有两种方法,一种是在代码当中注册,注册的方法是registerReceiver(receiver,filter)(用Activity的实例来调用),取消注册的方法:unregisterReceiver(receiver),如果一个BroadcastReceiver用于更新UI(UserInterface),那么通常会使用这种方法进行注册,在Activity启动的时候进行注册,在Activity不可见后取消注册;另一种就是在AndroidManifest当中进行注册。

区别:

   在AndroidManifest中进行注册后,不管改应用程序是否处于活动状态,都会进行监听,比如某个程序时监听 内存的使用情况的,当在手机上安装好后,不管改应用程序是处于什么状态,都会执行改监听方法中的内容。

   在代码中进行注册后,当应用程序关闭后,就不再进行监听。我们读知道,应用程序是否省电,决定了该应用程序的受欢迎程度,所以,对于那些没必要在程序关闭后仍然进行监听的Receiver,在代码中进行注册,无疑是一个明智的选择。

3.本地广播

LocalBroadcastManager用于应用内部传递消息,比broadcastReceiver更加安全高效。

使用:

localBroadcastManager = LocalBroadcastManager.getInstance(this);

    Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");

    localBroadcastManager.sendBroadcast(intent);

另外需要注意的一点是,本地广播是无法通过静态注册的广播接收器来接收的,因为静态注册主要是为了让程序能够在未启动的情况下也能收到广播,而能够发送本地广播的时候,程序肯定是已经启动了。

ContentProvider

1.ContentProvider了解多少?

推荐文章https://blog.csdn.net/carson_ho/article/details/76101093

数据存储

1.Android中提供哪些数据持久存储的方法?

① 使用SharedPreferences存储数据

② 文件存储数据

③  SQLite数据库存储数据

④ 使用ContentProvider存储数据

⑤ 网络存储数据

Android提供了一种方式来暴露你的数据(甚至是私有数据)给其他应用程序 - ContentProvider。它是一个可选组件,可公开读写你应用程序数据。

2.Java中的I/O流读写怎么做?

流就是程序和设备之间嫁接起来的一根用于数据传输的管道,这个管道上有很多按钮,不同的按钮可以实现不同的功能。

这根用于数据传输的管道就是流,流就是一根管道

流的分类和使用:

四大基本抽象流,文件流,缓冲流,转换流,数据流,Print流,Object流。

JAVA.io 包中定义了多个流类型(类或抽象类)来实现输入/输出功能;可以从不同角度对其进行分类:

*按数据流的方向不用可以分为输入流和输出流

*按处理数据单位不同可以分为字节流和字符流

*按照功能不同可以分为节点流和处理流

关于java流的使用https://blog.csdn.net/sinat_33921105/article/details/81081452

3.SharePreferences适用情形?使用中需要注意什么?

1.SharedPreferences是Android平台上一个轻量级的存储辅助类,特别适合保存软件的配置参数。它提供了string,set,int,long,float,boolean六种数据类型,它是以key-value的形式保存在 data/data/<packagename>/shared_prefs 下的xml文件中。

通过 getSharePreferences(String name,int mode)获取sharedpreferences对象

常用的文件操作模式

MODE_PRIVATE:指定该sharepreferences中的数据只能被本应用程序读写

MODE_APPEND:该文件的内容可以追加

获取编剧器 Editor editor = sp.edit();

注意:

1.sharedpreferences是单例形式存在

2.创建sp是将文件内容全部载入内存中

3.单进程下读取安全

4用apply()异步提交方法代替commit

5.因为sp的数据常驻内存所以不适合存储过大数据

6.只能存储 float,int,long,boolean,string,stringset 类型

4.了解SQLite中的事务处理吗?是如何做的?

使用SQLiteDatabase的beginTransaction()方法可以开启一个事务,程序执行到endTransaction() 方法时会检查事务的标志是否为成功,如果程序执行到endTransaction()之前调用了setTransactionSuccessful() 方法设置事务的标志为成功则提交事务,如果没有调用setTransactionSuccessful() 方法则回滚事务。多用于大量数据操作时,能明显减少耗时。

5.使用SQLite做批量操作有什么好的方法吗?

使用事务处理进行优化,默认SQLite的数据库插入操作,如果没有采用事务的话,它每次写入提交,就会触发一次事务操作,而这样几千条的数据,就会触发几千个事务的操作,这就是时间耗费的根源

6.如果现在要删除SQLite中表的一个字段如何做?

sqlite中是不支持删除列操作的,所以网上alter table table_name drop column col_name这个语句在sqlite中是无效的,而替代的方法可以如下:

1.根据原表创建一张新表

2.删除原表

3.将新表重名为旧表的名称

7.使用SQLite时会有哪些优化操作?

创建索引

索引有助于加快 SELECT 查询和 WHERE 子句,但它会减慢使用 UPDATE 和 INSERT 语句时的数据输入。索引可以创建或删除,但不会影响数据。

优点

加快了数据库检索的速度,包括对单表查询、连表查询、分组查询、排序查询。经常是一到两个数量级的性能提升,且随着数据数量级增长。

缺点

索引的创建和维护存在消耗,索引会占用物理空间,且随着数据量的增加而增加。

在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。

使用场景

当某字段数据更新频率较低,查询频率较高,经常有范围查询(>, <, =, >=, <=)或order by、group by发生时建议使用索引。并且选择度越大,建索引越有优势,这里选择度指一个字段中唯一值的数量/总的数量。

经常同时存取多列,且每列都含有重复值可考虑建立复合索引

索引的创建和维护存在消耗,索引会占用物理空间,且随着数据量的增加而增加。

在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。

什么情况下要避免使用索引

虽然索引的目的在于提高数据库的性能,但这里有几个情况需要避免使用索引。使用索引时,应重新考虑下列准则:

索引不应该使用在较小的表上。

索引不应该使用在有频繁的大批量的更新或插入操作的表上。

索引不应该使用在含有大量的 NULL 值的列上。

索引不应该使用在频繁操作的列上。

使用事务

特点

原子性操作,要么全部成功,要么全部失败;在执行大量数据的插入操作时,避免频繁操作cursor,可以大幅减少insert操作时间,一般为1-2个量级

查询必须字段

查询时只取需要的字段和结果集,更多的结果集会消耗更多的时间及内存,更多的字段会导致更多的内存消耗。

关于数据库优化https://www.trinea.cn/android/database-performance/

View

1.自定义View,基本流程

自定义View大家都懂,基本流程自己也能说得差不多。参考链接https://www.jianshu.com/p/cccbb3f1d75d

2.事件分发机制

基本会遵从 Activity => ViewGroup => View 的顺序进行事件分发,Activity的事件事实上也是调用它内部的ViewGroup的事件分发,可以直接当成ViewGroup处理,View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候就把内部的ViewGroup当成View来分析,先分析ViewGroup的处理流程:首先得有一个结构模型概念:ViewGroup和view组成了一颗树形结构,最顶层为Activity的ViewGroup,下面有若干的ViewGroup节点,每个节点下面又有若干的ViewGroup节点或者view节点,依次类推。当一个Touch事件到达根结点,它会依次下发,下发的过程是调用子View(ViewGroup)的dispatchTouchEvent方法实现的,简单来说,就是ViewGroup遍历它包含的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup并且不是最底层的元素时,又会通过调用ViewGrop的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法,dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回true时,顺序下发会中断。
1.Touch事件分发中包含有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent,dispatchTouchEvent,onTouchEvent三个相关事件。View包含dispatchTouchEvent,onTouchEvent两个相关事件,其中,ViewGroup是继承于View的。
2.ViewGroup和View组成一个树状结构,根节点为Activity内部的ViewGroup。
3.触摸事件由Action_down,Action_move,Action_up组成,一次完整的触摸事件中,down和up都只有一个,move可以有若干个,也可能是0个。
4.当Activity接收到Touch事件时,将遍历子View进行down事件的分发,ViewGroup的遍历可以看成是递归的,源码中使用组合模式处理ViewGroup和View的调用处理,分发的目的是找到真正要处理本次事件的View,这个View会在onTouchEvent结果返回true。
5.当某个子View返回true时,会终止Down事件的分发,同时ViewGroup中记录该子View.接下去的move和up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会时真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-textView的结构中,textView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true被保存在ViewGroup0中。当move和up事件来临时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至textView。
6.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发方式是调用super.dispatchTouchEvent函数,级父类View的dispatchTouchEvent方法,把自身当成View来处理,在所有子View都不处理的情况下,触发Activity的onTouchEvent方法。
7.onInterceptTouch有两个作用 ①拦截Down事件的分发,②终止up和move事件向目标View传递,使得目标View所在的ViewGroup捕获up和move事件。

要想彻底回答View的事件分发机制,必须回归源码,通过源码讲解。网上有很多优秀的文章,想要彻底弄明白需要花费一些功夫,推荐文章https://www.jianshu.com/p/e6ceb7f767d8

3.如何解决滑动冲突

解决冲突可以从两方面着手:

外部view拦截:如果外部viewGroup需要滑动则调用onInterceptTouchEvent并在内部做相应的拦截,返回true让viewGroup的onTouchEvent()消费事件。其伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {// TODO Auto-generated method stub

boolean result=false;

switch(ev.getAction()){

case MotionEvent.ACTION_DOWN: //getX()表示以该控件的左上角为0点 //getRawX()表示以屏幕的左上角为0点firstX=(int) ev.getX();firstY=(int) ev.getY();

break;

case MotionEvent.ACTION_MOVE://手指在屏幕上水平移动的绝对值

int disX=(int) Math.abs(ev.getX()-firstX);//手指在屏幕上竖直移动的绝对值

int disY=(int) Math.abs(ev.getY()-firstY);

if(disX>disY&&disX>10){

result=true;

}

break;

case MotionEvent.ACTION_UP:

break;

}

return result;

}

内部view拦截:父容器不拦截任何事件,传递给子view如果需要就进行拦截,这种就需要我们在子view通知父容器不要拦截当前我需要的事件,通知方式为requestDisallowInterceptTouchEvent()

public boolean dispatchTouchEvent(MotionEvent event){

    switch(event.getAction()){

     case MotionEvent.ACTION_DOWN:

         //父控件就不会拦截ListView的滑动事件

         parent.requestDisallowInterceptTouchEvent(true);

         break;

     case MOtionEvent.ACTION_MOVE:

        if(父容器需要此类点击事件){

          parent.requestDisallowInterceptTouchEvent(false);

        }

        break;

     }

     return super.dispatchTouchEvent(event);

  }

4.MeasureSpec是什么?有什么作用?

Android View 的测量过程中使用到了MeasureSpec,正如其字面意思所表达的那个-“测量规格”。View根据该规格从而决定自己的大小。MeasureSpec由俩部分组成,一部分是SpecMode(测量模式),另一部分是SpecSize(规格大小)。View的MeasureSpec由父容器和自己布局参数共同决定。

MeasureSpec是由一个32位 int 值来表示的。其中该 int 值对应的二进制的高2位代表SpecMode,低30位代表SpecSize。这种方法设计很巧妙,减少了空间占用。

SpecMode 测量模式

int mode = MeasureSpec.getMode(measureSpec);

measureSpec一共有三种模式,分别为UNSPECIFIED EXACTLY AT_MOST,下面分别做一下介绍

MeasureSpec.EXACTLY:在此模式下,父容器已经检测出子view所需要的精确大小,布局文件中直接指定宽高具体大小和match_parent对应的就是这个值,这个时候,view的测量大小就是通过getSize得到的数值。

MeasureSpec.AT_MOST:在此模式下,父容器未能检测出子view的大小,但指定了一个最大大小spec size,子view的大小不能超过此值。对应布局文件中的wrap_content方式

MeasureSpec.UNSPECIFIED:在此模式下,父容器不对子view的大小做限制,一般用于系统内部,或者ListView ScrollView等滑动控件。

5.View的绘制流程

View 绘制中主要流程分为measure,layout, draw 三个阶段。

measure :根据父 view 传递的 MeasureSpec 进行计算大小。

layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。

draw :把 View 对象绘制到屏幕上。

https://blog.csdn.net/sinat_27154507/article/details/79748010

6.invalidate()和postInvalidate()的区别?

postInvalidate() 方法在非 UI 线程中调用,通知 UI 线程重绘(实际上 postInvalidate() 底层的实现还是通过 Hanlder 的,但是底层封装起来了,让我们直接可以在子线程调用)。

invalidate() 方法在 UI 线程中调用,重绘当前 UI。

7.onTouch()、onTouchEvent()和onClick()关系?

通过源码查看: View – dispatchTouchEvent方法中

image

可以看出: onTouchListener的接口的优先级是要高于onTouchEvent的,假若onTouchListener中的onTouch方法返回true, 表示此次事件已经被消费了,那onTouchEvent是接收不到消息的。 那么思考: 如果给一个Button设置一个onTouchListener并且重写onTouch方法,返回值为true, 此时的Button的点击事件还处理吗?

image

答案是: 得不到处理的。 由于Button的performClick是在onTouchEvent后实现,假若onTouchEvent没有被调用到,那么Button的Click事件也无法响应。这里可以查看源码: View – onTouchEvent方法,来说明

image

总结:onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。顺序为: onTouch—–>onTouchEvent—>onclick

Drawable等资源

1.了解哪些Drawable?适用场景?

https://blog.csdn.net/plokmju88/article/details/103354988

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

mdpi 120~160dpi 48*48px

hdpi 160~240dpi 72*72px

xhdpi 240~320dpi 96*96px

xxhdpi 320~480dpi 144*144px

xxxhdpi 480~640dpi 192*192px

比例

1 :1.5 : 2 : 3 : 4

mdpi:hdpi:xhdpi:xxhdpi :xxxhdpi

例如界面上有一个长度为“80dp”的图片,那么它在240dpi的手机上实际显示为80x1.5=120px,在320dpi的手机上实际显示为80x2=160px。

如果你拿这两部手机放在一起对比,会发现这个图片的物理尺寸“差不多”,这就是使用dp作为单位的效果

简单理解就是跟像素密度有关,通过求屏幕对角线像素和尺寸可以得出一个值,这个值得大小对应这几个文件夹的范围,然后资源查找的时候回到对应的文件夹去找,只有当前文件夹找不到,才会依次往下查找。不对应的屏幕像素密度会对图片进行一定比例的缩放,具体参考文章https://blog.csdn.net/songzi1228/article/details/88972385

3.dp、dpi、px的区别?

px:像素,如分辨率1920x1080表示高为1920个像素、宽为1080个像素

dpi:每英寸的像素点,如分辨率为1920x1080的手机尺寸为5英寸,则该手机DPI为(1920x1920+ 1080x1080)½/5

dp:密度无关像素,是个相对值

其实dp就是为了使得开发者设置的长度能够根据不同屏幕(分辨率/尺寸也就是dpi)获得不同的像素(px)数量。比如:我将一个控件设置长度为1dp,那么在160dpi上该控件长度为1px,在240dpi的屏幕上该控件的长度为1*240/160=1.5个像素点。

也就是dp会随着不同屏幕而改变控件长度的像素数量。

关于dp的官方叙述为当屏幕每英寸有160个像素时(也就是160dpi),dp与px等价的。那如果每英寸240个像素呢?1dp—>1*240/160=1.5px,即1dp与1.5px等价了。

其实记住一点,dp最终都要化为像素数量来衡量大小的,因为只有像素数量最直观

https://www.cnblogs.com/JLZT1223/p/6784449.html

4.res目录和assets目录的区别?

res/raw中的文件会被映射到R.java文件中,访问时可以使用资源Id 不可以有目录结构

assets文件夹下的文件不会被映射到R.java中,访问时需要AssetManager类,可以创建子文件夹

动画

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

①Drawable Animation

帧动画,Frame动画,指通过指定的每一帧的图片和播放时间,有序的进行播放而形成的动画效果

②View Animation

视图动画,也就是所谓的补间动画。指通过指定View的初始状态、变化时间、方式、通过一系列的算法去进行图片变换,从而实现动画效果。主要有scale、alpha、Translate、Rotate四种效果。

注意:只是在视图层实现了动画效果,并没有真正改变View的属性。

③Property Animation

属性动画,通过不断地改变View的属性,不断重绘而形成动画效果。相比较视图动画,View的属性是真正改变了。

注意:Android3.0(API 11)以上才支持。

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

只是在视图层实现了动画效果,并没有真正改变View的属性。

3.View动画和属性动画的区别?

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

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

TimeInterpolator(时间插值器):

作用:根据时间流逝的百分比计算出当前属性值改变的百分比。

系统已有的插值器:

①LinearInterpolator(线性插值器):匀速动画。

②AccelerateDecelerateInterpolator(加速减速插值器):动画两头慢,中间快。

③DecelerateInterpolator(减速插值器):动画越来越慢。

TypeEvaluator(类型估值算法,即估值器):

作用:根据当前属性改变的百分比来计算改变后的属性值。

系统已有的估值器:

①IntEvaluator:针对整型属性

②FloatEvaluator:针对浮点型属性

③ArgbEvaluator:针对Color属性

那么TimeInterpolator和TypeEvaluator是怎么协同工作的呢?

答:它们是实现非匀速动画的重要手段。属性动画是对属性做动画,属性要实现动画,首先由TimeInterpolator(插值器)根据时间流逝的百分比计算出当前属性值改变的百分比,并且插值器将这个百分比返回,这个时候插值器的工作就完成了。比如插值器返回的值是0.5,很显然我们要的不是0.5,而是当前属性的值,即当前属性变成了什么值,这就需要估值器根据当前属性改变的百分比来计算改变后的属性值,根据这个属性值,我们就可以设置当前属性的值了。

详细使用方式https://blog.csdn.net/qq_24530405/article/details/50630744

Window

1.Activity、View、Window三者之间的关系?

首先分别介绍下这三者:

Activity是安卓四大组件之一,负责界面展示、用户交互与业务逻辑处理;

Window就是负责界面展示以及交互的职能部门,就相当于Activity的下属,Activity的生命周期方法负责业务的处理;

View就是放在Window容器的元素,Window是View的载体,View是Window的具体展示。

然后一句话介绍下三者的关系:

Activity通过Window来实现视图元素的展示,window可以理解为一个容器,盛放着一个个的view,用来执行具体的展示工作。
image

当我们运行程序的时候:

在Activity中调用attach

创建了一个Window创建的window是其子类PhoneWindow

在attach中创建PhoneWindow在Activity中调用setContentView(R.layout.xxx)

其中实际上是调用的getWindow().setContentView()

调用PhoneWindow中的setContentView方法

创建ParentView:作为ViewGroup的子类,实际是创建的DecorView(作为FramLayout的子类)

将指定的R.layout.xxx进行填充通过布局填充器进行填充【其中的parent指的就是DecorView】

调用到ViewGroup

调用ViewGroup的removeAllView(),先将所有的view移除掉

添加新的view:addView()

详情参考https://blog.csdn.net/qq_21399461/article/details/79836806

2.Window有哪几种类型?

TYPE_SEARCH_BAR: 搜索条窗口

TYPE_ACCESSIBILITY_OVERLAY: 拒绝使用

TYPE_APPLICATION: 只能配合Activity在当前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在当前APP使用

TYPE_APPLICATION_MEDIA: 无法使用(什么也不显示)

TYPE_APPLICATION_PANEL: 只能配合Activity在当前APP使用(PopupWindow默认就是这个Type)

TYPE_APPLICATION_STARTING: 无法使用(什么也不显示)

TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在当前APP使用TYPE_BASE_APPLICATION: 无法使用(什么也不显示)

TYPE_CHANGED: 只能配合Activity在当前APP使用

TYPE_INPUT_METHOD: 无法使用(直接崩溃)

TYPE_INPUT_METHOD_DIALOG: 无法使用(直接崩溃)

TYPE_KEYGUARD_DIALOG: 拒绝使用

TYPE_PHONE: 属于悬浮窗(并且给一个Activity的话按下HOME键会出现看不到桌面上的图标异常情况)

TYPE_TOAST: 不属于悬浮窗, 但有悬浮窗的功能, 缺点是在Android2.3上无法接收点击事件

TYPE_SYSTEM_ALERT: 属于悬浮窗, 但是会被禁止

不同的类型对应着不同的显示级别,显示级别高的window可以覆盖在显示级别的的上面,一些高级别的显示窗口需要申请权限

3.Activity创建和Dialog创建过程的异同?

参考https://blog.csdn.net/u014529755/article/details/103755355

线程

Q:Android中还了解哪些方便线程切换的类?

参考回答:对Handler进一步的封装的几个类:

AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在子线程中进行UI操作。

HandlerThread:一种具有消息循环的线程,其内部使用Handler。

IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。

Q:AsyncTask相比Handler有什么优点?不足呢?

AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,提供接口反馈当前异步执行的程度(可以通过接口实现UI进度更新),最后反馈执行的结果给UI主线程.

优点:简单,快捷

过程可控

使用的缺点:

在使用多个异步操作和并需要进行Ui变更时,就变得复杂起来.

Handler异步实现的原理和适用的优缺点

在Handler 异步实现时,涉及到 Handler, Looper, Message,Thread四个对象,实现异步的流程是主线程启动Thread(子线程)运行并生成Message-Looper获取Message并传递给HandlerHandler逐个获取Looper中的Message,并进行UI变更。

使用的优点:

结构清晰,功能定义明确

对于多个后台任务时,简单,清晰

使用的缺点:

在单个后台异步处理时,显得代码过多,结构过于复杂(相对性)

AsyncTask介绍

Android的AsyncTask比Handler更轻量级一些(只是代码上轻量一些,而实际上要比handler更耗资源),适用于简单的异步处理。

首先明确Android之所以有Handler和AsyncTask,都是为了不阻塞主线程(UI线程),且UI的更新只能在主线程中完成,因此异步处理是不可避免的。

Android为了降低这个开发难度,提供了AsyncTask。AsyncTask就是一个封装过的后台任务类,顾名思义就是异步任务。

AsyncTask直接继承于Object类,位置为android.os.AsyncTask。要使用AsyncTask工作我们要提供三个泛型参数,并重载几个方法(至少重载一个)。

AsyncTask定义了三种泛型类型 Params,Progress和Result。

Params 启动任务执行的输入参数,比如HTTP请求的URL。

Progress 后台任务执行的百分比。

Result 后台执行任务最终返回的结果,比如String。

使用过AsyncTask 的同学都知道一个异步加载数据最少要重写以下这两个方法:

doInBackground(Params…) 后台执行,比较耗时的操作都可以放在这里。注意这里不能直接操作UI。此方法在后台线程执行,完成任务的主要工作,通常需要较长的时间。在执行过程中可以调用publicProgress(Progress…)来更新任务的进度。

onPostExecute(Result) 相当于Handler 处理UI的方式,在这里面可以使用在doInBackground 得到的结果处理操作UI。 此方法在主线程执行,任务执行的结果作为此方法的参数返回

有必要的话你还得重写以下这三个方法,但不是必须的:

onProgressUpdate(Progress…) 可以使用进度条增加用户体验度。 此方法在主线程执行,用于显示任务执行的进度。

onPreExecute() 这里是最终用户调用Excute时的接口,当任务执行之前开始调用此方法,可以在这里显示进度对话框。

onCancelled() 用户调用取消时,要做的操作

使用AsyncTask类,以下是几条必须遵守的准则:

Task的实例必须在UI thread中创建;

execute方法必须在UI thread中调用;

不要手动的调用onPreExecute(), onPostExecute(Result),doInBackground(Params...), onProgressUpdate(Progress...)这几个方法;

该task只能被执行一次,否则多次调用时将会出现异常;

Q:使用AsyncTask需要注意什么?

1. 1) Task的实例必须在UI thread中创建

  1. execute方法必须在UI thread中调用

  2. 不要手动的调用onPreExecute(), onPostExecute(Result),doInBackground(Params...), onProgressUpdate(Progress...)这几个方法

  3. 该task只能被执行一次,否则多次调用时将会出现异常。

如果在子线程中创建调用 onPreExecute()也在创建AsyncTask的子线程中执行,doInBackground(Params...)在子线程中执行,onPostExecute(Result)和onProgressUpdate(Progress...)在主线程中

2. AsyncTask对象不可重复使用,也就是说一个AsyncTask对象只能execute()一次,否则会有异常抛出"java.lang.IllegalStateException: Cannot execute task: the task is already running"

3. 在doInBackground()中要检查isCancelled()的返回值,如果你的异步任务是可以取消的话。

cancel()仅仅是给AsyncTask对象设置了一个标识位,当调用了cancel()后,发生的事情只有:AsyncTask对象的标识位变了,和doInBackground()执行完成后,onPostExecute()不会被回调了,

而doInBackground()和 onProgressUpdate()还是会继续执行直到doInBackground()结束。所以要在doInBackground()中不断的检查 isCancellled()的返回值,当其返回true时就停止执行,

特别是有循环的时候。

public final boolean cancel(boolean mayInterruptIfRunning) {

mCancelled.set(true);

return mFuture.cancel(mayInterruptIfRunning);

}

4. 如果要在应用程序中使用网络,一定不要忘记在AndroidManifest中声明INTERNET权限,否则会报出很诡异的异常信息,比如上面的例子,如果把INTERNET权限拿掉会抛出"UnknownHostException"。

对比Java SE的Thread

Thread是非常原始的类,它只有一个run()方法,一旦开始,无法停止,它仅适合于一个非常独立的异步任务,也即不需要与主线程交互,对于其他情况,比如需要取消或与主线程交互,都需添加额外的代码来实现,

并且还要注意同步的问题。而AsyncTask是封装好了的,可以直接拿来用,如果你仅执行独立的异步任务,可以仅实现doInBackground()。

所以,当有一个非常独立的任务时,可以考虑使用Thread,其他时候,尽可能的用 AsyncTask。

5. 通常使用AsyncTask,是通过继承这个超类来完成的,如:

class BackgroundTask extends AsyncTask<Object,Object,Object>

{

    @Override

    protected Object doInBackground(Object... params) {

        return null;

    }

}   

子类必须重载 doInBackground方法。“<>”里面的三个类型,依次代表执行参数类型、进度参数类型和结果参数类型。doInBackground的参数类型必须是执行参数类型,返回的类型必须和结果参数类型。

这三个类型应该根据需要来定,其实用Object也可以,用的时候在做类型转换。启动一个AsyncTask,可以在这样做:

BackgroudTask bt = new BackgroundTask();

bt.execute("param");

使用AsyncTask的容易犯下的错误是在doInBackground方法里面直接对UI元素进行操作。如果需要和UI进行交互,可以配合使用publishProgress和onProgressUpdate。比如

@Override

protected Object doInbackground(Object... params)

{

...

publishProgress("20%");

...

publishProgress("80%");

...

return null;

}

protected void onProgressUpdate(Object... progress){

...

textView1.setText((String)progress[0]);

...

}

这里onProgressUpdate是工作在UI线程的。

使用AsyncTask的另一个问题是关于cancel。实际上,单单调用AsyncTask对象的cancel方法,并不能停止doInBackground方法的继续执行。通常比较接受的方法是设置一个标志位,

也就是在每次执行前检查一下某个变量的值(或者可以调用isCancelled方法判断),来决定继续执行还是停止。这种处理手段对于一些循环性的工作比较有用,但是对于一些循环性弱的工作可能并不怎么有效。

这也算是AsyncTask的一个弱点。和Thread相比,AsyncTask还有一个弱点是效率的问题,这个可以在本文开头给出的链接中找到相关的信息。

6. AsyncTask还有一个问题和onPreExecute方法有关。这个方法是工作在UI线程的。虽然是叫onPreExecute,但是doInBackground方法(也就是实际上的execute),并不会等待onPreExecute方法做完全部操作才开始执行。

所以,一般还是不要用这个方法,可以在调用AsyncTask对象的execute方法之前就把该完成的操作完成,以免引起某些错误。

AsyncTask还有一个方法是onPostExecute,这个方法也是工作在UI线程,它是在doInBackground方法执行结束,并返回结果后调用。这个方法里面可以调用UI线程的startActivity,这样可以实现完成大量后台操作后,

自动跳转Activity的功能。这个方法里面也可以执行另一个AsyncTask的execute方法。

Q:AsyncTask中使用的线程池大小?

AsyncTask的实现原理和注意事项推荐文章https://blog.csdn.net/hjjdehao/article/details/51999455

HandlerThread的使用步骤分为5步

// 步骤1:创建HandlerThread实例对象// 传入参数 = 线程名字,作用 = 标记该线程HandlerThreadmHandlerThread=newHandlerThread("handlerThread");
// 步骤2:启动线程
mHandlerThread.start();
// 步骤3:创建工作线程Handler & 复写handleMessage()// 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信// 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行
HandlerworkHandler=newHandler(handlerThread.getLooper()){
@OverridepublicbooleanhandleMessage(Messagemsg){
...//消息处理
returntrue;}});
// 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
// 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
// a. 定义要发送的消息
Messagemsg=Message.obtain();msg.what=2;
//消息的标识
msg.obj="B";
// 消息的存放// b. 通过Handler发送消息到其绑定的消息队列
workHandler.sendMessage(msg);
// 步骤5:结束线程,即停止线程的消息循环
mHandlerThread.quit();

Q:HandlerThread有什么特点?

HandlerThread的本质:继承Thread类 & 封装Handler类

Q:快速实现子线程使用Handler

Q:IntentService的特点?

IntentService具有以下特点:

-IntentService自带一个工作线程,当我们的Service需要做一些可能会阻塞主线程的工作的时候可以考虑使用IntentService。
-我们需要将要做的实际工作放入到IntentService的onHandleIntent回到方法中,当我们通过startService(intent)启动了IntentService之后,最终Android Framework会回调其onHandleIntent方法,并将intent传入该方法,这样我们就可以根据intent去做实际工作,并且onHandleIntent运行在IntentService所持有的工作线程中,而非主线程。
-当我们通过startService多次启动了IntentService,这会产生多个job,由于IntentService只持有一个工作线程,所以每次onHandleIntent只能处理一个job。面对多个job,IntentService会如何处理?处理方式是one-by-one,也就是一个一个按照先后顺序处理,先将intent1传入onHandleIntent,让其完成job1,然后将intent2传入onHandleIntent,让其完成job2…这样直至所有job完成,所以我们IntentService不能并行的执行多个job,只能一个一个的按照先后顺序完成,当所有job完成的时候IntentService就销毁了,会执行onDestroy回调方法。

Q:为何不用bindService方式创建IntentService?

IntentService本身设计就不支持bind操作,IntentService本身就是异步的,本身就不能确定是否在activity销毁后还是否执行,如果用bind的话,activity销毁的时候,IntentService还在执行任务的话就很矛盾了。

Q:线程池的好处、原理、类型,如何合理配置线程池?

使用线程池的好处:

-降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

-提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。

-提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。
java中使用ThreadPoolExecutor 实现线程池

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        //执行线程代码

        executor.excute(runable);

    }

}

CORE_POOL_SIZE:核心线程数定义了最小可以同时运行的线程数量。

MAX_POOL_SIZE:当队列中存放的任务到达队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

QUEUE_CAPACITY:当新任务加入是会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放到队列中。

KEEP_ALIVE_TIME:当线程池中的线程数量大于核心线程数时,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过KEEP_ALIVE_TIME才会被回收销毁。

ThreadPoolExecutor.CallerRunsPolicy():线程饱和的拒绝策略,该参数用于配置当当任务个数达到最大线程数和人物队列可存放的最大任务数时,如何处理新来的任务。
合理配置线程池
有一个简单且使用面比较广的公式:

-CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务停止,CPU就会出于空闲状态,而这种情况下多出来一个线程就可以充分利用CPU的空闲时间。

-I/O密集型(2N):这种任务应用起来,系统大部分时间用来处理I/O交互,而线程在处理I/O的是时间段内不会占用CPU来处理,这时就可以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,可以配置多一些线程,具体计算方是2N。

Q:ThreadPoolExecutor的工作策略(线程复用的实现原理)?

Q:开启一个线程的方法有哪些?销毁一个线程的方法呢?

-继承Thread类,新建一个当前类对象,并且运行其start方法,具体的工作在run方法中执行

public class Demo1_Thread extends Thread {
  
     public void run() {
         for (int i = 0; i < 10; i++) {
             System.out.println(i + " run()...");
         }
     }
 
     public static void main(String[] args) {
         Demo1_Thread demo = new Demo1_Thread();
         demo.start();
         for (int i = 0; i < 10; i++) {
             System.out.println(i + " main()...");
         }
     }
 
 }

-实现runnable接口,然后新建当前类对象,接着新建Thread对象把当前对象传递进去,最后执行thread对象的start()方法

public class Demo2_Thread implements Runnable {
  
     public void run() {
         for (int i = 0; i < 10; i++) {
             System.out.println(i + " run()...");
         }
     }
 
     public static void main(String[] args) {
         Demo2_Thread demo = new Demo2_Thread();
         Thread thread = new Thread(demo);
         thread.start();
         for (int i = 0; i < 10; i++) {
             System.out.println(i + " main()...");
         }
     }
 
 }

-实现Callable接口,新建当前类对象,在新建FutureTask类对象时传入当前类对象,接着新建Thread类对象时传入FutureTask类对象,最后运行Thread对象的start()方法

Callable<Integer> callable = new Callable<Integer>() {

            public Integer call() throws Exception {

                return new Random().nextInt(100);

            }

        };

        FutureTask<Integer> future = new FutureTask<Integer>(callable);

        new Thread(future).start();

可以使用callable.get()获取返回值,线程会阻塞等待返回。

销毁线程:线程执行完run方法会自动销毁,调用stop方法可以强行终止线程,该方法已过期,不推荐使用,因为这种强行终止会造成不可预期的后果,java中终止线程使用的是协议式的方式终止线程,用的是interrupt方法,该方法被调用后并不会立即终止线程,而是相当于给线程发送了一个终止信号,改变了线程的终止标识位,是否终止,何时终止取决于线程本身的实现方式,线程可调用isInterruped获取终止标识位,然后自行决定是否终止线程。线程在sleep过程中调用interrup终止会报异常,但是线程不会停止运行。

Q:同步和非同步、阻塞和非阻塞的概念
关于同步和异步

同步和异步其实指的是,请求发起方对消息结果的获取是主动发起的,还是等被动通知的。如果是请求方主动发起的,一直在等待应答结果(同步阻塞),或者可以先去处理其他的事情,但要不断轮询查看发起的请求是否有应答结果(同步非阻塞 )因为不管如何都要发起方主动获取消息结果,所以形式上还是同步操作。如果是由服务方通知的,也就是请求方发出请求后,要么在一直等待通知(异步阻塞),要么就先去干自己的事了(异步非阻塞),当事情处理完成之后,服务方会主动通知请求方,它的请求已经完成,这就是异步。异步通知的方式一般是通过状态改变,消息通知,或者回调函数来完成,大多数时候采用的都是回调函数。

关于阻塞和非阻塞

阻塞和非阻塞在计算机的世界里面,通常指的是针对IO的操作,如网络IO和磁盘IO等。那么什么是阻塞和非阻塞呢?简单的说就是我们调用了一个函数之后,在等待这个函数返回结果之前,当前的线程是处于挂起状态,还是运行状态,如果是挂起状态,就意味着当前线程什么都不能干,就等着获取结果,这就叫同步阻塞,如果仍然是运行状态,就意味当前线程是可以的继续处理其他任务,但要时不时的去看下是否有结果了,这就是同步非阻塞。

Q:Thread的join()有什么作用?
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才
从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long
millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时
时间里没有终止,那么将会从该超时方法中返回。
在代码清单所示的例子中,创建了10个线程,编号0~9,每个线程调用前一个线程的
join()方法,也就是线程0结束了,线程1才能从join()方法中返回,而线程0需要等待main线程结
束。join()方法可以确保线程按照自己制定的顺序执行。

Q:线程有哪些状态?
线程状态有 5 种,新建,就绪,运行,阻塞,死亡
-线程 start 方法执行后,并不表示该线程运行了,而是进入就绪状态,意思是随时准备运行,但是真正何时运行,是由操作系统决定的,代码并不能控制,

-同样的,从运行状态的线程,也可能由于失去了 CPU 资源,回到就绪状态,也是由操作系统决定的。这一步中,也可以由程序主动失去 CPU 资源,只需调用 yield 方法。

-线程运行完毕,或者运行了一半异常了,或者主动调用线程的 stop 方法,那么就进入死亡。死亡的线程不可逆转。

-下面几个行为,会引起线程阻塞。

主动调用 sleep 方法。时间到了会进入就绪状态
主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态
调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。
试图获取锁。成功的获取锁之后,会进入就绪状态。
线程在等待某个通知。其它线程发出通知后,会进入就绪状态

Q:什么是线程安全?保障线程安全有哪些手段?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
保障线程安全有哪些手段:
synchronized,可重入锁ReentrantLock,读写锁

Q:ReentrantLock和synchronized的区别?
关于二者的比较https://blog.csdn.net/qq_40551367/article/details/89414446

Q:synchronized和volatile的区别?
-volatile本质:是java虚拟机(JVM)当前变量在工作内存中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前的变量,只有当前线程可以访问到该变量,其他的线程将会被阻塞。
-volatile只能实现变量的修改可见性,并不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
-volatile只能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
-volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

Q:synchronized同步代码块还有同步方法本质上锁住的是谁?为什么?
synchronized是对象锁,锁住的是对象,作用于static的所谓类锁锁的其实也是对象,是当前类的class类

Q:sleep()和wait()的区别?

sleep是线程的方法,wait是object的方法,sleep休眠时间结束会自动唤醒,线程重新处于就绪状态,wait如果不设置等待时间,需要外部唤醒notify/notifyAll ,notify只能唤醒一个wait,且无法指定唤醒,所以,notifyAll方法更常用。sleep不会释放同步锁,wait会释放同步锁。

Q:如何合理配置线程池?

答:配置线程池应该根据任务特征决定,对于cpu密集型任务(需要做大量的运算的操作),一般最大线程数不要大于内核数。如果是io密集型则最大线程数应该大于核心数,一般为核心数的两倍。

bitmap

1.加载图片的时候需要注意什么?

主要就是内存问题,在必要的时候销毁,防止内存泄漏,加载大图防止内存溢出。android缓存LruCache的使用和实现原理。

2.LRU算法的原理?

LRU通过LinkedHashMap实现,LinkedHashMap是一个有序的HashMap,在HashMap的基础上维护一个双向链表。LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作,开发者通过LruCache的构造函数传入参数可以规定使用插入顺序排序还是访问顺序排序。
LruCache的使用非常简单,我们就已图片缓存为例。

int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };

①设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
②重写sizeOf方法,计算出要缓存的每张图片的大小。

注意:缓存的总容量和每个缓存对象的大小所用单位要一致。
LruCache的实现原理:LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。LruCache巧妙实现,就是利用了LinkedHashMap的这种数据结构。
LinkedHashMap介绍https://www.jianshu.com/p/8f4f58b4b8ab

3.Android中缓存策略?

  1. 内存缓存(LruCache)
    LRU,全称Least Rencetly Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU在操作系统中的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存空间不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰货删除,而LRU选择的是将最长时间未使用的对象进行淘汰。

  2. 磁盘缓存(文件缓存)——DiskLruCache分析
    不同于LruCache,LruCache是将数据缓存到内存中去,而DiskLruCache是外部缓存,例如可以将网络下载的图片永久的缓存到手机外部存储中去,并可以将缓存数据取出来使用,DiskLruCache不是google官方所写,但是得到了官方推荐

4.如何加载超大图片

主要就是BitmapRegionDecoder,图片区域解码,配合手势滑动,缩放等功能实现区域解码超大图片的功能,关于的使用BitmapRegionDecoder请参考https://blog.csdn.net/jjmm2009/article/details/49360751

5.Glide框架缓存原理,源码

①glide缓存分为:内存缓存和硬盘缓存
②在load方法中可以看出先调用内存缓存在加载图片,内存缓存找不到在调用硬盘缓存中加载图片
内存缓存
缓存key:决定缓存key的参数有十几个(包括url,宽,高,signature等等)
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());5

注意:4.4以前是Bitmap复用必须长宽相等才可以复用,而4.4及以后是Size>=所需就可以复用,只不过需要调用reconfigure来调整尺寸

2.默认是开启内存缓存的,可以调用skipMemoryCache(true)关闭缓存
内存缓存原理
①.缓存算法:算法是lru算法(当前看不见的图片,用LinkHashMap来存储的)+弱引用算法(当前正在显示的图片,用HashMap来存储的)

②.两个缓存区域:LruResourceCache(lru算法存储区域)+activeResources(弱引用算法存储区域)

③.优先级:LruResourceCache>activeResources

④.查找顺序:先从LruResourceCache中查找,找到了移除并添加到activeResources中,找不到再从activeResources中查找。

⑤.引用计数acquired:acquired()方法让其+1,release()方法让其-1,经过acquired()方法使acquired>0在activeResources中存储,然后不再使用调用release()方法使acquired==0被activeResources移除并且put到LruResourceCache中

⑥.onEngineJobComplete先加载到activeResources中
硬盘缓存
1.缓存模式:

3.x

DiskCacheStrategy.NONE: 表示不缓存任何内容。
DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。
DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

4.x

DiskCacheStrategy.NONE: 表示不缓存任何内容。
DiskCacheStrategy.DATA: 表示只缓存原始图片。对应上面的DiskCacheStrategy.SOURCE
DiskCacheStrategy.RESOURCE: 表示只缓存转换过后的图片。对应上面的DiskCacheStrategy.RESULT
DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
DiskCacheStrategy.AUTOMATIC: 表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)

2.硬盘缓存原理3.x:

①.默认情况先从decodeFromCache()硬盘缓存读取,如果没有读取到则从decodeFromSource()读取原始图片

②.decodeFromCache方法中先调用decodeResultFromCache方法就是对应的DiskCacheStrategy.RESULT模式加载图片,如果获取不到,会再调用decodeSourceFromCache()方法获取缓存,对应的是DiskCacheStrategy.SOURCE模式,注意此处decodeResultFromCache方法中的key是和内存缓存一致的,由十几个参数组成的,decodeSourceFromCache的key是由id和signature这两个参数来构成,大多数情况下signature是一致的可以忽略,所以是由url决定的key。

③.decodeFromSource()读取原始图片后根据判断来进行是否进行硬盘缓存。

序列化

1.Android 中两种序列化的实现方式,有什么区别?

序列化主要有2个作用:
对象持久化,对象生存在内存中,想把一个对象持久化到磁盘,必须已某种方式来组织这个对象包含的信息,这种方式就是序列化;
远程网络通信,内存中的对象不能直接进行网络传输,发送端把对象序列化成网络可传输的字节流,接收端再把字节流还原成对象。
Serializable 和Parcelable 的区别
在Android上应该尽量采用Parcelable,它效率更高。Parcelabe代码比Serializable多一些。但速度高十倍以上。
Serializable只需要对某个类以及它的属性实现Serializable接口即可,无需实现方法。缺点是使用的反射,序列化的过程较慢,这种机制会在序列化的时候创建许多的临时对象。容易触发GC。

Parcable方法实现的原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这样也就实现传递对象的功能。
Serializable
首先Serializable接口是空的!
_既然是空的,它是怎么做到序列化的呢?

其实啊,这个接口本身并没有做什么,它在这里只是充当了标记的作用。

有没有发现,在源码的注释里有ObjectOutputStream、ObjectIntputStream、ObjectOutput、ObjectInput。

这就是说,只要你的类打上了“Serializable”这个标记,就可以使用outputStream这个流把它写出去,如果没有打上这个标记你就去用流写的话,它就会抛出一个异常。

_那打上这个标记之后,它是如何做到序列化与反序列化的呢?

这后续的操作就是由JDK自己完成的了。

比如说我现在有一个Student类 要通过io流把它写到文件中去,这个时候我就必须让它实现Serializable接口。

使用之后,当你用流去写的时候,jdk自己就会把这个类中的所有属性(类型/值/名字等所有信息)打包写到一个文件中去。

这样在以后别人要用它的时候,就能根据标记来恢复,这是一个逆向的过程,所以叫做反序列化。

当然,它底层其实是用的反射,用反射去获取类的属性、名字等等信息。
Parcel的简介
简单来说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。
![X1}5N}EK2S]LW8GFRX718EE.png](https://upload-images.jianshu.io/upload_images/12338301-05560edeacfdd63b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
Parcel可以包含原始数据类型(用各种对应的方法写入,比如writeInt(),writeFloat()等),可以包含Parcelable对象,它还包含了一个活动的IBinder对象的引用,这个引用导致另一端接收到一个指向这个IBinder的代理IBinder。
Parcelable通过Parcel实现了read和write的方法,从而实现序列化和反序列化
详细介绍https://www.jianshu.com/p/df35baa91541

JNI

1.C/C++基础

2.jni层如何调用java方法(方法签名)

3.动态库,静态库

混合开发

性能优化

1.项目中如何做性能优化的?

2.了解哪些性能优化的工具?

3.布局上如何优化?列表呢?

4.内存泄漏是什么?为什么会发生?常见哪些内存泄漏的例子?都是怎么解决的?

5.内存泄漏和内存溢出的区别?

6.什么情况会导致内存溢出?

7.冷启动优化,分析启动时间,启动黑屏问题

8.View渲染机制

9.apk瘦身

热门技术

1.组件化

2.插件化

3.热修复

4.增量更新

5.插件换肤

6.Gradle

Framework

1.Binder机制
Binder是Android中最最要最复杂的知识模块,强烈推荐https://www.jianshu.com/p/429a1ff3560c
2.Handler机制(为什么线程looper死循环不会产生anr)
https://blog.csdn.net/sam0750/article/details/84976396
主线程looper死循环不会产生anr:looper并不是一个封闭的死循环,android是基于消息驱动的,在looper循环中,会使用调用
queue.next();方法不断的从消息队列中获取消息并处理,如果消息队列为空,该方法会阻塞,当有消息到达队列时,looper就会取出消息,循环不断的从消息队列中获取并处理消息,所以这个循环不是产生anr,而循环的目的正是为了处理源源不断的消息,只有当一个消息处理时间过长,导致主线程阻塞,新来的消息无法及时处理时,才会产生anr

3.Activity启动流程
过程不算复杂,但是涉及的源码比较多,强烈推荐https://blog.csdn.net/u012267215/article/details/91406211

网络

1.三次握手,四次挥手

2.http和https的区别

数据结构与算法

Q:怎么理解数据结构?

Q:什么是斐波那契数列?

Q:迭代和递归的特点,并比较优缺点

Q:了解哪些查找算法,时间复杂度都是多少?

Q:了解哪些排序算法,并比较一下,以及适用场景

Q:快排的基本思路是什么?最差的时间复杂度是多少?如何优化?

Q:AVL树插入或删除一个节点的过程是怎样的?

Q:什么是红黑树?

Q:100盏灯问题

Q:老鼠和毒药问题,加个条件,必须要求第二天出结果

Q:海量数据问题

Q:(手写算法)二分查找

Q:(手写算法)反转链表

Q:(手写算法)用两个栈实现队列

Q:(手写算法)多线程轮流打印问题

Q:(手写算法)如何判断一个链有环/两条链交叉

Q:(手写算法)快速从一组无序数中找到第k大的数/前k个大的数

Q:(手写算法)最长(不)重复子串

设计模式

Q:谈谈MVC、MVP和MVVM,好在哪里,不好在哪里?

Q:如何理解生产者消费者模型?

Q:是否能从Android中举几个例子说说用到了什么设计模式?

Q:装饰模式和代理模式有哪些区别?

Q:实现单例模式有几种方法?懒汉式中双层锁的目的是什么?两次判空的目的又是什么?

Q:谈谈了解的设计模式原则?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342