android面试

 和大多数的面经不同,我不是大牛,手头也没有3,4个 sp 的 offer 求比较,我只是一个非211,985的本科应届毕业生,想分享一下自己坎坷的求职历程,来给更多求职路上迷茫的应届生一些鼓励,特别是本科应届生。另外还要特别感谢北邮信安研二的赵翔,研三信安的吴博还有清华的金辉,虽然只是做了短短一两个月的同事,但是在之后一直尽其所能的帮助鼓励我。还有研三信安的胡相铎,非常感谢这位大神在技术学习上对我的指导!最后,当然是牛客网这个大平台了,提供的校招信息相当的全面,希望能越办越好!  

  8月初就开始准备校招,一直到10月份下旬,一切都结束尘埃落定了。因为家在深圳而人又在北京,所以我一开始就打算在北京参加校招,找回深圳的工作,真正实践下来,还是相当有难度。我是非985,211的应届本科生,像这种技术岗位,在北京,面临的问题不仅仅是你学校是不是重本的问题,还有很多中科院,清北,北邮,北航,北理工的研究生跟你一起竞争,除非你真的非常优秀,拿过ACM 奖,或者实习经历和项目经历都非常丰富,你的简历才有通过的可能,不然很可能连简历筛选这关都无法通过。如果一些非重本的本科应届生,想要从事技术岗位的工作,一定要好好丰富自己的履历,一个人在学校闷头学和外出实习学习,二者的能学到的东西,比较起来真的差很多。我运气比较好,遇到了相当开明的辅导员和系主任,都表示愿意放我走,于是大三就开始在已经在三星实习。

  非重本的本科应届毕业生,在很多地方都相当受歧视。有些企业点名就只要211的毕业生,比如华为,中兴等等。我现在仍然记得我最受屈辱的一件事情;当时华为在北邮的宣讲会结束,允许宣讲会后找面试官直接投递简历,面试官在收到我的简历后,连我的实习经历都没多看一眼,直接翻到最后找到我学校,然后露出一副鄙夷的态度,把简历打还给我,表示不接受非重本的应届毕业生。我当时心情沮丧到几天都没缓过来,心神恍惚,淋着雨走到了地铁站,连地铁都坐过了站。我几可预见即将到来的2个月是我人生第二个转折,却没想到迎头就摔了一个大大的跟头,2个月之后我会去往哪里,夙愿的offer能否拿到,能否回到家人身边工作,种种矛盾与迷茫,汇集成激流,汹涌而至。

  真正的心态的转变,是从网易的第一通电话开始的,也算是我的第一次面试的开始。感谢北邮人这个平台,让我找到了内推码,才把简历发了出去。网易的内推相当早,基本8月初就已经开始了,大家一定要尽早写好简历,很多好的互联网公司也是从8月份就开始了内推,我个人认为整个内推流程下来,感觉难度和后期参加的BAT,TMD的(头条,美团,滴滴)校招差不多,大家不需要担心难度会很大,最要紧的还是尽早复习,准备好基础知识。

  回到网易的内推上,电话面试其实也有很多坑,并不是所有的面试官都有备而来想好了面试的一系列问题。更多时候他只是想了解你对于项目经历的深入程度,需要你主动的讲解项目经历。我曾经听过在网易电面就挂了的同学的吐槽,他当时在魅族实习,公司规定进行的项目需要保密,当面试官问他项目经历时候,他便回答说这个保密不能说,面试官当场就不高兴了(可能之前电面太多同学了有点累了不耐烦了),觉得他在装逼,没聊10分钟就这同学丧失了兴趣挂了电话。所以大家应该在内推前,应该想好现在在公司的项目,什么该说什么不该说。另外,在你主动讲解项目的时候,不要介绍的太浅,可以仔细聊聊你在项目中遇到的棘手的技术难题或者难以实现的项目需求,你是怎么突破实现的,从而引起面试官的兴趣,引导他在你熟悉的技术上对你发问。我大概和面试官聊了45分钟,顺利通过了第一轮面试。

  第二轮技术面试,因为时间问题赶不到杭州了,我选择了视频面试,短信告知要求使用网易的易信进行视频面试,结果面试过程中各种声音延迟,视频卡顿,面着面着就不得以改成了语音面试,面试官也叫苦连天,真是自己人坑了自己人。第二面时间相当紧,说好的10点半结果拖到11点15才面,可能面试官赶着吃饭,见面还没打招呼问题就上来了,炮弹式发问,答到点上马上就提出下一问题。面试官那里应该有个列表的,照着列表提问,根据回答给予不同程度的评分。都是 Android 开发题目,问题相当的细,当时问了这么一个问题:View中onTouch,onTouchEvent,onClick的执行顺序,如果只是简单的在学校写下 Demo,是很难把这么细的问题回到上来的,只有真正的参与到整个 App 开发流程,才能回答上来。面了45分钟左右,答得七七八八,让我等 HR 通知

  在我很意外的情况下接到了 HR 面,因为等的时间比较长,我几乎都认为我的网易面已经跪了。HR 面也是相当的斗智斗勇,上来让我介绍下我自己,做过什么项目,个人的职业规划是什么,课外兴趣有哪些,手头有别家 Offer 吗,最后难点来了,问我为什么会选择来杭州,家人是否有在杭州的,感觉这个就被卡住了,临时急匆匆撒了个慌,感觉这个地方答得太蹩脚,最后让我说下自己的5个缺点,我以自己可能有些冒失悲观为由跟她讲了一下我参加华为宣讲会简历被拒的经历,她反倒安慰起我,忘记问我后面2个缺点了,不知道要不要感谢华为。一个 offer 就这个到手了  

  拿到网易 offer 后已经是9月底,手头也有一家C 轮的北京创业公司的 offer,可是我还是希望能的找到深圳的工作。与腾讯在北京地区的校招失之交臂后,华为中兴两家虽在深圳,无奈又卡我学历。我虽然顺利通过几家互联网公司的网上笔试,进入面试环节,但是今年互联网寒冬真的来的太猛了,北京地区竞争又激烈,说是百里挑一都不为过了,基本上校招的问题的难度已经和社招没什么区别的,印象最深的还有一道题目,让应用防第三方清理的方法,面试官要求我说至少4种,我脑汁绞尽,除了最基本的双进程守护外,连利用 Android 4.1 的系统漏洞获取临时Root权限伪装成系统级应用都说了,才勉强放过我。

  百度在深圳也有Android 开发的岗位,线上笔试虽然过了,但是我投的时候选择的是在北京参加面试,应聘的是深圳地区的岗位。我机缘巧合下得到了深圳地区的 HR 的电话,询问在深圳地区的Android 开发的岗位的情况,她回答我说在其他城市进行校招时已经招满了。我心情瞬间跌到谷底,在北京找回深圳的工作的希望正式宣告破灭了,我下决心回深圳参加社招拼一拼(深圳几乎没有什么校招宣讲会)。

  在深圳海投一波简历后,我也确实通过了不少公司的面试,无奈别人是社招的岗位,需要我立刻上岗工作,我学校还有事情要处理,不可能全职工作的。在这里也给大家提个醒,不到万不得已,不要参加社招,时间上的确合不来,而且企业也更容易毁约,大部分大规模的公司,用人方面都有规定,只允许应届生走校招流程进来。

  就我认为我希望再次破灭之际,突然接到美图公司的电话,我已经说明我是应届生,不能立刻报道,他们说没问题他们这边有校招名额空缺(之前在北邮有宣讲会,没去成),问我方便过来深圳分公司这边面试吗?我一口答应下,第二天到公司后,一路笔试,技术面试,HR 面,CTO 面,轻车熟路过关斩将,下午就收到Offer,可能我之前在三星也是做图像处理类的 App 比较多,技术那边觉得相当符合期望,薪资比之前谈的还要高了一点。瞬间觉得之前受的背运白眼都有了回报,真是苦尽甘来了。

  就在答应过几天去美图签三方了,结果梦寐以求的腾讯突然打电话来技术面试,想起原来是社招的投的简历,问的问题相当有难度,答的磕磕巴巴的,以为没戏了,晚上打电话来又要求到总部面。感觉自己像个快结婚的人了,突然学生时代的初恋女神过来撩拨一下你,明知不可能却又心存侥幸,心情起起伏伏又患得患失,人生的精彩不过如此吧。最后再次与腾讯失之交臂,加入了美图。

  最后分享一下干货,是我在面试美团,今日头条,网易,腾讯等公司时候遇到的面试题,希望能给大家接下来的面试带来帮助!如果我有哪里写得不对的,欢迎知乎私信我!

