Android interview share1

作者: hello_bin | 来源:发表于2017-03-23 14:20 被阅读55次

前言

Android interview

 大三下学期,正值春招之际,大家伙都在尝试着找实习了。前两天去了"广州久邦数码"面试Android开发实习生。初次参加技术面试,没有什么经验,虽说先前有在网上看一些面试经验和宝典,但是面试官一问起问题来,会有种似曾相识的感觉,回答起来模模糊糊,不够彻底,仔细一想应该是自己掌握程度不够,还得好好学习。
 下面我就此次面试中的一些题目做个总结,欢迎大家共同探讨,批评指正。
 PS:文章中多处贴着代码,如在手机上浏览可能会因为代码过长而导致体验不好,所以建议在电脑上阅读文章。

Android

-Listview如何优化?

对于Android开发者来说,listview是一个熟客了。它以列表的形式展示具体内容,并且能够根据数据的长度自适应显示。列表数据的显示需要4个元素,分别为:
(1)用来展示列表的Listview;
(2)用来把数据映射到Listview上的Adapter;
(3)需要展示的数据集;
(4)数据展示的View模板。
 Listview控件只负责加载、管理视图(每项数据称为Item View)。至于有多少数据项、每一项数据是如何显示的它并不关心,这一切交由adapter类去实现,通过adapter模式,用户只需要重写几个函数就可以将listview的每项数据构建出来。需要重写的函数有:
(1)getCount()函数:获取数据的个数;
(2)getItem(int )函数:获取position位置的数据;
(3)getItemId(int )函数:获取position位置的数据Id,一般直接返回position即可;
(4)getView(int View,ViewGroup)函数:获取position位置上的Item View视图。
 每个Item View是通过getView()函数实现,在这个函数中用户必须构建Item View,然后将该position位置上的数据绑定到Item View上。这样一来,数据和视图就绑定在一起了。
 但是,重点来了。并不是有多少数据项就会生成多少Item View,Android采用了视图复用的形式来避免创建过多的Item View,这样能够非常有效地提升性能和降低内存占用率。在处理数据量较大时,listview会构建铺满屏幕所需的Item View个数,当屏幕向下滚动时,第一项数据就会滚出屏幕的可见范围之内,并且进入listview的一个recycler中,recycler会将该视图缓存。此时要加载新插入的视图,listview会先从recycler中获取视图,如果视图存在,那么用户可以直接使用该缓存视图,否则才重新创建视图。也就是会有视图复用判断逻辑:

public View getView(int position,View covertView,ViewGroup parent){
  View view = null;
  // 有视图缓存,则复用视图
  if(covertView != null){
    view = covertView;
  }else{
    // 重新加载视图
  }
  // 进行数据绑定
  // 返回Item View
  return View;
}

总的来说,listview就是通过adapter模式、观察者模式、Item View复用机制实现了高效的列表显示。
  顺便提一下GridView,它与listview非常相似,同样继承自AbsListView,不同的是布局方式,GridView通过网格布局形式展示。本是同根生的listview和GridView有很好的的兼容性,同一个adapter可以设置给listview或者GridView,不需要半点修改。当然也可以同时设置给这两个视图,这样一来,两个视图都作为该adapter的观察者。

-XML格式数据、JSON格式数据如何解析?

数据要以什么样的格式在网络上传输呢?随便传递一段文本肯定是不行的,因为另一方不知道这段文本的用途是什么。因此,一般我们会在网络上传输一些格式化后的数据,这种数据会有一定的结构规格和语义,当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而取出他想要的那部分内容。在网络上传输数据最常用的格式有两种:XML和JSON。
 XML格式数据解析较常用的有两种:pull解析和sax解析。
