android面试题

最近在准备android面试,整理了下相关的面试题,分为如下三个部分:android部分、Java部分、算法面试题,后续有新内容直接在对应的文章中补充。

android部分:本文

Java部分:

https://www.jianshu.com/p/c2c8f5019c8f

算法部分:

https://www.jianshu.com/p/d9bfc440ada3

0、android系统架构图

android架构(http://gityuan.com/)

Android系统启动过程过程如下:Loder--Kernel--Native--FrameWork--App。

Loader层:分为Boot ROM和Boot Loader,其中Boot ROM是当手机处于关机状态时,长按Power键开机,引导芯片开始从固化ROM里的预设代码开始执行,然后加载引导程序到RAM;Boot Loader是启动Android系统之前的引导程序,主要是检查RAM,初始化应急参数等功能。

Kernel层:指的是android内核层,到这里才刚刚开始进入android系统。启动Kernel的Swapper进程,该进程也成为idle进程,用于初始化进程管理、内存管理,加载Display、Camera Driver、Binder Driver等。然后接着启动kthhreadd进程,该进程是Linux的内核进程,用于创建内核工作线程kworker等,kthreadd进程是所有内核进程的鼻祖。

Native层:进入到Native层主要包括启动init进程,init是linux系统的用户进程,init进程是所有用户进程的鼻祖。Init进程会启动ServiceManager(binder服务大管家),Init进程还会会孵化出zygote进程,zygote是android系统的第一个java进程,zygote进程是所有java进程的父进程。

Framework层:zygote进程是右init进程通过解析init.rc文件后fork生成的,zygote进程主要包括加载ZygoteInit类,注册Zygote Socket套接字;加载虚拟机;preloadclass;preloadresource。随后zygote孵化SystemServer进程,SystemServer是zygote孵化的第一个进程,SystemServer负责启动和管理整个java framework,包含AMS、WMS、PMS等。

APP层:zygote进程孵化出的第一个APP进程是Launcher(SystemServer启动完成调用AMS的systemReady会启动Launcher),他还会孵化出Browser、Phone等;所有的APP进程都是有zygote进程孵化出来的。

1、Http请求流程

https://www.jianshu.com/p/24c320699dd8

2、android如何实现Https单向认证

https://www.jianshu.com/p/0a392c38b711

3、如何导入外部数据库

可以把数据导入data目录下,然后通过SQLiteDatabase.openOrCreateDatabase来打开数据库。

4、Proguard代码混淆,混淆后的Class是如何查找的(加载类,调用方法)

Proguard有以下四个功能:

    压缩:检测并移除代码中无用的类、字段、方法和特性。

    优化:对字节码进行优化,移除无用的指令。

    混淆:使用a b c这样简短无意义的名字对类、字段、方法进行重命名。

    预验:Java平台对处理后的代码进行预验,确保加载后的class是可以执行的。

    混淆后的class是一些无意义的名称,类的引用关系没有变,增加了反编译的难度。混淆会生成mapping文件,该文件描述了混淆后的名称和原来名称的映射关系。

5、本地广播和全局广播的区别

https://www.jianshu.com/p/71b973997ddf

本地广播通过LocalBroadcastManager来实现广播的注册和发送,只在本应用范围内传播,不必担心隐私数据的泄漏。全局广播会在整个系统内发布。

6、Window View Activity三者之间的区别

Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图),LayoutInflater像剪刀,XML配置窗花图纸。

    在Activity的attach会创建PhoneWindow类型的Window。

    Activity中调用setContentView其实是调用PhoneWindow.setContentView。

    PhoneWindow.setContentView会创建DectorView,DectorView是FrameLayout类型的ViewGroup

    之后通过LayoutInflater加载XML布局,并将View加入到DectorView中。

7、进程间通信的方式

Activity Service Receiver通过Intent通信;Socket方式(zygote通过sockect接受消息);基于文件共享;AIDL方式;Messanger方式

8、Binder原理

https://www.jianshu.com/p/b26c7bcef5e4

9、Java ClassLoader和Android ClassLoder的对比

https://www.jianshu.com/p/bce29536a602

10、Messanger和AIDL的区别

Messenger不适用大量并发的请求,它以传下的方式来处理客户端发来的消息,如果大量消息发送到服务端,服务端由于是通过Handler来处理消息,所以只能一个个顺序执行。Messenger主要是为了传递Message,对于跨进程调用服务端的方法,messenger不适合。Messenegr的底层实现是AIDL,系统为了跨进程传输Message提供了这个API方便使用。AIDL适用于大量并发请求,以及涉及服务端方法调用的情况。

Messenger实现跨进程双向通信示例:

Messenger双向通信

Client通过bindService在onServiceConnected中创建一个Messenger,创建一个含有replyTo的message,通过Messenger发送这个Message到Service。

Messenger Server端

Service端创建一个Messenger,并在onBind中将Messenger.getBinder返回给客户端,Service端的Messager在收到Client传来的Message后,取出replyTo字段,通过replyTo回复消息给客户端,由此就实现了双向通信。

11、OOM和内存泄漏的区别

OOM(Out Of Memory),也叫内存溢出,即内存不够用了。申请新内存的时候,发现剩余内存不够,就会出现OOM内存溢出。

内存泄漏(MemoryLeak),只的是申请的内存没法释放(引用无法释放),如果内存泄漏严重,最后可能导致内存溢出。

OOM可以try catch吗? 可以

12、产生内存泄漏的原因

资源对象没有关闭,Cursor或者File往往使用了一些缓冲,在不使用的时候需要及时关闭。可以定义一个公共的Closoable接口来执行这些关闭。

使用Adapter时没有使用缓存convertView