知乎: https://www.zhihu.com/people/wenmingvs

新浪微博:http://weibo.com/wenmingvs

GitHub:https://github.com/wenmingvs

J2EE 部分:

1.Switch能否用string做参数?

在 Java 7 之前, switch 只能支持 byte 、 short 、 char 、 int 或者其对应的封装类以及 Enum 类型。在 Java 7 中, String 支持被加上了。

2. equals与==的区别:

==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同

3. Object有哪些公用方法?

  • 方法equals测试的是两个对象是否相等
  • 方法clone进行对象拷贝
  • 方法getClass返回和当前对象相关的Class对象
  • 方法notify,notifyall,wait都是用来对给定对象进行线程同步的

5. 实际开发中软引用或者弱引用的使用场景:

利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题 通过软可及对象重获方法实现Java对象的高速缓存:比如我们创建了一Employee的类,如果每次需要查询一个雇员的信息。哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这是需要消耗很多时间的。我们可以通过软引用和 HashMap 的结合,先是保存引用方面:以软引用的方式对一个Employee对象的实例进行引用并保存该引用到HashMap 上,key 为此雇员的 id,value为这个对象的软引用,另一方面是取出引用,缓存中是否有该Employee实例的软引用,如果有,从软引用中取得。如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,并保存对这个新建实例的软引用

6. Hashcode的作用,与 equal 有什么区别

同样用于鉴定2个对象是否相等的,java集合中有 list 和 set 两类,其中 set不允许元素重复实现,那个这个不允许重复实现的方法,如果用 equal 去比较的话,如果存在1000个元素,你 new 一个新的元素出来,需要去调用1000次 equal 去逐个和他们比较是否是同一个对象,这样会大大降低效率。hashcode实际上是返回对象的存储地址,如果这个位置上没有元素,就把元素直接存储在上面,如果这个位置上已经存在元素,这个时候才去调用equal方法与新元素进行比较,相同的话就不存了,散列到其他地址上