pull解析(结合eg来讲):
 通过URL参数结合http协议访问服务器,得到服务器返回的数据。接下来,要获取到一个XmlPullParserFactory的实例,并借助这个实例得到XmlPullParser对象,然后调用XmlPullParser的setInput()方法将服务器返回的XML数据设置进去就可以开始解析了。解析的过程也非常简单,通过getEventType()可以得到当前的解析事件,然后在一个while循环中不断地进行解析,如果当前的解析事件不等于XMLPullParser.END_DOCUMENT,说明解析工作还没完成,调用next()方法后可以获取下一个解析事件。
 在while循环中,我们通过getName()方法得到当前结点的名字,如果发现结点名等于id、name或version,就调用nextText()方法来获取结点内具体的内容,每当解析完一个app结点后就将获取到的内容打印出来:

public class MainActivity extends Activity implements OnClickListener { 
  …… 
  private void sendRequestWithHttpClient() { 
    new Thread(new Runnable() { 
      @Override 
      public void run() {
        try { 
          HttpClient httpClient = new DefaultHttpClient(); 
          // 指定访问的服务器地址是电脑本机 
          HttpGet httpGet = new HttpGet("http://10.0.2.2/
get_data.json");
          HttpResponse httpResponse = httpClient.execute(httpGet);                                                 if(httpResponse.getStatusLine().getStatusCode()==200){ 
            // 请求和响应都成功了
            HttpEntity entity = httpResponse.getEntity(); 
            String response = EntityUtils.toString(entity,
"utf-8");
            parseJSONWithJSONObject(response);
            }catch (Exception e) {
            e.printStackTrace();
            }
}
  }).start();
  }
        private void parseXMLWithPull(String xmlData) { 
          try { 
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 
            XmlPullParser xmlPullParser = factory.newPullParser(); 
            xmlPullParser.setInput(new StringReader(xmlData));
            int eventType = xmlPullParser.getEventType(); 
            String id = ""; 
            String name = "";
            String version = ""; 
            while (eventType != XmlPullParser.END_DOCUMENT) { 
              String nodeName = xmlPullParser.getName(); 
              switch (eventType) {
                  // 开始解析某个结点 
                case XmlPullParser.START_TAG: { 
                  if ("id".equals(nodeName)) { 
                    id = xmlPullParser.nextText();
                  } else if ("name".equals(nodeName)) {
                    name = xmlPullParser.nextText();
                  } else if ("version".equals(nodeName)) { 
                    version = xmlPullParser.nextText(); 
                  } break; 
                }

// 完成解析某个结点 
                case XmlPullParser.END_TAG: { 
                  if ("app".equals(nodeName)) { 
                    Log.d("MainActivity", "id is " + id);
                    Log.d("MainActivity", "name is " + name); 
                    Log.d("MainActivity", "version is " + version);
                  }
                  break; 
                } 
                default: break; 
              } 
              eventType = xmlPullParser.next();
}
          } catch (Exception e) {
            e.printStackTrace();
          }
}
} 

sax解析:也是一种常用的解析方式,虽然它的用法比pull解析更复杂一些,但在语义方面会跟更加的清楚。
  通常情况下我们会新建一个类继承自Defaulthandler,并重写父类的5个方法:

public class MyHandler extends DefaultHandler { 
@Override 
public void startDocument() throws SAXException {
} 
@Override 
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
}
@Override 
public void characters(char[] ch, int start, int length) throws SAXException {
}
@Override
public void endElement(Stringuri,StringlocalName,StringqName)throws SAXException {
} 
@Override 
public void endDocument() throws SAXException {
} 
}

startDocument()方法会在开始XML解析的时候调用,startElement()方法会在开始解析某个结点的时候调用,characters()方法会在获取结点中内容的时候调用,endElement()方法会在完成解析某个结点的时候调用,endDocument()方法会在完成整个XML解析的时候调用。其中,startElement()、characters()、endElement()这三个方法是有参数的,从XML解析出的数据就会以参数的形式传入到这些方法中。需要注意的是,在获取结点中的内容时,characters()方法可能会被调用多次。
 在得到了服务器返回的数据后,我们这次去调用 parseXMLWithSAX()方法(自定义的)来解析 XML 数据。parseXMLWithSAX()方法中先是创建了一个 SAXParserFactory的对象,然后再获取到 XMLReader对象,接着将我们编写的 ContentHandler的实例设置到 XMLReader中,最后调 用 parse()方法开始执行解析就好了。

private void parseXMLWithSAX(String xmlData) { 
  try { 
    SAXParserFactory factory = SAXParserFactory.newInstance(); 
    XMLReader xmlReader = factory.newSAXParser().getXMLReader();
    ContentHandler handler = new ContentHandler(); 
    // 将ContentHandler(一个继承自Defaulthandler的类)的实例设置到XMLReader中
    xmlReader.setContentHandler(handler); 
    // 开始执行解析 
    xmlReader.parse(new InputSource(new StringReader(xmlData))); 
  } catch (Exception e) {
    e.printStackTrace(); 
  }
}
}

