Andorid 面试总结
给出我面试 Andorid 中被问的问题,以及解决方案。
Activity 的生命周期
onCreate() — onStart() — onResume() — onPause() — onStop() — onDestory()
ListView 的优化方案
ps: 其实我建议采用 RecycleView 来代替 ListView ,后面说会 两者之间的区别。
converView 复用
利用好 convertView 来重用 View,切忌每次 getView() 都新建。ListView 的核心原理就是重用 View,如果重用 view 不改变宽高,重用View可以减少重新分配缓存造成的内存频繁分配/回收。
ViewHolder 优化
使用ViewHolder的原因是findViewById方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间。通过setTag,getTag直接获取View。
代码类似如下:
12345678910111213141516171819202122class ViewHolder{ImageView img;TextView name;}public View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder = null;if(convertView==null){convertView = inflater.inflate(R.layout.list_item, parent, false);holder.img = (ImageView) convertView.findViewById(R.id.img);holder.name = (TextView) convertView.findViewById(R.id.name);holder = new ViewHolder();convertView.setTag(holder);}else{holder = (ViewHolder) convertView.getTag();}//设置holderholder.img.setImageResource(R.drawable.ic_launcher);holder.name.setText(list.get(position).partname);return convertView;}图片加载优化
如果ListView需要加载显示网络图片,我们尽量不要在ListView滑动的时候加载图片,那样会使ListView变得卡顿,所以我们需要在监听器里面监听ListView的状态,如果ListView滑动(SCROLL_STATE_TOUCH_SCROLL)或者被猛滑(SCROLL_STATE_FLING)的时候,停止加载图片,如果没有滑动(SCROLL_STATE_IDLE),则开始加载图片。
其实 RecycleView 已经解决了这个问题。
- RecyclerView 和 ListView 的区别:
RecyclerView可以完成ListView,GridView的效果,还可以完成瀑布流的效果。同时还可以设置列表的 滚动方向(垂直或者水平);
RecyclerView中view的复用不需要开发者自己写代码,系统已经帮封装完成了。
RecyclerView可以进行局部刷新。
RecyclerView提供了API来实现item的动画效果。
在性能上:
如果需要频繁的刷新数据,需要添加动画,则RecyclerView有较大的优势。
如果只是作为列表展示,则两者区别并不是很大。
Webview 和 js 的交互
Android 调用 js 方法:
webView.loadUrl(url),url 可以是一个地址,也可以是一个类似于 javascript:retrunpage() 这样一个定义在 js 内部的方法,都可以达到调用 js 的效果,简单便捷。但是此方法效率比较低,获取返回值比较困难。
webView.evaluateJavascript()
123456webView.evaluateJavascript(url, new ValueCallback<String>() {public void onReceiveValue(String value) {//获取返回值}});该方法效率高,而且不会刷新页面,loadUrl 会刷新页面,效率低。但是该方法只能在 4.4 以上的版本使用,所以 2 个方法应该结合使用。
具体可以看该博文
Js 调用 Andorid 代码
通过 webview.addJavascriptInterface(new JSHook(), “androidApp”);
12345678webview.addJavascriptInterface(new JSHook(), "androidApp");public class JSHook {public void setResult(String result) {//得到参数}}这个方法缺陷比较大,漏洞产生原因是:当JS拿到Android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(java.lang.Runtime 类),从而进行任意代码执行。
解决方式:
(1)Google 在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击。
(2)在Android 4.2版本之前采用拦截prompt()进行漏洞修复
通过 webviewClient的 shouldOverrideUrlLoading 回调拦截 url
1234567webview.setWebViewClient(new WebViewClient() {public boolean shouldOverrideUrlLoading(WebView view, String url) {view.loadUrl(url);return true;}});js 代码:
1234567891011121314151617181920<!DOCTYPE html><html><head><meta charset="utf-8"><title>Carson_Ho</title><script>function callAndroid(){/*约定的url协议为:js://webview?arg1=111&arg2=222*/document.location = "js://webview?arg1=111&arg2=222";}</script></head><!-- 点击按钮则调用callAndroid()方法 --><body><button type="button" id="button1" onclick="callAndroid()">点击调用Android代码</button></body></html>Android 代码:
12345678910111213141516171819202122232425262728293031323334353637// 步骤1:加载JS代码// 格式规定为:file:///android_asset/文件名.htmlmWebView.loadUrl("file:///android_asset/javascript.html");// 复写WebViewClient类的shouldOverrideUrlLoading方法mWebView.setWebViewClient(new WebViewClient() {public boolean shouldOverrideUrlLoading(WebView view, String url) {// 步骤2:根据协议的参数,判断是否是所需要的url// 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)//假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)Uri uri = Uri.parse(url);// 如果url的协议 = 预先约定的 js 协议// 就解析往下解析参数if ( uri.getScheme().equals("js")) {// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议// 所以拦截url,下面JS开始调用Android需要的方法if (uri.getAuthority().equals("webview")) {// 步骤3:// 执行JS所需要调用的逻辑System.out.println("js调用了Android的方法");// 可以在协议上带有参数并传递到Android上HashMap<String, String> params = new HashMap<>();Set<String> collection = uri.getQueryParameterNames();}return true;}return super.shouldOverrideUrlLoading(view, url);}});}特点:
1. 该方法不存在 第一种方法中的 漏洞问题 2. 但是 js 想要获取 Android 返回值,只能通过 Andorid 再调用 js 代码。
Android通过
WebChromeClient
的onJsAlert()
、onJsConfirm()
、onJsPrompt()
方法回调分别拦截JS对话框 。整个 Android 和 js 的交互大致就是这几种,详情可以看该博文。
Andorid 多线程的线程锁
Andorid 线程锁大致分为 2 种,同步机制关键字 synchronized 和 显示锁 Lock。
同步机制关键字 synchronized
对于 java 来说,最常用的同步机制就是 synchronized 关键字,他是一种基于语言的粗略锁,能够作用于对象、函数、class。每个对象都只有一个锁,谁能够拿到这个锁谁就有访问权限。当 synchronized 作用于函数时,实际上锁的也是对象,锁定的对象就是该函数所在类的对象。而 synchronized 作用于 class 时则是锁的这个 Class 类,并非具体对象。
123456789101112131415161718192021public class SynchronizedClass {public synchronized void syncMethod(){//代码}public void syncThis(){synchronized (this){//代码}}public void syncClassMethod(){synchronized (SynchronizedClass.class){//代码}}public synchronized static void syncStaticMethod(){//代码}}显示锁 Lock
Lock 接口的实现类提供了比使用 synchronized 关键字更加灵活和广泛的锁定对象操作,而且是以面向对象的方式进行对象加锁。
12345678910111213public class ReentrantLockDemo {Lock lock = new ReentrantLock();public void doSth(){lock.lock();try {//执行某些操作}finally {lock.unlock();}}}需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在 try 内,释放锁放在finally 内!!
读写锁 ReadWriteLock
当我们读取与写入数据时,有这么一个需求:读与写互斥、写与写互斥、读与读不互斥。这里就必须用到读写锁.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980package SychronizedTest;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 读写锁测试** @author zy_style*/public class ReadWriteLockTest {/*** @author zhouyang 2016-12-1* @time 下午3:01:27* @param args*/public static void main(String[] args) {final Data data = new Data();// 开启3个子线程,分别写入数据for (int i = 0; i < 3; i++) {new Thread() {public void run() {for (int j = 0; j < 5; j++) {try {data.set(j); // 写入数据} catch (Exception e) {e.printStackTrace();}}};}.start();}// 分别读取数据for (int i = 0; i < 3; i++) {new Thread() {public void run() {for (int j = 0; j < 5; j++) {try {data.get(); // 读取数据} catch (Exception e) {e.printStackTrace();}}};}.start();}}}/*** 需要操作的数据** @author zy_style*/class Data {private int data;private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();public void set(int data) throws Exception {readWriteLock.writeLock().lock(); // 获取写锁try {System.out.println(Thread.currentThread().getName() + "准备写入数据...");Thread.sleep(50); // 模拟耗时操作this.data = data;System.out.println(Thread.currentThread().getName() + "写入数据成功!");} finally {readWriteLock.writeLock().unlock(); // 释放写锁}}public void get() throws Exception {readWriteLock.readLock().lock(); // 获取读锁try {System.out.println(Thread.currentThread().getName() + "准备读取数据...");Thread.sleep(50); // 模拟耗时操作System.out.println(Thread.currentThread().getName() + "读取数据:"+ this.data);} finally {readWriteLock.readLock().unlock(); // 释放读锁}}}
自定义 View
View 的绘制流程:OnMeasure() — OnLayout() — OnDraw()
OnMeasure():测量试图大小。从顶层父 View 到子 View 递归调用 measure 方法,measure 方法又回调 OnMeasure。
OnLayout():确定 View 位置,进行页面布局。从顶层父 View 向子 View 的递归调用 view.layout 方法的过程,即父 View 根据上一步 measure 子 View 所得到的布局大小和布局参数,将子 View 放在合适的位置上。
OnDrwa():绘制试图。ViewRoot 创建一个 Canvas 对象,然后调用 OnDraw()。六个步骤:
①.绘制视图的背景;②.保存画布的图层(Layer);③.通过 onDraw() 绘制 View 的内容;④.通过 dispatchDraw() 绘制 View 子视图,如果没有就不用;⑤.还原图层(Layer);⑥.绘制滚动条
View 的绘制流程具体可以看这篇博文
View 的事件分布机制
- Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。
- ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。
- 触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。
- 当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。
- 当某个子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。
- 当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。
- onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。
view 事件分发机制具体可以看该博文
Thread 和 AsyncTask
其实 Thread 和 AsyncTaks 并不是一个等级的东西。
AsyncTask是作为异步任务,执行除了UI界面更新的任务之外的其他耗时操作的。UI界面的更新是在主线程,也就是UI线程中执行的,而在这个异步任务中,开启了一个工作线程来执行耗时操作。而这个工作线程和UI线程的执行顺序是不同步的,也就是说只有执行完工作线程中的下载之后,才会调用UI线程中的onPostExecute()执行后续UI操作,这样就实现了异步下载。如果UI线程销毁之后工作线程再发送下载结束的信息,由于工作线程再使用过程中是与AsyncTask绑定的,所以他也会随着当前AsyncTask的销毁而销毁,不会执行后续的下载操作,自然也不会执行发送下载结束的信息。
而用 Thread 开启简单的子线程执行下载,子线程和 UI 线程只能保持简单的同步关系,这个时候需要通过 Handler 进行异步操作。
举例说明:
Thread 开启简单的子线程下载,下载完成后进行 UI 操作。
12345678910111213141516171819202122232425/*** 下载线程*/private Thread downThread = new Thread(new Runnable() {public void run() {//下载操作Message message = Message.obtain();message.what =1;message.obj = "data";handler.sendMessage(message);}});private Handler handler = new Handler(new Handler.Callback() {public boolean handleMessage(Message msg) {if(1==msg.what){//UI 操作}return false;}});//最后在需要下载的地方 调用downThread.start();AsyncTask:
12345678910111213141516171819202122232425262728293031323334353637383940class DownLoadAsyncTask extends AsyncTask<Void, Integer, Boolean> {protected void onPreExecute() {super.onPreExecute();}protected void onPostExecute(Boolean aBoolean) {super.onPostExecute(aBoolean);if (aBoolean) {Toast.makeText(mContext, "下载成功", Toast.LENGTH_SHORT).show();} else {Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show();}}protected void onProgressUpdate(Integer... values) {super.onProgressUpdate(values);}protected void onCancelled(Boolean aBoolean) {super.onCancelled(aBoolean);}protected void onCancelled() {super.onCancelled();}protected Boolean doInBackground(Void... voids) {return null;}}//调用方法DownLoadAsyncTask downLoadAsyncTask = new DownLoadAsyncTask();downLoadAsyncTask.execute();使用 AsyncTask 的注意事项:
① 异步任务的实例必须在UI线程中创建,即AsyncTask对象必须在UI线程中创建。
② execute(Params… params)方法必须在UI线程中调用。
③ 不要手动调用onPreExecute(),doInBackground(Params… params),onProgressUpdate(Progress… values),onPostExecute(Result result)这几个方法。
④ 不能在doInBackground(Params… params)中更改UI组件的信息。
⑤ 一个任务实例只能执行一次,如果执行第二次将会抛出异常。
手写一个创建 sql 的过程,并附带一个些基本增删改查操作。
Android 本身并不提供关于数据库的内容,所以一般是使用轻量级的 sqlite 数据库。
创建数据库时候,需要继承于 SQLiteOpenHelper 这个类进行数据库的创建。
1234567891011121314151617181920212223242526272829303132333435public class MySqlHelper extends SQLiteOpenHelper {private final static String dbName = "test.sql";public MySqlHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {super(context, name, factory, version);}public MySqlHelper(Context context) {//context :上下文 , name:数据库文件的名称 factory:用来创建cursor对象,默认为null//version:数据库的版本号,从1开始,如果发生改变,onUpgrade方法将会调用,4.0之后只能升不能将//这里注意,如果想要指定路径 sql 文件,比如 assets 或者其他目录,需要输入 sql 文件的完整目录名。同时,需要检测文件是否存在,如果不存在,则需要手动创建//默认创建目录是在 /data/data/database/... 目录下 ,这个权限在 android 6.0 后,非 root 手机无法直接访问目录super(context, dbName, null, 1);}public void onOpen(SQLiteDatabase db) {super.onOpen(db);//通过SQLiteDatabase执行一个创建表的sql语句db.execSQL("create table userinfo (_id integer primary key autoincrement,name varchar(20))");}//oncreate方法是数据库第一次创建的时候会被调用; 特别适合做表结构的初始化,需要执行sql语句;SQLiteDatabase db可以用来执行sql语句public void onCreate(SQLiteDatabase db) {}public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {//添加一个phone字段db.execSQL("alter table userinfo add phone varchar(11)");}}而在主 java 程序中,直接调用该类就可以创建一个 sql。
123//比如在 onCreate 方法中加入MySqlHelper mySqlHelper = new MySqlHelper(this);SQLiteDatabase database = mySqlHelper.getReadableDatabase();然后后续所有的数据库操作都是通过 database 这个类进行,例如增删改查等等。
而这些基础操作,都需要基于 Cursor 类来进行处理,个人觉得很麻烦,所有后面都采用了 GreenDao 这个ORM。关于 Curor 可以看看该博客 。
GreenDao 的原理
这个 ORM 简单几句完全说不清,本人处于只可意会不可言传这种状态。具体参考下面的几个博文:
https://blog.csdn.net/u010687392/article/details/48465315
https://www.jianshu.com/p/0d3cbe6278fb
我觉得里面说的非常详细了,其实 GreenDao 其实就是基于 Android 原生的 DataBase 进行一个封装。直接对象关系映射,这个是最简单方便的一个操作,并且简化了数据库的增删改查操作。
####暂时就更新这么多,后续看到有其他内容,会一一加上