7. String、StringBuffer与StringBuilder的区别

String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象 StringBuffer和StringBuilder底层是 char[]数组实现的 StringBuffer是线程安全的,而StringBuilder是线程不安全的

8. Override和Overload的含义去区别

Overload顾名思义是重新加载,它可以表现类的多态性,可以是函数里面可以有相同的函数名但是参数名、返回值、类型不能相同;或者说可以改变参数、类型、返回值但是函数名字依然不变。 Override顾名思义就是ride(重写)的意思,在子类继承父类的时候子类中可以定义某方法与其父类有相同的名称和参数,当子类在调用这一函数时自动调用子类的方法,而父类相当于被覆盖(重写)了。

9. 抽象类和接口的区别

一个类只能继承单个类,但是可以实现多个接口 接口强调特定功能的实现,而抽象类强调所属关系 抽象类中的所有方法并不一定要是抽象的,你可以选择在抽象类中实现一些基本的方法。而接口要求所有的方法都必须是抽象的

10.解析XML的几种方式的原理与特点:DOM、SAX、PULL

  • DOM:消耗内存:先把xml文档都读到内存中,然后再用DOM API来访问树形结构,并获取数据。这个写起来很简单,但是很消耗内存。要是数据过大,手机不够牛逼,可能手机直接死机
  • SAX:解析效率高,占用内存少,基于事件驱动的:更加简单地说就是对文档进行顺序扫描,当扫描到文档(document)开始与结束、元素(element)开始与结束、文档(document)结束等地方时通知事件处理函数,由事件处理函数做相应动作,然后继续同样的扫描,直至文档结束。
  • SAX:与 SAX 类似,也是基于事件驱动,我们可以调用它的next()方法,来获取下一个解析事件(就是开始文档,结束文档,开始标签,结束标签),当处于某个元素时可以调用XmlPullParser的getAttributte()方法来获取属性的值,也可调用它的nextText()获取本节点的值。

    11.wait()和sleep()的区别

  • sleep来自Thread类,和wait来自Object类
  • 调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
  • sleep睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU
  • sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒

13.JAVA多态的实现原理

抽象的来讲,多态的意思就是同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用) 实现的原理是动态绑定,程序调用的方法在运行期才动态绑定,追溯源码可以发现,JVM 通过参数的自动转型来找到合适的办法。

14.JAVA 垃圾回收与内存分配策略

14.1 垃圾回收是什么?

就是释放那些不再持有引用的对象的内存

14.2怎么判断一个对象是否需要收集?

  • 引用计数(最简单古老的方法):指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程
    • 对象引用遍历(现在大多数 jvm 使用的方法):对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集
    • 引用计数缺陷:引用计数无法解决循环引用问题:假设对象A,B都已经被实例化,让A=B,B=A,除此之外这两个对象再无任何引用,此时计数器的值就永远不可能为0,但是引用计数器无法通知gc回收他们

14.3 Java的四种引用的区别

  • 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM 也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
  • 软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
  • 弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
  • 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。

14.4 介绍垃圾回收机制

  • 标记回收法:遍历对象图并且记录可到达的对象,以便删除不可到达的对象,一般使用单线程工作并且可能产生内存碎片
  • 标记-压缩回收法:前期与第一种方法相同,只是多了一步,将所有的存活对象压缩到内存的一端,这样内存碎片就可以合成一大块可再利用的内存区域,提高了内存利用率
  • 复制回收法:把现有内存空间分成两部分,gc运行时,它把可到达对象复制到另一半空间,再清空正在使用的空间的全部对象。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。
  • 分代回收发:把内存空间分为两个或者多个域,如年轻代和老年代,年轻代的特点是对象会很快被回收,因此在年轻代使用效率比较高的算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年的内存空间,老年代则采取标记-压缩算法

14.5 JAVA 中堆和栈的区别

  • 基本数据类型比变量和对象的引用都是在栈分配的
  • 堆内存用来存放由new创建的对象和数组
  • 类变量(static修饰的变量),程序在一加载的时候就在堆中为类变量分配内存,堆中的内存地址存放在栈中
  • 实例变量:当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量,是根据零散的堆内存地址,通过哈希算法换算为一长串数字以表征这个变量在堆中的"物理位置”,实例变量的生命周期--当实例变量的引用丢失后,将被GC(垃圾回收器)列入可回收“名单”中,但并不是马上就释放堆中内存
  • 局部变量: 由声明在某方法,或某代码段里(比如for循环),执行到它的时候在栈中开辟内存,当局部变量一但脱离作用域,内存立即释放

15. Java 集合系列问题

15.1 ArrayList、LinkedList、Vector的区别

  • ArrayList 和Vector底层是采用数组方式存储数据,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差
  • LinkedList使用双向链表实现存储,随机存取比较慢
  • HashMap的底层源码实现:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
  • Fail-Fast机制:在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast机制。这一机制在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map

    15.2 HashMap和 HashTable 的区别

    HashTable比较老,是基于Dictionary 类实现的,HashTable 则是基于 Map接口实现的 HashTable 是线程安全的, HashMap 则是线程不安全的 HashMap可以让你将空值作为一个表的条目的key或value