** JSON格式数据解析较常用的有两种:使用JSONObject、使用GSON。**

比起XML,JSON的主要优势在于它的体积更小,在网络上传输的时候可以更省流量。但缺点在于,它的语义性较差,看起来不如XML直观。
 JSONObject:得到服务器返回的数据后,我们可以通过getJSONObject(int position)方法来获取第i个JSON对象,然后再对应地取出字段内容。eg如下:

      假设文件中的JSON格式数据如下:
      [{"id":"5","version":"5.5","name":"Angry Birds"}, 
       {"id":"6","version":"7.0","name":"Clash of Clans"}, 
       {"id":"7","version":"3.5","name":"Hey Day"}]
       ……
        private void parseJSONWithJSONObject(String jsonData) {
          try { 
            JSONArray jsonArray = new JSONArray(jsonData); 
            for (int i = 0; i < jsonArray.length(); i++) { 
              JSONObject jsonObject = jsonArray.getJSONObject(i); 
              String id = jsonObject.getString("id"); 
              String name = jsonObject.getString("name"); 
              String version = jsonObject.getString("version"); 
              Log.d("MainActivity", "id is " + id); 
              Log.d("MainActivity", "name is " + name); 
              Log.d("MainActivity", "version is " + version);
              } 
          } catch (Exception e) {
            e.printStackTrace(); 
          }
    }
}                                      

GSON:谷歌提供的开源库可以让解析JSON数据的工作简单到让你不敢想象的地步,不过要使用此功能的话必须在项目中添加一个GSON的Jar包。。首先我们需要将GSON的资源压缩包下载下来, 下载地址是:http://code.google.com/p/google-gson/downloads/list。 其中 gson-2.2.4.jar这个文件就是我们所需要的了,现在将它拷贝到项目的 libs目录下,GSON库就会自动添加到项目中了。

那么GSON库究竟是神奇在哪里呢?其实它主要就是可以将一段JSON格式的字符串自 动映射成一个对象,从而不需要我们再手动去编写代码进行解析了。
 比如说一段 JSON格式的数据如下所示:

 {"name":"Tom","age":20} 

那我们就可以定义一个 Person类,并加入 name和 age这两个字段,然后只需简单地调 用如下代码就可以将 JSON数据自动解析成一个 Person对象了:

Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);

如果需要解析的是一段 JSON数组会稍微麻烦一点,我们需要借助 TypeToken将期望解 析成的数据类型传入到 fromJson()方法中,如下所示:

 List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>() {}.getType()); 

-Android网络通信技术

Android手机肯定也是可以上网的,所以作为开发者的我们就需要考虑 如何利用网络来编写出更加出色的应用程序,像 QQ、微博、新闻等常见的应用都会大量地 使用到网络技术。本章主要会讲述如何在手机端使用 HTTP协议和服务器端进行网络交互, 并对服务器返回的数据进行解析,这也是 Android中最常使用到的网络技术了。
 在 Android上发送 HTTP请求的方式一般有两种,HttpURLConnection和 HttpClient。