Bitmap不使用时没有recycle

注册的监听器没有取消

Handler造成的内存泄漏:在newHandler时,非静态内部类会持有外部类的引用。下图的代码延迟10分钟发送消息,如果发送消息后关闭界面,由于message引用了handler,handler又引用了activity,就导致activity无法被回收,出现泄漏。

handler泄露

13、内存泄漏分析步骤

代码里面集成LeakCanary工具,在测试的时候显示引用没有释放

使用android studio的内存监测工具,监测内存波动,例如内存突然变大释放,就可能存在内存抖动

Monkey测试,抓取hprof文件,使用mat工具进行分析,首先使用hprof-conf进行转换,然后mat打开文件,会展示的内存问题,然后查看detail可以查看GC根元素到内存消耗聚集点的最短路径。使用List Object / Path to GC roots可以查看对象被谁引用,当然在查看时要过滤掉soft引用和weak引用。

几个概念:

Shallow Size:对象自身占用的内存大小,不包括它引用的对象。针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。针对数组类型的对象,它的大小是数组元素对象的大小总和。

Retained Size:Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C;C就是间接引用)

换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。

14、低版本SDK如何实现高版本API

例如File.getTotalSpace()在API Level 9及其以上才会存在,在API level8调用这个方法就会出现Call requires API level 9(current min is 8)的错误,通过设置@ TargetApi(9)可以在8的SDK完成编译,但是在低版本机器运行会报出NoSushMethodError;所以在代码上要加一个版本判断以保证在低版本不允许该代码执行。

在低版本SDK下使用该版本的API需要按照如下三步进行代码编写:

TargetApi

使用@TargetApi($API_LEVEL)使可以通过编译;运行时判断API Level,仅在足够高且有此方法的系统中调用此方法;保证功能完整性,针对低版本需要自己实现API的功能。

15、Ubuntu下编译android的步骤

下载aosp源码(清华或者科大网站下载镜像)—进入根目录—source ./build/envsetup.sh—lunch—选择版本— make–j8(开启8个线程编译)

16、ANR产生的原因和分析步骤

ANR(Application Not responding),指应用程序未响应,android系统对于一些事情需要在一定时间内完成,如果超过时间还没有响应,就会产生ANR。以下场景会产生ANR:

        ·ServiceTimeout:前台服务需要在20秒内完成。

        ·BroadcastQueue Timeout:前台广播需要在10秒内完成。

        ·ContentProvider Timeout:provider在publish后需要在10秒内完成。

        ·InputDispatching Timeout:输入事件分发不能超过5秒,包含按键和触摸事件。

Service Timeout:位于ActivityManager线程中的AMS.MainHandler收到SERVICE_TIME_OUT消息时触发。对于Service有两类:前台服务的超时时间为20S,后台服务的超时时间为200s。

BroadcastReceiver Timeout:位于ActivityManager线程中的Broadcast.Queue.BroadcastHandler收到BROADCAST_TIMEOUT_MSG消息时触发,对于广播有两个队列:前台队列的超时时间为10秒,后台队列的超时时间为60s。

ContentProvider Timeout:位于ActivityManager线程中的AMS.MainHandler收到CONTENT_PROVIDER_PUBLISH_TIME_MSG消息时触发,超时时间为CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10s,这和前面的Service和BroadcastQueue完全不同,由Provider进程启动过程相关。

Input事件处理慢触发ANR,一般有如下原因:无窗口,有应用;窗口暂停;窗口未连接;窗口连接已死亡;窗口连接已满;按键事件,输出队列或事件等待队列不为空;非按键事件,事件等待队列不为空且头事件分发超时500ms。

如何分析ANR呢?

系统针对ANR,会生成一个trace.txt文件,log文件找到tid=1的线程,该线程为主线程。可以看到是什么原因导致的ANR,查看主线程的状态,是主线程等待了、死锁了还是其他什么原因。然后查看main log,搜索ANR关键字,查看当前的CPU状态、IO wait、message的延时时间、block、memory leak等。如果是死锁了,可以通过trace文件看到对象被哪个线程占用着,如果内存泄漏了,需要dump内存文件进行分析。我们也可以借助于第三方工具BlockCanary在测试的时候进行慢操作分析,该组件利用了Looper类的loop函数的特性,如下所示,loop函数在调用dispatchMessage函数(会调用handleMessage)的时候会可以打印handleMessage的执行日志,通过自定义Printer类,将printer类设置到当前looper对象,这样在就可以打印每个message的执行时间,进而找到慢操作。

Looper.dispatchMessage

如何避免ANR?

要避免ANR可以这样做:数据库操作或者文件读写等IO耗时操作使用异步;UI线程只做UI的事;广播如果需要耗时操作可以在onReceiver启动一个IntentService;如果要使用Service,劲量使用IntentService;线程间交互使用Handler来做;后台线程使用Background优先级;图片质量劲量降低,如使用565图片;界面层次布局降低,减少绘制时间;数据库打开并发读写模式;release环境关闭log。

17、log打印的buffer溢出了怎么解决

https://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg%3D%3D&mid=2653578220&idx=3&sn=5691bdd82ae0715ab12fd6b849f74aee&chksm=84b3b1ebb3c438fddf86bf74e232fa14222932ebd6d6439bed04ad17d5e64e9270d4ab460f64&scene=4

参考微信mars的xlog,xlog是微信使用的独立日志模块,android平台使用java实现日志模块,每有一句日志就写入文件,这样在使用的过程中存在大量的GC,而且大量的IO操作容易导致程序卡顿。Xlog2.0引入mmap,mmap是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝,操作内存就相当于在操作文件,避免内核空间和用户空间的频繁切换。

xlog