17.什么事反射,在哪里需要用到?

18. 什么是线程池,线程池的作用是什么

答:线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。就好比原来去食堂打饭是每个人看谁抢的赢,谁先抢到谁先吃,有了线程吃之后,就是排好队形,今天我跟你关系好,你先来吃饭。比如:一个应用要和网络打交道,有很多步骤需要访问网络,为了不阻塞主线程,每个步骤都创建个线程,在线程中和网络交互,用线程池就变的简单,线程池是对线程的一种封装,让线程用起来更加简便,只需要创一个线程池,把这些步骤像任务一样放进线程池,在程序销毁时只要调用线程池的销毁函数即可。

单个线程的弊端:a. 每次new Thread新建对象性能差b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或者OOM,c. 缺乏更多功能,如定时执行、定期执行、线程中断。

java提供的四种线程池的好处在于:a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。c. 提供定时执行、定期执行、单线程、并发数控制等功能。

2、Java 线程池

Java通过Executors提供四种线程池,分别为:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

(1). newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

(2). newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

(3) newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。ScheduledExecutorService比Timer更安全,功能更强大

(4)、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

Android部分(给出重点部分):

1. Activity 系列问题

1.1 绘制Activity生命周期流程图

1.2 介绍下不同场景下Activity生命周期的变化过程

  • 启动Activity: onCreate()--->onStart()--->onResume(),Activity进入运行状态。
  • Activity退居后台: 当前Activity转到新的Activity界面或按Home键回到主屏: onPause()--->onStop(),进入停滞状态。
  • Activity返回前台: onRestart()--->onStart()--->onResume(),再次回到运行状态。
  • Activity退居后台,且系统内存不足, 系统会杀死这个后台状态的Activity,若再次回到这个Activity,则会走onCreate()-->onStart()--->onResume()
  • 锁定屏与解锁屏幕 只会调用onPause(),而不会调用onStop方法,开屏后则调用onResume()

1.3 内存不足时系统会杀掉后台的Activity,若需要进行一些临时状态的保存,在哪个方法进行?

Activity的 onSaveInstanceState() 和 onRestoreInstanceState()并不是生命周期方法,它们不同于 onCreate()、onPause()等生命周期方法,它们并不一定会被触发。当应用遇到意外情况(如:内存不足、用户直接按Home键)由系统销毁一个Activity,onSaveInstanceState() 会被调用。但是当用户主动去销毁一个Activity时,例如在应用中按返回键,onSaveInstanceState()就不会被调用。除非该activity是被用户主动销毁的,通常onSaveInstanceState()只适合用于保存一些临时性的状态,而onPause()适合用于数据的持久化保存。

1.4 onSaveInstanceState()被执行的场景有哪些:

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

  1. 当用户按下HOME键时
  2. 长按HOME键,选择运行其他的程序时
  3. 锁屏时
  4. 从activity A中启动一个新的activity时
  5. 屏幕方向切换时

1.5 介绍Activity的几中启动模式,并简单说说自己的理解或者使用场景

2. Service系列问题

2.1 注册Service需要注意什么

Service还是运行在主线程当中的,所以如果需要执行一些复杂的逻辑操作,最好在服务的内部手动创建子线程进行处理,否则会出现UI线程被阻塞的问题

2.2 Service与Activity怎么实现通信

方法一:

  1. 添加一个继承Binder的内部类,并添加相应的逻辑方法
  2. 重写Service的onBind方法,返回我们刚刚定义的那个内部类实例
  3. Activity中创建一个ServiceConnection的匿名内部类,并且重写里面的onServiceConnected方法和onServiceDisconnected方法,这两个方法分别会在活动与服务成功绑定以及解除绑定的时候调用,在onServiceConnected方法中,我们可以得到一个刚才那个service的binder对象,通过对这个binder对象进行向下转型,得到我们那个自定义的Binder实例,有了这个实例,做可以调用这个实例里面的具体方法进行需要的操作了

方法二 通过BroadCast(广播)的形式 当我们的进度发生变化的时候我们发送一条广播,然后在Activity的注册广播接收器,接收到广播之后更新视图

2.3 介绍源码中binder机制

2.4 IntentService与Service的区别

IntentService是Service的子类,是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题

  • 会创建独立的worker线程来处理所有的Intent请求;
  • 会创建独立的worker线程来处理onHandleIntent()方法实现的代码,无需处理多线程问题;
  • 所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止Service;
  • 为Service的onBind()提供默认实现,返回null;
  • 为Service的onStartCommand提供默认实现,将请求Intent添加到队列中;
  • IntentService不会阻塞UI线程,而普通Serveice会导致ANR异常
  • Intentservice若未执行完成上一次的任务,将不会新开一个线程,是等待之前的任务完成后,再执行新的任务,等任务完成后再次调用stopSelf()

3. Handle系列问题