HttpURLConnection:
首先我们要获取到HttpURLConnection实例,一般只需new出一个URL对象,并传入目标的网络地址,然后调用一下 openConnection()方法即可,如下所示:

URL url = new URL("http://www.baidu.com"); 
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 

得到了 HttpURLConnection的实例之后,我们可以设置一下 HTTP请求所使用的方法。 常用的方法主要有两个,GET和 POST。GET表示希望从服务器那里获取数据,而 POST则 表示希望提交数据给服务器。写法如下:

 connection.setRequestMethod("GET"); 

接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服 务器希望得到的一些消息头等。这部分内容根据自己的实际情况进行编写,示例写法如下:

 connection.setConnectTimeout(8000); 
 connection.setReadTimeout(8000); 

之后再调用 getInputStream()方法就可以获取到服务器返回的输入流了,剩下的任务就是 对输入流进行读取,如下所示:

 InputStream in = connection.getInputStream();

最后可以调用 disconnect()方法将这个 HTTP连接关闭掉,如下所示:

 connection.disconnect(); 

那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将 HTTP请求 的方法改成 POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键 值对的形式存在,数据与数据之间用&符号隔开,比如说我们想要向服务器提交用户名和密 码,就可以这样写:

connection.setRequestMethod("POST"); 
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456"); 

HttpClient:
首先你需要知道,HttpClient是一个接口,因此无法创建它的实例,通常情况下都会创 建一个 DefaultHttpClient的实例,如下所示:

 HttpClient httpClient = new DefaultHttpClient(); 

接下来如果想要发起一条 GET请求,就可以创建一个 HttpGet对象,并传入目标的网络 地址,然后调用 HttpClient的 execute()方法即可:

 HttpGet httpGet = new HttpGet("http://www.baidu.com");
 httpClient.execute(httpGet); 

如果是发起一条 POST请求会比 GET稍微复杂一点,我们需要创建一个 HttpPost对象, 并传入目标的网络地址,如下所示:

HttpPost httpPost = new HttpPost("http://www.baidu.com"); 

然后通过一个 NameValuePair集合来存放待提交的参数,并将这个参数集合传入到一个 UrlEncodedFormEntity中,然后调用 HttpPost的 setEntity()方法将构建好的 UrlEncodedFormEntity 传入,如下所示:

List<NameValuePair> params = new ArrayList<NameValuePair>(); 
params.add(new BasicNameValuePair("username", "admin"));
params.add(new BasicNameValuePair("password", "123456"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "utf-8"); 
httpPost.setEntity(entity); 

接下来的操作就和 HttpGet一样了,调用 HttpClient的 execute()方法,并将 HttpPost对 象传入即可:

httpClient.execute(httpPost); 

执行 execute()方法之后会返回一个 HttpResponse对象,服务器所返回的所有信息就会包 含在这里面。通常情况下我们都会先取出服务器返回的状态码,如果等于 200就说明请求和响应都成功了,如下所示:

 if (httpResponse.getStatusLine().getStatusCode() == 200) { 
// 请求和响应都成功了
 } 

接下来在这个 if判断的内部取出服务返回的具体内容,可以调用 getEntity()方法获取到 一个 HttpEntity实例,然后再用 EntityUtils.toString()这个静态方法将 HttpEntity转换成字符串 即可,如下所示:

HttpEntity entity = httpResponse.getEntity(); 
String response = EntityUtils.toString(entity); 

注意如果服务器返回的数据是带有中文的,直接调用 EntityUtils.toString()方法进行转换 会有乱码的情况出现,这个时候只需要在转换的时候将字符集指定成 utf-8就可以了,如下 所示:
String response = EntityUtils.toString(entity, "utf-8");
由于要联网,所以要在AndroidManifest.xml里面加入连接网络的权限:

<uses-permission android:name="android.permission.INTERNET" />  

计算机基础

-线程和进程

进程概念
 几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
 一般而言,进程包含如下三个特征:
1.独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2.动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中是不具备的。
3.并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

这里需要注意:并发性和并行性是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。

进程的五种基本状态及转换

线程概念
 如何能使多个程序更好地并发执行,同时又尽量减少系统的开销,已成为操作系统所追求的重要目标。主要的改进方向有两点:①并不把其作为调度和分派的基本单位也要同时作为拥有资源的单位,以做到"轻装上阵";②对于拥有资源的基本单位,应不对之施以频繁的切换。于是,线程应运而生。
 线程是进程的组成成分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程的全部资源,因此编程更加方便。但也必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
 线程的特征:①调度的基本单位;②并发性;③拥有资源;④独立性;⑤支持多处理机系统。

多线程编程优势:
①进程之间不能共享内存,但线程之间共享内存非常容易;②系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;③Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

创建Java多线程
1、创建Thread的子类

创建Thread子类的一个实例并重写run方法,run方法会在调用start()方法之后被执行。例子如下:

public class MyThread extends Thread {
   public void run(){
     System.out.println("MyThread running");
   }
}

MyThread myThread = new MyThread();
myTread.start();

也可以如下创建一个Thread的匿名子类:

Thread thread = new Thread(){
   public void run(){
     System.out.println("Thread Running");
   }
};
thread.start();

2、实现Runnable接口

第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法可以被线程调用。下面给出例子:

public class MyRunnable implements Runnable {
   public void run(){
    System.out.println("MyRunnable running");
   }
}

Thread thread = new Thread(new MyRunnable());
thread.start();

同样,也可以创建一个实现了Runnable接口的匿名类,如下所示:

Runnable myRunnable = new Runnable(){
   public void run(){
     System.out.println("Runnable running");
   }
}
Thread thread = new Thread(myRunnable);
thread.start();

线程安全
 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
 当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

Java同步块
 Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。
 有四种不同的同步块:

  1. 实例方法
  2. 静态方法
  3. 实例方法中的同步块
  4. 静态方法中的同步块

实例方法同步:Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。

 public synchronized void add(int value){
    this.count += value;
 }

静态方法同步:静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。

public static synchronized void add(int value){
     count += value;
 }

实例方法中的同步块:注意Java同步块构造器用括号将对象括起来。下面例子中使用了“this”,即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。
 下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。

public class MyClass {
   public synchronized void log1(String msg1, String msg2){
      log.writeln(msg1);
      log.writeln(msg2);
   }
   public void log2(String msg1, String msg2){
      synchronized(this){
         log.writeln(msg1);
         log.writeln(msg2);
      }
   }
 }

静态方法中的同步块:下面两个方法不允许同时被线程访问。如果第二个同步块不是同步在MyClass.class这个对象上,那么这两个方法可以同时被线程访问。

public class MyClass {
    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }
    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2);
       }
    }
  }

Java线程通信
 线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。
 Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。
 一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。
 以下为一个使用了wait()和notify()实现的线程间通信的共享对象:

public class MyWaitNotify{
  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;
  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }
  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

注意以下几点:
1、不管是等待线程还是唤醒线程都在同步块里调用wait()和notify()。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。否则,会抛出IllegalMonitorStateException异常;
2、一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程也可以调用wait()或者notify();
3、为了避免丢失信号,必须把它们保存在信号类里。如上面的wasSignalled变量;
4、不要在字符串常量或全局对象中调用wait()。即上面MonitorObject不能是字符串常量或是全局对象。每一个MyWaitNotify的实例都拥有一个属于自己的监视器对象,而不是在空字符串上调用wait()/notify()。

后言
 由于文章篇幅不宜过长,所以问题没有全部囊括。感兴趣的朋友可以继续阅读我的另一篇简文(Android interview share2) 
 有什么不对的地方还望多多指教,也欢迎大家关注我(简书/GitHub
谢谢观看此文!

相关文章

网友评论

    本文标题:Android interview share1

    本文链接:https://www.haomeiwen.com/subject/doiznttx.html