Mmap的回写时机如下:内存不足、进程退出、调用msync或者munmap、不设置MAP_NOSYNC

18、如何降低应用功耗

常见的导致功耗问题的原因如下:阻止手机休眠(Wake_Lock)、时常唤醒手机(Alarm)、后台频繁运行、过渡绘制。

Wakelock有以下类型:

PARTIAL_WAKE_LOCK:保持CPU运转,屏幕和键盘灯有可能是关闭的。SCREEN_DIM_WAKE_LOCK:保持CPU运转,允许保持屏幕显示但有可能是灰的,允许关闭键盘灯。

SCREEN_BRIGHT_WAKE_LOCK:保持CPU运转,允许保持屏幕高亮,允许关闭键盘灯

FULL_WAKE_LOCK:保持CPU运转,保持屏幕高亮显示,键盘灯也保持亮度

使用方式

Wakelock默认是引用计数(reference counted)行为,同一wakelock 呼叫几次acquire(),就需搭配几次release() 才能真正释放

若wakelock 已释放,再呼叫release() 就会丢出RuntimeException

可以使用setReferenceCounted(boolean value)方法,传入false设置不使用引用计数方式,release一次之后即可释放所有wakelock

acquire(long timeout),使用这种方式,传入超时时间,在timeout后自动释放锁。

Activity在使用时如果需要阻止屏幕熄灭,例如播放视频,可以使用:

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

AlarmManager类型:

应用进程及时不存在,也想在后台做一些事,这时候就需要用到AlarmManager,相当于定时器。有如下四种类型Alarm:

ELAPSED_REALTIME:在多少ms 后呼叫指定的PendingIntent。若当时手机处于休眠,会延迟到手机醒来时 (屏幕可能还是关的)才做。(不一定准时)

ELAPSED_REALTIME_WAKEUP:在多少ms 后呼叫指定的PendingIntent。若当时手机处于休眠,会唤醒手机做事,同时休眠期间被延迟的事也会一起执行。是最常见的耗电凶手。

RTC:在指定的某段时间做事。若当时手机处于休眠,会延迟到手机醒来时 (屏幕可能还是关的)才做。(不一定准时)

RTC_WAKEUP:在指定的某段时间做事。若当时手机处于休眠,会唤醒手机做事,同时休眠期间被延迟的事也会一起执行。通常用于闹钟、记事提醒等。

降低功耗的方式:

劲量不使用静态广播,以减少后台自启动工作;如果要使用静态广播,可以先设置disable,然后在进入应用后再设置enable。

少用alarm,尤其是wakeup的alarm。

使用partial wakelock 需设定timeout,在超过时间后自动释放wakelock。

网络请求如果对及时性要求不高可以对请求进行组合。

考虑使用指数退避算法。

避免2G情况传输数据。

Activity Pause时劲量释放资源。

避免动画不停运行。

减少view的过渡绘制。

19、CRASH的产生和处理

Crash分为native crash和framework crash,如果没有try…catch,就会导致crash,系统会自己捕获,进入crash流程。对应进程来说,在启动的时候会运行RuntimeInit的commonInit函数,该函数方法设置了UncaughExceptionHandler,

RuntimeInit.commonInit

UncaughExceptionHandler会触发AMS的handleApplicationCrash函数,处理crash。应用可以通过自定义UncaughExceptionHandler来设置自定义的crash后续处理,例如保存log等待下次上传。

60秒内连续crash两次的非persistent进程被认定为bad进程,如果第三次从后台启动该进程(Intent.getFlags判断),则会拒绝创建进程。

当crash次数达到两次的非persistent进程,再次杀死该进程,即使允许自启动的service也会在被杀后拒绝启动。

20、App进程的创建流程

App的进程是由zygote fork出来的,运行在Android Runtime,进程创建图如下:

进程创建(http://gityuan.com/)

当点击桌面应用图标时,会触发startActivity,startActivity发起进程便是Launcher所在进程。发起进程通过binder发送消息到system_server进程。

system_server进程检测到目标应用所在进程没有启动的话,就会调用Process.start来启动目标进程,该函数通过socket向zygote进程发送创建新进程的请求。

Zygote进程启动的时候运行了ZygoteInit.main函数,并进入runSelectLoop循环,该循环用来检测外部的socket请求,当运行Process.start的时候,该循环检测到请求,就会调用ZygoteConnection.runOnce函数,最后创建出新的目标进程。

新的进程的入口函数就是ActivityThread.main函数。

上图所示为具体调用的函数和进程间关系:

system_server通过Process.start发起进程创建请求,会先收集新进程的uid、gid、nice-name等参数,然后通过socket发给zygote进程。

Zygote在接受到system_server进程发送过来的参数后进入forkAndSpecialize函数开始创建进行。

新进程创建时进入handleChildProc,设置进程名,打开binder驱动,启动binder线程,设置art虚拟机参数,然后反射调用ActivityThread的main函数,该函数是新apk的入口函数。

21、杀进程的方法

Process.java提供了三个方法来杀死用户态进程。

杀进程

killProcess和killProcessQuite的唯一区别在于是否输出log,最终调用的都是kill(pid,sig)方法。

22、android如何开启一个新的进程

app都是单进程架构,对于多进程架构的app一般是通过在AndroidManifest.xml中android:process属性来实现的。

当android:process属性值以”:”开头,则代表该进程是私有的,只有该app可以使用,其他应用无法访问;

当android:process属性值不以”:“开头,则代表的是全局型进程,但这种情况需要注意的是进程名必须至少包含“.”字符。

23、如何做过度绘制优化

大多数手机的刷新频率为60hz,如果在1000/60=16.67ms内没有办法把这一帧的任务执行完成,就会发生丢帧现象,丢帧越多,卡顿越明显。

RefreshRate:一秒内刷新屏幕的次数,取决于硬件的固定参数,例如60Hz

Frame Rate:帧率,代表GPU一秒内绘制的帧数,例如30fps/60fps

GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,两者之间不停地协作。理想状态如下图所示:

理想状态下,GPU渲染完成的时间和硬件准备呈现时间保持一致,但不幸的是,刷新频率和帧率并不是总能保持相同的节奏。如果发生帧率和刷新频率不一致,就容易出现Tearing现象(画面上线两部分显示的内容发生断裂,来做不同的数据发生重叠),当帧率小于刷新属性频率的时候,即GPU渲染内容慢了,如下所示:

在这种情况下,某些帧显示的画面内容就和上一帧画面一样,出现掉帧现象,用户会感觉卡顿。

掉帧的原因很多,例如布局过于复杂、内存抖动、主线程做太多的事等导致GPU渲染太慢。我们可以使用View Hierarchy英[ˈhaɪərɑ:ki]来查看界面的布局结构,打开lint工具进行检查。劲量使用RelativeLayout,使用merge标签,使用ViewStub延迟布局加载,background和主题不应该重复使用。

24、IdleHandler

IdleHandler的使用

该方法是在消息队列的消息全部处理完或者在MessageQueue在阻塞的过程中等待更多的消息的时候调用的,返回值用于判断处理一次后是否保存这个接口,如果为true表示每次消息处理完或者消息阻塞了都会处理这个接口;反之只处理一次。调用IdelHandler的代码如下所示:

MessageQueue.next

Looper里面loop函数通过调用MessageQueue.next来获取消息,在nativePollOnce来获取一个message,当出现超时或者获取的消息为空,说明当前没有消息要处理,就idler.queueIdle()函数来处理IdelHandelr。Android源码里也用到了IdelHandler,例如ActivityThread中:

25、应用启动速度如何优化

通常我们使用TraceView在进行启动速度优化的分析工具,但该工具有个缺点,在冷启动的情况下只能通过代码的方式来埋点,例如在Application的onCreate中埋入开始抓取Trace文件的代码:Debug.startTrace,在主界面的onResume中埋入抓取结束的代码:Debug.stopTrace。咋抓取出trace文件后,可以通过android studio打开,打开后分为上下两个面板,上面如下:

该面板记录了各个线程的函数调用时间,main表示主线程,图中不同的颜色代表不同的方法执行,同一个颜色时间越长点执行的时间越久,空白表示这个时间段没有方法执行。下面板详细记录了各个方法的执行时间。

关键指标如下:

定位问题的时候按照如下步骤:

我们一般只关心两点:一类是调用次数不多,但每次却花费很长时间的函数(Cpi

time/Call);第二类是本身占用时间不长,但却频繁调用的函数(Calls+Recur&Calls/Total)。

从上面板查看哪些线程执行时间长,什么时间执行,与主线程的交互如何。

点击下面板的Cpu Time/Call,按照占用Cpu的时间进行排序。

哪些方法调用次数频繁,按照Calls+Recur和Calls/Total次数排序。

冷启动优化

当app进程不存在的时候,系统启动app会先创建进程,执行Application的onCreate方法,在进程的初始化中势必会消耗一段时间,这段时间WindowManager会先加载app主题样式的窗口背景windowBackground作为预览颜色,然后才会去加载真正的布局,如果这个时间很长,而默认的背景又是黑色或者白色,就会给用户造成一种卡顿错觉。一般有两种方式解决该问题:

使用闪屏页,将启动界面的windowBackground设置为APP的logo背景图片

设置windowBackground为透明色,这样当用户点击桌面app的时候,会先显示出透明的界面,给用户造成一种假象,以为是rom的卡顿,微信就是这么干的。在Activity.onCreate之前再设置本来的theme

26、Systrace

https://www.jianshu.com/p/fba521349f19

27、RemoteViews原理

https://www.jianshu.com/p/f666d7ca23d7

28、IntentService

Android的Service是在处理任务时都是同步执行的,IntentService在执行任务时却是异步的,原因是IntentService内部使用HandlerThread来执行任务,在onCreate会创建一个HandlerThread,并将HandlerThread的looper传给ServiceHandler:

IntentService.onCreate

这样在onStart中处理任务时,可以直接通过ServiceHandler进行异步处理。ServiceHandler在handleMessage的后就自动stop掉IntentService。

29、HandlerThread

HandleThread继承了Thread,在new HandlerThread后通过start方法开启这个线程;HandlerThread.run会开启一个Looper,并调用Looper.prepare,最终调用Looper.loop开启消息循环。

HandleThread.run

HandlerThread提供一个getLooper方法,在创建Handler时传入这个Looper对象可以保证Looper.loop运行在异步线程。

30、AsyncTask

Android提供的异步处理API,有三个主要方法,onPreExecute、doInBackground、onPostExecute,onpre在异步执行前调用、doInBackground是异步调用、onpost是在异步执行后调用。AsyncTask提供两个方法执行异步任务:execute/executeOnExecutor,execute方法异步任务运行在SerialExecutor线程池上,如下:

SerialExecutor

当多个AsyncTask同时执行execute方法时,如果前面一个任务没有执行完成,也就是run方法没有执行完成,而此时mActive又不为空,所以后续的任务都会在mTasks中进行等待,SerialExecutor也就是单线程的顺序执行。

executeOnExecutor执行的任务运行在自己定义的线程池上,我们可以使用AsyncTask提供的默认THREAD_POOL_EXECUTOR线程池,该线程池如下:

由于该线程池没有实现RejectedExecutionHandler拒绝任务处理策略,所以可能会抛出异常。

AsyncTask如何获取处理结果:AsyncTask通过Java的FutureTask来获取处理结果,在异步处理完成后调用AsyncTask的post方法。

FutureTask

如何取消AsyncTask?三步:

1、如果AsyncTask的状态不是RUNNING,调用cancel(true)

2、在doInBackground中判断isCancelled,如果已经取消,直接return

3、在onProgressUpdate中判断isCancelled,如果已经取消,直接return

31、Handler机制

https://www.jianshu.com/p/b0c8cfab59ef

Looper.prepare:创建一个Looper对象,并将该对象设置给ThreadLocal,这样每个Thread有自己的Looper对象。创建Looper对象会创建一个MessageQueue,所以每个Looper对应一个MessageQueue。

      Looper.loop的时候会进入死循环,获取MessageQueue,调用其next方法来获取里面的消息进行处理。Next函数在阻塞或者没有消息处理的时候会运行所有的IdelHandler。

ActivityThread的Loop.loop为啥没有阻塞主线程:

ActivityThread.main

Main函数中,在调用Loop.loop后有一个throw语句,该语句不会执行,说明Looper.loop将函数阻塞了。那为啥主线程还能运行?

在主线程使用Looper.loop可以保证主线程一直在运行,事实上,在Looper.loop死循环之前,已经创建了一个Binder线程:

thread.attach会建立Binder通道,创建新线程。attach(false)会创建一个ApplicaitonThread的Binder线程,用于接受AMS发来的消息,该Binder线程通过ActivityThread的H类型Handler将消息发送给主线程。ActivityThread并不是线程类,只是它运行在主线程。

主线程的死循环是否一致耗费CPU资源?

Handler底层采用Linux的pipe/epoll机制,MessageQueue没有消息的时候,便阻塞在Looper.mQueue.next方法中,此时主线程会释放CPU资源进入休眠,直到下个事件到达,当有新消息的时候,通过往pipe管道写数据来唤醒主线程工作。所以主线程大多数时候处于休眠状态,不会阻塞。

32、如何展示一张超大的图,并实现手指缩放

1、 使用BitmapRegionDecoder进行局部加载,用户手势拖动在加载其余部分。使用BitmapRegionDecoder.newInstance(inputStream,false)创建对象,使用decodeRegion(rect,options)加载局部矩形。

2、 参照图库的实现

33、View touch事件分发

https://www.jianshu.com/p/62b638e1712a

34、OnTouchListener .onTouch()和View.onTouchEvent()的区别

这2个方法都是在View.dispatchTouchEvent()中调用,但onTouch()优先于onTouchEvent执行;若手动复写在onTouch()中返回true(将事件消费掉),将不会再执行onTouchEvent。

当代码运行到View.dispatchTouchEvent的时候,如下所示:

View.dispatchTouchEvent

可以看到,如果设置了TouchListener,且View的Enable属性为true,就会先运行OnTouchListener.onTouch,并根据其返回值来判断是否需要进一步运行onTouchEvent。如果没有设置OnTouchListener或者OnTouchListener.onTouch函数返回false,就会运行onTouchEvent,此时onTouchEvent的返回值就是View.dispatchTouchEvent的返回值。

35、View绘制过程

https://www.jianshu.com/p/ddf78cc6296a

36、如何优化自定义控件

onDraw中不应该做内存分配

减少onDraw次数,例如减少invalidate调用,调用含有四个参数的invalidate函数,没有参数的invalidate会强制刷新整个view

减少requestLayout的调用,view层级劲量扁平化

37、LinnearLayout和RelativeLayout的区别

正常情况下LinnearLayout的性能更好点,比较两者的性能,关键在于比较Measure函数的执行。RelativeLayout的onMeasure如下:

可以发现RelativeLayout会根据二次排列的结果对子View做一次measure,因为ReleativeLayout的子View的排列方式存在彼此的依赖关系,而这个依赖关系可能和Xml布局中的View顺序不同,在确定每个View的位置时,需要先给所有的子View排序,又因为RelativeLayout允许ViewB在横向上依赖ViewA,ViewA在纵向上依赖ViewB,所以横向和纵向都需要测量一次。

LinnearLayout的onMeasure过程如下:

每次测量完child后,都会调用child.getMeasuredHeight获取child的高度,并将高度加入mTotalLength中,但是该方法暂时避开了lp.weight>0且高度为0的子view,后面会将剩余的高度按照weight分配给对应的子View。因此,如果LinearLayout没有使用weight属性,将只进行一次measure;如果使用了weight属性,LinearLayout在第一次测量时会获取所有子View的高度,之后再将剩余高度按照weight加入到子View,可见weight会影响LinearLayout的测量性能。

总结:1、RelativeLayout慢于LinearLayout是因为它会让子View执行两次measure,而LinearLayout只需要执行一次;但是如果LinearLayout有了weight属性,那么LinearLayout也需要两次measure

2、在不影响层级深度的情况下,优先使用LinearLayout。

38、Volley总结

https://www.jianshu.com/p/fcae3633a7e9

39、Activity的启动模式

https://www.jianshu.com/p/18d1917acbb8

standard默认的启动模式,每次启动actvity都会创建一个新的activity实例

singleTop一般情况下和standard模式一样,但如果某个singleTop的activity已经在某task的栈顶,此时再尝试启动该activity是不会创建新的activity对象的。

singleTask当启动一个模式为singleTask的activity时,系统会检查是否存在与这个activity的taskAffinify相同的task。有以下两种情况:

1、如果存在这样的task,就去检查该activity是否已经实例化,如果已经实例化,就销毁该activity以上的其余activity,然后调用这个activity的onNewIntent方法;如果该activity没有实例化,那么就创建该activity实例,并压入到该栈。

2、如果不存在这样的task,那么就重新创建该task,然后创建该activity的实例,然后入栈,最后将该task提到最顶位置。

singleInstance是对singleTask的进一步加强,当启动该模式的activity时,如果该activity没有被实例化,那么就重新创建一个task,然后实例化activity,并入栈;如果activity已经被实例化,那么就调用activity的onNewIntent。

该模式的activity所在task不允许存在其余activity,任何从该activity加载的其它activity(Activity2)都会被放到其它task中,如果存在于Activity2相同affinity相同的task,则在该task内创建Activity2,否则重新生成task。

40、Glide源码解析

https://www.jianshu.com/p/c555a4fe5fd1

41、OOM_ADJ

Out of memory adjustment(OOM_ADJ)是app“进程属性”的其中一种;根据运行状态,system会动态调整app的ADJ值;根据内存状态,会kill掉adj值较大的app;Linux本身存在oom机制,android进行了扩展,每个app相关。

1.cat /proc/$pid/oom_adj,此文件内容为所有进程都可以读,但只有进程自己可以写自己,也就是更改此值;oom_adj的作用,是当手机内存free不足会触发oomkiller,作为执行时的决策依据因素,值越大越先被考虑杀死;当然决策时也考虑app的内存占用大小,同等oom的优先考虑size大的;(参考ProcessList.java定义和lowmemorykiller.c判断)

2.oom_adj为1:比如“设置”界面,点开360的悬浮小窗口,此时“设置”的oom_adj的值;以及当前正在对外提供服务的provider或service;

3.oom_adj为2:可以在状态栏等位置感知到的情况;例如豌豆荚、360、音乐播放器,在状态栏有条目;

4.oom_adj为6:通俗说法此时launcher存在在后台,比如从launcher点击进入了设置或者长按HOME,此时launcher作为后台值是6;launcher在前台时为0;

5.oom_adj为7:打开“我的文件”,此时我的文件oom_adj为0(当前正在运行的应用);当按下home键,此时变为7;当按下返回或者切换了别的应用后,“我的文件”可能变为9或者9+;

6.oom_adj为-12:此apk(包括system_server)永远为-12,无论在前台还是后台;所以期望杀死后重新被调起来而采用的persistent,副作用会是持续占据内存;除了Android默认的电话等模块,其它模块若要用需要审核方可采用此方案;

7.A/B

service:通过startService()启动的,默认是Aservice,其中旧的1/3会成为Bservice;B>A;默认启动的service也是backgroundservice,可以set;

8.纯粹的activity、接收过广播、执行过provider等的到后台后默认oom_adj都比较大,会优先考虑被杀;

9.oom_adj小于0:这块主要是一些如后台二进制app、常驻后台的socket如installd、vold、netd、zygote等使用,为-16(android5.0+:-17);当在shell下执行二进制命令时,oom值默认是0;像牛盾后台root服务占据40M,但因为也是-16(android5.0+:-17)不会被杀;因为有些二进制程序是不受Android管控的,但是会受Linux管控;

42、动画的分类、实现原理,插值器、估值器

动画分为View动画、帧动画、属性动画。

线性插值器

属性动画通过时间插值器和属性估值器类进行工作,插值器根据时间流失的百分比计算属性变化的百分比,估值器根据属性变化的百分比计算属性值。图中所示为随着时间t的编号,属性x的对应的值,两者使用了不同的插值器,上面是线性插值器,下面使用的是AccelerateDecelerateInterpolator。

下面的t=10ms时,为啥x=6,计算方式如下:

Step1:计算时间流失百分比,elapsed fraction = 10/40 = 0.25

Step2:计算插值器:

先加速后减速插值器

getInteroilation就是根据时间流失百分比input还计算属性变化的百分比。

Step3:计算属性值,这时候就用到估值器

属性估值器

这里是有整型估值器,计算方式为 开始属性值+属性变化百分比*(结束属性值-开始属性)

插值器:本质是时间的函数,定义了动画的变化规律(提前/延迟执行默认的时间点来达到加速/减速的效果)。将消失的时间因子转化为插值因子,只需要实现getInterpolation函数,输入的是当前动画执行了多久了,根据公式将时间执行的百分比进行修改,来达到加速或者减速的目的,例如动画已经运行了一半时间0.5,如果getInterpolation计算后返回0.8,就达到了加速效果,如果返回0.3就达到减速的效果。

插值器:用来决定属性的计算方式,实现evalute的计算方式即可,android提供了IntEvalutor、FloatEvalutor等

43、Bitmap占用的内存

http://blog.csdn.net/u010652002/article/details/72676723

https://blog.csdn.net/smileiam/article/details/68946182

本地图片:

内存=width * height*(手机屏幕密度/资源图片文件密度)^2 * 每一象素占用字节数。

本地图片占用内存跟图片本身大小、手机屏幕密度、图片所在的文件夹密度,图片编码的色彩格式有关

网络图片:

内存=width*height*ARGB_Config

网络图片,在不同屏幕密度的手机上加载出来,占用内存是一样的。只取决于ARGB值。

44、Activity的生命周期

如果启动一个透明Activity或者Dialog,前面Activity的生命周期只会运行到onPause,而不会运行onStop。当ActivityA启动Activity时,如果ActvityB不是透明的,那么先运行ActivityB的onCreate后才运行ActivityA的onStop。

onPause和onResume对应.

onStop对应了onRestart->onStart->…

onSaveInstanceState调用的时机不确定,但肯定在onStop前面,在不在onPause后面不得而知,onSaveInstanceState是当系统觉得Activity要被可能会被回收的时候进行调用,例如按下home键或者锁屏,正常的点击返回键不会运行这个方法,与之对应的方法是onRestoreInstanceState。

下拉状态栏不会调用Activity的生命周期。

45、Fragment的生命周期

ViewPager如何实现Fragment的懒加载:setUserVisibleToUser

Fragment之间如何通信:

可以通过观察者模式来实现Fragment之间的通信,实现方式如下:

Fragment通信

46、Service的生命周期

Service生命周期分为statService和bindService两种:

startService生命周期为onCreate->onStartCommand->onDestroy;如果多次startService,onCreate之后运行一次,onStartCommand会运行多次,次数和startService相对应。

BindService生命周期为onCreate->onBind->onUnbind->onDestroy,不论调用bindDervice几次,onCreate之后运行,onStartCommand方法始终不会被调用,有如下场景。

1、appA多次bind appB的Service,如果不执行unBind,appB的Service只会onBind一次。

2、appA appB同时去bind appC的Service,如果不执行unBind,appB的onBind只会执行一次。

可以这样理解,如果appA appB同时去bind AMS,假如AMS的onBind执行了多次,那么每个APP岂不是拿到了不同的ActivityManagerNative

47、SharePreference如何实现进程同步

SP不是进程同步的,多进程并发读取会有问题。可以使用MODE_MULTI_PROCESS属性,但是该属性在androidM中已经废弃,建议使用ContentProvider来做多进程数据共享。利用Provider的增删改查特性,来完成对SP的文件的增删改查。

48、Push的实现

可以使用Socket长连接形式,通过心跳包维持长连接。

Push采用TCP长连接,如果出现粘包和分包怎么办?

https://www.cnblogs.com/wade-luffy/p/6165671.html

粘包产生的原因:

(1)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

(2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前面数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。

分包产生的原因:

可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

如何解决粘包和分包问题

一个是采用分隔符的方式,即我们在封装要传输的数据包的时候,采用固定的符号作为结尾符(数据中不能含结尾符),这样我们接收到数据后,如果出现结尾标识,即人为的将粘包分开,如果一个包中没有出现结尾符,认为出现了分包,则等待下个包中出现后 组合成一个完整的数据包,这种方式适合于文本传输的数据,如采用/r/n之类的分隔符;

另一种是采用在数据包中添加长度的方式,即在数据包中的固定位置封装数据包的长度信息(或可计算数据包总长度的信息),服务器接收到数据后,先是解析包长度,然后根据包长度截取数据包(此种方式常出现于自定义协议中)。

nio有内置的解码器来处理粘包分包

49、Websocket和Socket的区别

Http协议有一个缺陷,就是通信只能由客户端发起,是单向的,服务器端如果有什么变化就没法立即告知客户端。要想及时了解服务器的变化只能通过轮询的方式,效率非常低,很费资源。

WebSock就是为了解决这一温柔引入的,最大的特点就是服务器可以主动向客户端推送消息,客户端也可以主动向服务器推消息。如下图所示:

WebSocket处于应用层,都是基于TCP协议来传输数据的;与HTTP协议有很好的兼容性;Socket其实不是一个协议,工作在会话层,是为了大家方便直接使用底层TCP协议而做的一层封装,它是应用层和TCP协议族通信的中间软件抽象层。

50、Android签名机制和UID

签名的好处:

应用升级:应用升级时,具有同样证书的应用才能安装成功,不同签名的应用包名必须不一样。

应用程序模块化:android系统允许同一个签名的多个应用程序运行在同一个进程里面。

代码或数据共享:一个应用程序可以为另一个相同证书签名的应用程序公开自己的功能,以同一个证书对多个应用签名,利用基于签名的权限检查,应用间就可以共享代码和数据了。Pid即进程ID,UID即用户ID,Android中每个程序都有一个UID,将Activity、Service、Provider进行共享有以下方式:

1、完全暴露,android:exported=”true”,一旦设置了IntentFilter,exported就被设置为true

2、权限提示暴露:定义Permission,使用者需要使用use-permission才能访问。

私有暴露:使用shareUserId,android每个app有自己的UID,这样权限就被设置为只对该用户可见,如果要想其他应用程序可见,就可以使用shareUserId,也就是让两个APP使用同样的UID,让他们运行在同一个进程中,这样他们就可以互相看到对方的内容,当然要保证两个APP的签名一致,具有相同签名和相同sharedUserId的app才能共享UID,否则会出现Package com.test.MyTest has no signatures that match those in shared user android.uid.system错误。例如android:shareUserId=”android.uid.system”需要配合LOCAL_CERTIFICATE:=platform使用。

51、EventBus源码解析

EventBus.getInstance().register(this)注册,通过EventBus.getInstance().unRregister(this)反注册。通过@Subscriber来注解方法,表示方法需要接受某个事件。在register的时候,反射获取this类中所有@Subscriber的方法,并存储起来,存储格式:

@Subscriber方法的参数类型EventType---(方法method,方法所属对象);当进行post事件时,根据事件类型EventType找到对应的Method和Method所属对象,然后反射调用Method,并传入事件。

52、Apk打包流程

Android项目经过编译打包,形成如下文件:.dex文件、resource.arsc资源表、uncompiled resources(xml、drawable等)、AndroidManifest.xml。具体的详细打包流程如下:

打包流程

aapt阶段:使用aapt打包res资源文件,生成R文件、resource.arsc文件、res文件;resource.arsc文件记录了所有应用程序的资源目录信息,可以当成一个资源索引表,根据资源ID可以快速找到资源。

aidl阶段:将aidi文件生成对应的java文件。

java compiler阶段:编译java代码,生成class文件。

dex阶段:将class文件打包到dex文件中,生成classes.dex。

apk builder阶段:将classes.dex、resource.arsc、res带包到APK中

Jar Signer阶段:给apk进行签名

zipaligin阶段:对签名后的apk进行对齐处理。

53、APK的安装过程

Apk安装有如下步骤:

将apk拷贝到data/app目录

解析apk信息

Dexopt操作

更新权限信息

完成安装,发送ACTION_PACKAGED_ADDED广播

安装流程

54、如何降低后台service被杀的概率

结合OOM_ADJ属性,adj越大越容易被杀死,可以通过以下方式降低被杀概率。

优化内存使用

将程序进行拆分,减少每个进程的资源占用贡献

Service.startForeground使后台服务获取前台优先级(在通知栏显示)

Context.bindSevice使后台服务优先级不低于前台activity,先startService,然后再bind。service不在前台时通过bindService无法有效提升优先级。bindService只保证其与unbindService直接的时间段内服务为运行状态,startService保证在stopService/stopSelf之前的服务为运行状态,系统会劲量保持服务的运行状态(程序异常终止会尝试重启STICKY服务,crash 2次后就不会再重启,但是Setting里面强行停止的服务不会重启),所以一般先startService然后在bindService。

Service设置android:priority,只会影响service的启动优先级,不会影响运行期的生存优先级。

被杀之后怎么办?

onStartCommand返回START_STICKY

多应用抱成团,进程互拉,类似于个推,比应用内两个service互相监督更靠谱。

Native fork进程来定时启动我们的service,am startservice …,但5.0后没有用,5.0引入了forceStopPackage会杀死子进程;通过独立apk里面实现两个service互相监督,两个service互相bind,同时监听linkToDeath,监听到对方挂掉后立马把对方开启来,不过好像也没用。

通过注册各种系统广播来寻求重生

使用AlarmManager定时发广播,4.2之后Setting强制杀死后没用。应用初始安装或者forceStopPackage后是收不到广播的,可以通过非广播类的组件来进行重启,例如startService

结论:所有的手段只能保证劲量维持服务,都不是可靠的,作为厂商可以通过自身的优势来实现防杀。设置为persistent,会在开机第一时间启动,通过预置的应用来组团唤起我的服务……

55、RXJava解析

https://www.jianshu.com/p/d45c65f022de

56、Activity的启动流程

https://www.jianshu.com/p/a768810c3ff8

57、Service的启动流程

https://www.jianshu.com/p/f0108309761b

58、Receiver的注册和发送流程

https://www.jianshu.com/p/71b973997ddf

59、Provider的启动流程

https://www.jianshu.com/p/31fbd5ecdddc

60、VirtualApk代码解析

https://www.jianshu.com/nb/25534574

61、Gradle编译Apk的过程

https://www.jianshu.com/p/ed4ef3b96a29

Gradle里有Project,表示一个待编译打包的工程,可以是apk,可以是Jar,Project里面可以包含多个Project,每个Project由很多Task构成,每个Task又有不同的Action和一系列要执行的操作。Project中的Task执行顺序由dependsOn控制。

Gardle执行时以Task为执行单位,执行过程分为三个阶段:

初始化阶段:判断包含哪些工程,创建对应的Project实例,最先执行的就是setting.gradle中的语句。

配置阶段:创建不同的Task,并根据Task之间的dependsOn确定Task的执行顺序。此阶段后Task之间的依赖关系也就确定下来。

执行阶段:配置结束后,按照依赖关系,按顺序执行Task。

62、SystemServer启动过程

SystemServer是zygote孵化的第一个进程,ZygoteInit.java中,通过startSystemServer函数启动SystemServer进程:

ZygoteInit.startSystemServer

启动SystemServer进程后,运行SystemServer.run():

run()函数启动了android系统的核心Service并注册到ServiceManager,最后调用了Looper.loop()使得该线程一直处于运行状态。startOtherServices启动最后运行AMS.systemReady表示SystemServer启动完成。

AMS.systemReady()如下:

AMS.systemReady()

startHomeActivityLocked启动Launcher。

63、MVP架构描述

MVP类图

MVP架构将Activity中复杂的业务移动到Presenter层,Activity和Presenter采用双向关联,Activity实现IView接口并持有IPresenter的实现,Presenter实现IPresenter并持有IView的实现已经IModle的实现。以一次完整的数据请求为例,运行流程如下:

关于MVP demo实现可以参照如下项目:

https://github.com/JasmineBen/GankImitation_MVP

基于"干货集中营"的开放API,采用MVP架构、RxJava、dagger2、glide、retrofit、GreenDao、butterknife、rxpermissions2等技术实现了一个简单的Gank客户端

65.ButterKnife原理

https://www.jianshu.com/p/036a635da941

66.EventBus原理

https://www.jianshu.com/p/8d6c377ff026

67、.未完待续....

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,796评论 25 707
  • Java中的String类可以被继承么? 答:不能,因为它是一个final类,同样的还有Integer,Float...
    gyymz1993阅读 3,970评论 2 104
  • Android Studio JNI流程首先在java代码声明本地方法 用到native关键字 本地方法不用去实现...
    MigrationUK阅读 11,848评论 7 123
  • 说起大学,在这个青春盛开的地方,最向往的抗战时的大学。以西南联大为首的大学集体,其中学子抛开热切的报国热情之外,浓...
    屎伯君阅读 199评论 0 0
  • 其实从来没有忘记过你~其实每天都很想你~其实没想到一转身就是一辈子~其实还想躺在你怀里看电视~这些年一起看过那么多...
    02961348f6aa阅读 166评论 0 1