3.1 介绍Handle的机制

  • Handler通过调用sendmessage方法把消息放在消息队列MessageQueue中,Looper负责把消息从消息队列中取出来,重新再交给Handler进行处理,三者形成一个循环
  • 通过构建一个消息队列,把所有的Message进行统一的管理,当Message不用了,并不作为垃圾回收,而是放入消息队列中,供下次handler创建消息时候使用,提高了消息对象的复用,减少系统垃圾回收的次数
  • 每一个线程,都会单独对应的一个looper,这个looper通过ThreadLocal来创建,保证每个线程只创建一个looper,looper初始化后就会调用looper.loop创建一个MessageQueue,这个方法在UI线程初始化的时候就会完成,我们不需要手动创建

3.2 谈谈对HandlerThread的理解

4. ListView系列问题

4.1 ListView卡顿的原因与性能优化,越多越好

  1. 重用converView: 通过复用converview来减少不必要的view的创建,另外Infalte操作会把xml文件实例化成相应的View实例,属于IO操作,是耗时操作。

  2. 减少findViewById()操作: 将xml文件中的元素封装成viewholder静态类,通过converview的setTag和getTag方法将view与相应的holder对象绑定在一起,避免不必要的findviewbyid操作

  3. 避免在 getView 方法中做耗时的操作: 例如加载本地 Image 需要载入内存以及解析 Bitmap ,都是比较耗时的操作,如果用户快速滑动listview,会因为getview逻辑过于复杂耗时而造成滑动卡顿现象。用户滑动时候不要加载图片,待滑动完成再加载,可以使用这个第三方库glide

  4. Item的布局层次结构尽量简单,避免布局太深或者不必要的重绘

  5. 尽量能保证 Adapter 的 hasStableIds() 返回 true 这样在 notifyDataSetChanged() 的时候,如果item内容并没有变化,ListView 将不会重新绘制这个 View,达到优化的目的

  6. 在一些场景中,ScollView内会包含多个ListView,可以把listview的高度写死固定下来。 由于ScollView在快速滑动过程中需要大量计算每一个listview的高度,阻塞了UI线程导致卡顿现象出现,如果我们每一个item的高度都是均匀的,可以通过计算把listview的高度确定下来,避免卡顿现象出现

  7. 使用 RecycleView 代替listview: 每个item内容的变动,listview都需要去调用notifyDataSetChanged来更新全部的item,太浪费性能了。RecycleView可以实现当个item的局部刷新,并且引入了增加和删除的动态效果,在性能上和定制上都有很大的改善

  8. ListView 中元素避免半透明: 半透明绘制需要大量乘法计算,在滑动时不停重绘会造成大量的计算,在比较差的机子上会比较卡。 在设计上能不半透明就不不半透明。实在要弄就把在滑动的时候把半透明设置成不透明,滑动完再重新设置成半透明。

  9. 尽量开启硬件加速: 硬件加速提升巨大,避免使用一些不支持的函数导致含泪关闭某个地方的硬件加速。当然这一条不只是对 ListView。

4.2 怎么实现一个部分更新的 ListView?

4.3 怎么实现ListView多种布局?

4.4 ListView与数据库绑定的实现

5. JNI系列问题

5.1 如何使用JNI

  1. JAVA中声明native 方法如private native String printJNI(String inputStr);

  2. 使用javah工具生成.h头文件这时候头文件中就会自动生成对应的函数JNIEXPORT jstring JNICALL Java_com_wenming_HelloWorld_printJNI

  3. 实现JNI原生函数源文件,新建HelloWorld.c文件,对刚才自动生成的函数进行具体的逻辑书写,例如返回一个java叫做HelloWorld的字符串等

  4. 编译生成动态链接so文件**

  5. Java中调用Sysytem.load方法把刚才的so库加载进来,就可以调用native方法了

5.2 如何通过JNI传递String对象

Java的String和C++的string是不能对等起来的,所以当我们拿到.h文件下面的jstring对象,会做一次转换我们把jstring转换为C下面的char*类型, 获取值

constchar* str;
str = env->GetStringUTFChars(prompt,false);

赋予值

char* tmpstr ="return string succeeded";
jstring rtstr = env->NewStringUTF(tmpstr);

6. OOM系列问题

6.1 什么OOM?

OOM全称是Out Of Merrory,Android系统的每一个应用程序都设置一个硬性的Dalvik Heap Size最大限制阈值,如果申请的内存资源超过这个限制,系统就会抛出OOM错误

