Andorid面试总结

Andorid 面试总结

给出我面试 Andorid 中被问的问题,以及解决方案。

  1. Activity 的生命周期

    onCreate() — onStart() — onResume() — onPause() — onStop() — onDestory()

  2. ListView 的优化方案

    ps: 其实我建议采用 RecycleView 来代替 ListView ,后面说会 两者之间的区别。

    1. converView 复用

      利用好 convertView 来重用 View,切忌每次 getView() 都新建。ListView 的核心原理就是重用 View,如果重用 view 不改变宽高,重用View可以减少重新分配缓存造成的内存频繁分配/回收。

    2. ViewHolder 优化

      使用ViewHolder的原因是findViewById方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间。通过setTag,getTag直接获取View。

      代码类似如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      class 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();
      }
      //设置holder
      holder.img.setImageResource(R.drawable.ic_launcher);
      holder.name.setText(list.get(position).partname);
      return convertView;
      }
    3. 图片加载优化

      如果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有较大的优势。

      如果只是作为列表展示,则两者区别并不是很大。

  3. Webview 和 js 的交互

    Android 调用 js 方法:

    1. webView.loadUrl(url),url 可以是一个地址,也可以是一个类似于 javascript:retrunpage() 这样一个定义在 js 内部的方法,都可以达到调用 js 的效果,简单便捷。但是此方法效率比较低,获取返回值比较困难。

    2. webView.evaluateJavascript()

      1
      2
      3
      4
      5
      6
      webView.evaluateJavascript(url, new ValueCallback<String>() {
      @Override
      public void onReceiveValue(String value) {
      //获取返回值
      }
      });

      该方法效率高,而且不会刷新页面,loadUrl 会刷新页面,效率低。但是该方法只能在 4.4 以上的版本使用,所以 2 个方法应该结合使用。

      具体可以看该博文

    Js 调用 Andorid 代码

    1. 通过 webview.addJavascriptInterface(new JSHook(), “androidApp”);

      1
      2
      3
      4
      5
      6
      7
      8
      webview.addJavascriptInterface(new JSHook(), "androidApp");
      public class JSHook {
      @JavascriptInterface
      public void setResult(String result) {
      //得到参数
      }
      }

      这个方法缺陷比较大,漏洞产生原因是:当JS拿到Android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(java.lang.Runtime 类),从而进行任意代码执行。

      解决方式:

      (1)Google 在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击。

      (2)在Android 4.2版本之前采用拦截prompt()进行漏洞修复

    2. 通过 webviewClient的 shouldOverrideUrlLoading 回调拦截 url

      1
      2
      3
      4
      5
      6
      7
      webview.setWebViewClient(new WebViewClient() {
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
      view.loadUrl(url);
      return true;
      }
      });

      js 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <!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 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      // 步骤1:加载JS代码
      // 格式规定为:file:///android_asset/文件名.html
      mWebView.loadUrl("file:///android_asset/javascript.html");
      // 复写WebViewClient类的shouldOverrideUrlLoading方法
      mWebView.setWebViewClient(new WebViewClient() {
      @Override
      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 代码。
      
    3. Android通过 WebChromeClientonJsAlert()onJsConfirm()onJsPrompt()方法回调分别拦截JS对话框 。

      整个 Android 和 js 的交互大致就是这几种,详情可以看该博文

  4. Andorid 多线程的线程锁

    Andorid 线程锁大致分为 2 种,同步机制关键字 synchronized 和 显示锁 Lock。

    1. 同步机制关键字 synchronized

      对于 java 来说,最常用的同步机制就是 synchronized 关键字,他是一种基于语言的粗略锁,能够作用于对象、函数、class。每个对象都只有一个锁,谁能够拿到这个锁谁就有访问权限。当 synchronized 作用于函数时,实际上锁的也是对象,锁定的对象就是该函数所在类的对象。而 synchronized 作用于 class 时则是锁的这个 Class 类,并非具体对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      public class SynchronizedClass {
      public synchronized void syncMethod(){
      //代码
      }
      public void syncThis(){
      synchronized (this){
      //代码
      }
      }
      public void syncClassMethod(){
      synchronized (SynchronizedClass.class){
      //代码
      }
      }
      public synchronized static void syncStaticMethod(){
      //代码
      }
      }
    2. 显示锁 Lock

      Lock 接口的实现类提供了比使用 synchronized 关键字更加灵活和广泛的锁定对象操作,而且是以面向对象的方式进行对象加锁。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class ReentrantLockDemo {
      Lock lock = new ReentrantLock();
      public void doSth(){
      lock.lock();
      try {
      //执行某些操作
      }finally {
      lock.unlock();
      }
      }
      }

      需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在 try 内,释放锁放在finally 内!!

    3. 读写锁 ReadWriteLock

      当我们读取与写入数据时,有这么一个需求:读与写互斥、写与写互斥、读与读不互斥。这里就必须用到读写锁.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      package 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(); // 释放读锁
      }
      }
      }
  5. 自定义 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 的绘制流程具体可以看这篇博文

  6. View 的事件分布机制

    1. Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。
    2. ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。
    3. 触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。
    4. 当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回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都不处理的情况下,触发Acitivity的onTouchEvent方法。
    7. onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

    view 事件分发机制具体可以看该博文

  7. Thread 和 AsyncTask

    其实 Thread 和 AsyncTaks 并不是一个等级的东西。

    AsyncTask是作为异步任务,执行除了UI界面更新的任务之外的其他耗时操作的。UI界面的更新是在主线程,也就是UI线程中执行的,而在这个异步任务中,开启了一个工作线程来执行耗时操作。而这个工作线程和UI线程的执行顺序是不同步的,也就是说只有执行完工作线程中的下载之后,才会调用UI线程中的onPostExecute()执行后续UI操作,这样就实现了异步下载。如果UI线程销毁之后工作线程再发送下载结束的信息,由于工作线程再使用过程中是与AsyncTask绑定的,所以他也会随着当前AsyncTask的销毁而销毁,不会执行后续的下载操作,自然也不会执行发送下载结束的信息。

    而用 Thread 开启简单的子线程执行下载,子线程和 UI 线程只能保持简单的同步关系,这个时候需要通过 Handler 进行异步操作。

    举例说明:

    Thread 开启简单的子线程下载,下载完成后进行 UI 操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 下载线程
    */
    private Thread downThread = new Thread(new Runnable() {
    @Override
    public void run() {
    //下载操作
    Message message = Message.obtain();
    message.what =1;
    message.obj = "data";
    handler.sendMessage(message);
    }
    });
    private Handler handler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
    if(1==msg.what){
    //UI 操作
    }
    return false;
    }
    });
    //最后在需要下载的地方 调用
    downThread.start();

    AsyncTask:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    class DownLoadAsyncTask extends AsyncTask<Void, Integer, Boolean> {
    @Override
    protected void onPreExecute() {
    super.onPreExecute();
    }
    @Override
    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();
    }
    }
    @Override
    protected void onProgressUpdate(Integer... values) {
    super.onProgressUpdate(values);
    }
    @Override
    protected void onCancelled(Boolean aBoolean) {
    super.onCancelled(aBoolean);
    }
    @Override
    protected void onCancelled() {
    super.onCancelled();
    }
    @Override
    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组件的信息。

    ⑤ 一个任务实例只能执行一次,如果执行第二次将会抛出异常。

  8. 手写一个创建 sql 的过程,并附带一个些基本增删改查操作。

    Android 本身并不提供关于数据库的内容,所以一般是使用轻量级的 sqlite 数据库。

    创建数据库时候,需要继承于 SQLiteOpenHelper 这个类进行数据库的创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public 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);
    }
    @Override
    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语句
    @Override
    public void onCreate(SQLiteDatabase db) {
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    //添加一个phone字段
    db.execSQL("alter table userinfo add phone varchar(11)");
    }
    }

    而在主 java 程序中,直接调用该类就可以创建一个 sql。

    1
    2
    3
    //比如在 onCreate 方法中加入
    MySqlHelper mySqlHelper = new MySqlHelper(this);
    SQLiteDatabase database = mySqlHelper.getReadableDatabase();

    然后后续所有的数据库操作都是通过 database 这个类进行,例如增删改查等等。

    而这些基础操作,都需要基于 Cursor 类来进行处理,个人觉得很麻烦,所有后面都采用了 GreenDao 这个ORM。关于 Curor 可以看看该博客

  9. GreenDao 的原理

    这个 ORM 简单几句完全说不清,本人处于只可意会不可言传这种状态。具体参考下面的几个博文:

    https://blog.csdn.net/u010687392/article/details/48465315

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

    我觉得里面说的非常详细了,其实 GreenDao 其实就是基于 Android 原生的 DataBase 进行一个封装。直接对象关系映射,这个是最简单方便的一个操作,并且简化了数据库的增删改查操作。

####暂时就更新这么多,后续看到有其他内容,会一一加上