6.2 内存泄漏有哪些场景以及解决方法

  • 类的静态变量持有大数据对象 静态变量长期维持到大数据对象的引用,阻止垃圾回收。

  • 非静态内部类存在静态实例 非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被回收掉。

  • 资源对象未关闭 资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们, 以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。 如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。 解决办法: 比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭), 如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。 因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null. 在我们的程序退出时一定要确保我们的资源性对象已经关闭。 程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小, 对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险,记得try catch后,在finally方法中关闭连接

  • Handler内存泄漏 Handler作为内部类存在于Activity中,但是Handler生命周期与Activity生命周期往往并不是相同的,比如当Handler对象有Message在排队,则无法释放,进而导致本该释放的Acitivity也没有办法进行回收。 解决办法

  • 声明handler为static类,这样内部类就不再持有外部类的引用了,就不会阻塞Activity的释放
  • 如果内部类实在需要用到外部类的对象,可在其内部声明一个弱引用引用外部类。

    public class MainActivity extends Activity {
     private CustomHandler mHandler;
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mHandler = new CustomHandler(this);
     }
    
     static class CustomHandlerextends Handler {
         // 内部声明一个弱引用,引用外部类
         private WeakReference activityWeakReference;
         public MyHandler(MyActivity activity) {
             activityWeakReference= new WeakReference(activity);
         }
                 // ... ...   
     }
    }
    
  • 在Activity onStop或者onDestroy的时候,取消掉该Handler对象的Message和Runnable

    Override
    public void onDestroy() {
     //  If null, all callbacks and messages will be removed.
     mHandler.removeCallbacksAndMessages(null);
    }
    
  • 一些不良代码习惯 有些代码并不造成内存泄露,但是他们的资源没有得到重用,频繁的申请内存和销毁内存,消耗CPU资源的同时,也引起内存抖动 解决方案 如果需要频繁的申请内存对象和和释放对象,可以考虑使用对象池来增加对象的复用。 例如ListView便是采用这种思想,通过复用converview来避免频繁的GC

6.2 如何避免 OOM 问题的出现

1. 使用更加轻量的数据结构 例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与value的自动装箱(autoboxing),并且避免了装箱后的解箱。

2. 避免在Android里面使用Enum Android官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考《Android性能优化典范(三)》,所以请避免在Android里面使用到枚举。

3. 减小Bitmap对象的内存占用 Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用可谓是重中之重,,通常来说有以下2个措施: inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。 decode format:解码格式,选择ARGB_6666/RBG_545/ARGB_4444/ALPHA_6,存在很大差异

4.Bitmap对象的复用 缩小Bitmap的同时,也需要提高BitMap对象的复用率,避免频繁创建BitMap对象,复用的方法有以下2个措施 LRUCache : “最近最少使用算法”在Android中有极其普遍的应用。ListView与GridView等显示大量图片的控件里,就是使用LRU的机制来缓存处理好的Bitmap,把近期最少使用的数据从缓存中移除,保留使用最频繁的数据, inBitMap高级特性:利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小

4. 使用更小的图片 在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。尽量使用更小的图片不仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。

5.StringBuilder 在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。

4.避免在onDraw方法里面执行对象的创建 类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。

5. 避免对象的内存泄露 android中内存泄漏的场景以及解决办法,参考上一问

7. ANR 系列问题

7.1 什么ANR

ANR全称Application Not Responding,意思就是程序未响应。如果一个应用无法响应用户的输入,系统就会弹出一个ANR对话框,用户可以自行选择继续等待亦或者是停止当前程序。一旦出现下面两种情况,则弹出ANR对话框

  • 应用在5秒内未响应用户的输入事件(如按键或者触摸)
  • BroadcastReceiver未在10秒内完成相关的处理

7.2 ANR是怎么引起的?

  • 主线程中存在耗时的计算-
  • 主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。-
  • 主线程中错误的操作,比如Thread.wait或者Thread.sleep等

7.3 如何避免ANR问题的出现

基本思路就是把一些耗时操作放到子线程中处理

  • 使用AsyncTask处理耗时IO操作。

  • 降低子线程优先级使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同。

  • 使用Handler处理子线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程。

  • Activity的onCreate和onResume回调中尽量避免耗时的代码

  • BroadcastReceiver中onReceive代码也要尽量减少耗时操作建议使用IntentService处理。IntentService是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题

8. Asynctask问题

8.1 AsynTask为什么要设计为只能够一次任务?

最核心的还是线程安全问题,多个子线程同时运行,会产生状态不一致的问题。所以要务必保证只能够执行一次

8.2 AsynTask造成的内存泄露的问题怎么解决,》比如非静态内部类AsynTask会隐式地持有外部类的引用,如果其生命周期大于外部activity的生命周期,就会出现内存泄漏

  • 注意要复写AsynTask的onCancel方法,把里面的socket,file等,该关掉的要及时关掉
  • 在 Activity 的onDestory()方法中调用Asyntask.cancal方法
  • Asyntask内部使用弱引用的方式来持有Activity

8.3 若Activity已经销毁,此时AsynTask执行完并且返回结果,会报异常吗?

当一个App旋转时,整个Activity会被销毁和重建。当Activity重启时,AsyncTask中对该Activity的引用是无效的,因此onPostExecute()就不会起作用,若AsynTask正在执行,折会报 view not attached to window manager 异常

同样也是生命周期的问题,在 Activity 的onDestory()方法中调用Asyntask.cancal方法,让二者的生命周期同步

8.4 Activity销毁但Task如果没有销毁掉,当Activity重启时这个AsyncTask该如何解决?

还是屏幕旋转这个例子,在重建Activity的时候,会回掉Activity.onRetainNonConfigurationInstance()重新传递一个新的对象给AsyncTask,完成引用的更新

9. Android触摸分发机制

9.1 介绍触摸事件的分发机制

enter image description here

(1) 事件从Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的View(ViewGroup)开始一直往下(子View)传递。子View可以通过onTouchEvent()对事件进行处理。

(2) 事件由父View(ViewGroup)传递给子View,ViewGroup可以通过onInterceptTouchEvent()对事件做拦截,停止其往下传递。

(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子View没有消费事件,事件会反向往上传递,这时父View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到Activity的onTouchEvent()函数。

(4) 如果View没有对ACTION_DOWN进行消费,之后的其他事件不会传递过来。

(5) OnTouchListener优先于onTouchEvent()对事件进行消费。

上面的消费即表示相应函数返回值为true。

9.2 View中 setOnTouchListener的onTouch,onTouchEvent,onClick的执行顺序

追溯到View的dispatchTouchEvent源码查看,有这么一段代码

public boolean dispatchTouchEvent(MotionEvent event) {  
        if (!onFilterTouchEventForSecurity(event)) {  
            return false;  
        }  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        }  
        return onTouchEvent(event);  
    }

当以下三个条件任意一个不成立时,

  • mOnTouchListener不为null
  • view是enable的状态
  • mOnTouchListener.onTouch(this, event)返回true,

函数会执行到onTouchEvent。在这里我们可以看到,首先执行的是mOnTouchListener.onTouch的方法,然后是onTouchEvent方法

继续追溯源码,到onTouchEvent()观察,发现在处理ACTION_UP事件里有这么一段代码

 if (!post(mPerformClick)) {  
                                    performClick();  
                                }

此时可知,onClick方法也在最后得到了执行

所以三者的顺序是:

  1. setOnTouchListener() 的onTouch
  2. onTouchEvent()
  3. onClick()

10. Dalvik虚拟机系列问题

10.1 什么是Dalvik虚拟机?

Dalvik虚拟机是Android平台的核心。它可以支持.dex格式的程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,可以减少整体文件尺寸,提高I/O操作的速度,适合内存和处理器速度有限的系统。

10.2 Dalvik虚拟机的作用是什么?

Dalvik虚拟机主要是完成对象生命周期管理,内存回收,堆栈管理,线程管理,安全和异常管理等等重要功能。

10.3 Dalvik虚拟机与JVM有什么区别

  • Dalvik 基于寄存器,而 JVM 基于栈。基于寄存器的虚拟机对于更大的程序来说,在它们编译的时候,花费的时间更短。
  • Dalvik执行.dex格式的字节码,而JVM执行.class格式的字节码。

10.4 每个应用程序对应多少个Dalvik虚拟机

  • 每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,其代码在虚拟机的解释下得以执行 ,而所有的Android应用的线程都对应一个Linux线程

11. 注册广播接收器有哪几种方式,有什么区别

  • 静态注册:在AndroidManifest.xml文件中进行注册,当App退出后,Receiver仍然可以接收到广播并且进行相应的处理
  • 动态注册:在代码中动态注册,当App退出后,也就没办法再接受广播了

12. 显示Intent与隐式Intent的区别

对明确指出了目标组件名称的Intent,我们称之为“显式Intent”。 对于没有明确指出目标组件名称的Intent,则称之为“隐式 Intent”。

对于隐式意图,在定义Activity时,指定一个intent-filter,当一个隐式意图对象被一个意图过滤器进行匹配时,将有三个方面会被参考到:

  • 动作(Action)
  • 类别(Category ['kætɪg(ə)rɪ] )
  • 数据(Data )

13. Android中的动画有哪些,区别是什么

  • 逐帧动画(Drawable Animation): 加载一系列Drawable资源来创建动画,简单来说就是播放一系列的图片来实现动画效果,可以自定义每张图片的持续时间

  • 补间动画(Tween Animation): Tween可以对View对象实现一系列简单的动画效果,比如位移,缩放,旋转,透明度等等。但是它并不会改变View属性的值,只是改变了View的绘制的位置,比如,一个按钮在动画过后,不在原来的位置,但是触发点击事件的仍然是原来的坐标。

  • 属性动画(Property Animation): 动画的对象除了传统的View对象,还可以是Object对象,动画结束后,Object对象的属性值被实实在在的改变了

14. 不使用动画,怎么实现一个动态的 View?

15. Postvalidata与Validata有什么区别?

16. 如何自定义ViewGroup?

17. View的绘制流程

enter image description heremeasure()方法,layout(),draw()三个方法主要存放了一些标识符,来判断每个View是否需要再重新测量,布局或者绘制,主要的绘制过程还是在onMeasure,onLayout,onDraw这个三个方法中

1.onMesarue() 为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性: mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

2.onLayout() 为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。

3. onDraw() 开始绘制图像,绘制的流程如下

  1. 首先绘制该View的背景
  2. 调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
  3. 如果该View是ViewGroup,调用dispatchDraw ()方法绘制子视图
  4. 绘制滚动条

18. 数据持久化的四种方式有哪些?

  1. 文件存储: 通过java.io.FileInputStream和java.io.FileOutputStream这两个类来实现对文件的读写,java.io.File类则用来构造一个具体指向某个文件或者文件夹的对象。

  2. SharedPreferences: SharedPreferences是一种轻量级的数据存储机制,他将一些简单的数据类型的数据,包括boolean类型,int类型,float类型,long类型以及String类型的数据,以键值对的形式存储在应用程序的私有Preferences目录(/data/data/<包名>/shared_prefs/)中,这种Preferences机制广泛应用于存储应用程序中的配置信息。

  3. SQLite数据库: 当应用程序需要处理的数据量比较大时,为了更加合理地存储、管理、查询数据,我们往往使用关系数据库来存储数据。Android系统的很多用户数据,如联系人信息,通话记录,短信息等,都是存储在SQLite数据库当中的,所以利用操作SQLite数据库的API可以同样方便的访问和修改这些数据。

  4. ContentProvider: 主要用于在不同的应用程序之间实现数据共享的功能,不同于sharepreference和文件存储中的两种全局可读写操作模式,内容提供其可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险

19. fragement里面可以再嵌套fragment?

20. Socker编程的步骤

21. Activity中如何动态的添加Fragment

22. Scrollview怎么判断是否滑倒底部

23. 什么是 MVC 模式?MVC 模式的好处是什么?

24. 应用常驻后台,避免被第三方杀掉的方法,讲讲你用过的奇淫巧技?

  1. Service设置成START_STICKY kill 后会被重启(等待5秒左右),重传Intent,保持与重启前一样

  2. 通过 startForeground将进程设置为前台进程, 做前台服务,优先级和前台应用一个级别​,除非在系统内存非常缺,否则此进程不会被 kill

  3. 双进程Service: 让2个进程互相保护**,其中一个Service被清理后,另外没被清理的进程可以立即重启进程

  4. QQ黑科技: 在应用退到后台后,另起一个只有 1 像素的页面停留在桌面上,让自己保持前台状态,保护自己不被后台清理工具杀死

  5. 在已经root的设备下,修改相应的权限文件,将App伪装成系统级的应用 Android4.0系列的一个漏洞,已经确认可行

  1. 用C编写守护进程(即子进程) : Android系统中当前进程(Process)fork出来的子进程,被系统认为是两个不同的进程。当父进程被杀死的时候,子进程仍然可以存活,并不受影响。鉴于目前提到的在Android->- Service层做双守护都会失败,我们可以fork出c进程,多进程守护。死循环在那检查是否还存在,具体的思路如下(Android5.0以上的版本不可行)
  2. 用C编写守护进程(即子进程),守护进程做的事情就是循环检查目标进程是否存在,不存在则启动它。
  3. 在NDK环境中将1中编写的C代码编译打包成可执行文件(BUILD_EXECUTABLE)。主进程启动时将守护进程放入私有目录下,赋予可执行权限,启动它即可。

  4. 联系厂商,加入白名单

25.Context与ApplicationContext的区别,分别用在什么情况下

Application的Context是一个全局静态变量,SDK的说明是只有当你引用这个context的生命周期超过了当前activity的生命周期,而和整个应用的生命周期挂钩时,才去使用这个application的context。

在android中context可以作很多操作,但是最主要的功能是加载和访问资源。在android中有两种context,一种是 application context,一种是activity context,通常我们在各种类和方法间传递的是activity context。

26. 同一个应用程序的不同Activity可以运行在不同的进程中么?如果可以,举例说明;

27. Java中的线程同步有哪几种方式,举例说明;

28. dp, dip, dpi, px, sp是什么意思以及他们的换算公式?layout-sw400dp, layout-h400dp分别代表什么意思;

29. 如何让两个TextView在一个RelativeLayout水平居中显示;

30. 如何画出一个印章的图案

31. 如何实现一个字体的描边与阴影效果

32. 设计一个从网络请求数据,图片,并加载到列表的系统,画出客户端架构并简单的分析下;

33. 设计一个文件的断点续传系统;

34. 设计一个图片缓存加载机制

数据结构与算法部分:

  1. 给最外层的rootview,把这个根视图下的全部button背景设置成红色,手写代码,不许用递归
  2. 给一串字符串比如abbbcccd,输出a1b3c3d1,手写代码(注意有个别字符可能会出现十次以上的情况)
  3. 一个序列,它的形式是12349678,9是最高峰,经历了一个上升又下降的过程,找出里面的最大值的位置,要求效率尽可能高
  4. 二叉查找树的删除操作,手写代码
  5. 反转链表,手写代码
  6. 二分查找,手写代码
  7. 有海量条 url,其中不重复的有300万条,现在希望挑选出重复出现次数最高的 url,要求效率尽可能的高
  8. 一篇英语文章,去掉字符只留下k个,如何去掉才能使这k个字符字典序最小
  9. 弗洛伊德算法和 Dijkstra算法的区别?复杂度是多少?讲讲 Dijkstra算法的具体过程
  10. 反转字符串,要求手写代码,优化速度、优化空间
  11. 给出两个无向图,找出这2个无向图中相同的环路。手写代码
  12. 单例模式,手写代码
  13. 生产者与消费者,手写代码
  14. 二叉树镜像,手写代码
  15. 最长不重复子串(最长重复子串),手写代码

操作系统部分:

  1. 分别从操作系统的内存角度与进程线程角度解释分析堆,栈二者的区别
  2. 什么是事务?
  3. OSI七层模型有哪些,各层次的作用
  4. TCP的三次握手过程,四次挥手过程,为什么需要三次?
  5. 说说操作系统中进程的通信方式
  6. 浏览器输入地址之后,之后的过程
  7. 谈谈 HTTP 中Get 和 Post 方法的区别?

你可能感兴趣的:(android面试)