前言
大三下学期,正值春招之际,大家伙都在尝试着找实习了。我也不例外,也在投递简历寻求面试机会,前两天去了”广州久邦数码”面试Android开发实习生。初次参加技术面试,没有什么经验,虽说先前有在网上看一些面试经验和宝典,但是面试官一问起问题来,会有种似曾相识的感觉,回答起来模模糊糊,不够彻底,仔细一想应该是自己掌握程度不够,还得好好学习。给个忠告,面试的时候态度一定要认真,不然很容易紧张,造成答题思路不清晰,会影响后面的回答。
下面我就此次面试题目以及答案做个总结,欢迎大家共同探讨。
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中获取视图,如果视图存在,那么用户可以直接使用该缓存视图,否则才重新创建视图。也就是会有视图复用判断逻辑:
1 2 3 4 5 6 7 8 9 10 11 12
| public View getView(int position,View covertView,ViewGroup parent){ View view = null; if(covertView != null){ view = covertView; }else{ } 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结点后就将获取到的内容打印出来:
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
| 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个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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()方法开始执行解析就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private void parseXMLWithSAX(String xmlData) { try { SAXParserFactory factory = SAXParserFactory.newInstance(); XMLReader xmlReader = factory.newSAXParser().getXMLReader(); ContentHandler handler = new ContentHandler(); 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 假设文件中的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格式的数据如下所示:
那我们就可以定义一个 Person类,并加入 name和 age这两个字段,然后只需简单地调 用如下代码就可以将 JSON数据自动解析成一个 Person对象了:
1 2
| Gson gson = new Gson(); Person person = gson.fromJson(jsonData, Person.class);
|
如果需要解析的是一段 JSON数组会稍微麻烦一点,我们需要借助 TypeToken将期望解 析成的数据类型传入到 fromJson()方法中,如下所示:
1
| 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()方法即可,如下所示:
1 2
| URL url = new URL("http://www.baidu.com"); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
得到了 HttpURLConnection的实例之后,我们可以设置一下 HTTP请求所使用的方法。 常用的方法主要有两个,GET和 POST。GET表示希望从服务器那里获取数据,而 POST则 表示希望提交数据给服务器。写法如下:
1
| connection.setRequestMethod("GET");
|
接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服 务器希望得到的一些消息头等。这部分内容根据自己的实际情况进行编写,示例写法如下:
1 2
| connection.setConnectTimeout(8000); connection.setReadTimeout(8000);
|
之后再调用 getInputStream()方法就可以获取到服务器返回的输入流了,剩下的任务就是 对输入流进行读取,如下所示:
1
| InputStream in = connection.getInputStream();
|
最后可以调用 disconnect()方法将这个 HTTP连接关闭掉,如下所示:
1
| connection.disconnect();
|
那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将 HTTP请求 的方法改成 POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键 值对的形式存在,数据与数据之间用&符号隔开,比如说我们想要向服务器提交用户名和密 码,就可以这样写:
1 2
| connection.setRequestMethod("POST"); DataOutputStream out = new DataOutputStream(connection.getOutputStream()); out.writeBytes("username=admin&password=123456");
|
HttpClient:
首先你需要知道,HttpClient是一个接口,因此无法创建它的实例,通常情况下都会创 建一个 DefaultHttpClient的实例,如下所示:
1
| HttpClient httpClient = new DefaultHttpClient();
|
接下来如果想要发起一条 GET请求,就可以创建一个 HttpGet对象,并传入目标的网络 地址,然后调用 HttpClient的 execute()方法即可:
1 2
| HttpGet httpGet = new HttpGet("http://www.baidu.com"); httpClient.execute(httpGet);
|
如果是发起一条 POST请求会比 GET稍微复杂一点,我们需要创建一个 HttpPost对象, 并传入目标的网络地址,如下所示:
1
| HttpPost httpPost = new HttpPost("http://www.baidu.com");
|
然后通过一个 NameValuePair集合来存放待提交的参数,并将这个参数集合传入到一个 UrlEncodedFormEntity中,然后调用 HttpPost的 setEntity()方法将构建好的 UrlEncodedFormEntity 传入,如下所示:
1 2 3 4 5
| 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对 象传入即可:
1
| httpClient.execute(httpPost);
|
执行 execute()方法之后会返回一个 HttpResponse对象,服务器所返回的所有信息就会包 含在这里面。通常情况下我们都会先取出服务器返回的状态码,如果等于 200就说明请求和响应都成功了,如下所示:
1 2 3
| if (httpResponse.getStatusLine().getStatusCode() == 200) { // 请求和响应都成功了 }
|
接下来在这个 if判断的内部取出服务返回的具体内容,可以调用 getEntity()方法获取到 一个 HttpEntity实例,然后再用 EntityUtils.toString()这个静态方法将 HttpEntity转换成字符串 即可,如下所示:
1 2
| HttpEntity entity = httpResponse.getEntity(); String response = EntityUtils.toString(entity);
|
注意如果服务器返回的数据是带有中文的,直接调用 EntityUtils.toString()方法进行转换 会有乱码的情况出现,这个时候只需要在转换的时候将字符集指定成 utf-8就可以了,如下 所示:
1
| String response = EntityUtils.toString(entity, "utf-8");
|
由于要联网,所以要在AndroidManifest.xml里面加入连接网络的权限:
1
| <uses-permission android:name="android.permission.INTERNET" />
|
-Android连接数据库
Android平台下与服务器数据库通信的方法主要有:
1、直接连接:在Android工程中引入jdbc驱动,采用驱动连接数据库;
2、间接连接:在服务器上用PHP+DBMS做服务器端,PHP将DBMS中的数据用JSON或者XML进行封装,然后将数据封装成接口,给Android平台回调。
注意:采用jdbc方法主要问题是安全性不高,而且一旦需要访问的数据过多,容易出问题。另外,Android本身有对JSON或者XML数据直接解析的API,所以间接连接的方式更为可靠。
JDBC连接
JDBC是Java Data Base Connectivity的缩写,意思为“Java数据库连接”,由一组用Java语言编写的类和接口组成,为java层直接操作关系型数据库提供了标准的API。原理很简单,主要是先服务器DBMS发送SQL(结构化查询语言)指令。实现各种数据库的操作。
在Android工程使用JDBC连接数据库的主要步骤:加载JDBC驱动程序——->建立连接———>发送SQL语句。
在项目中导入jdbc驱动,然后在代码开始出import jdbc的包。
建立链接:每种DBMS的JDBC驱动是不一样的,同一个DBMS也会有几种JDBC驱动,如Microsoft SQL Server的JDBC驱动主要有两种,Microsoft 官方提供的JDBC驱动和民间开源的JDBC驱动(JTDS),推荐JTDS,bug少,而且是完全开放源代码的。目前JTDS只能支持Microsoft SQL Server和Sybase。
由于DBMS与JDBC驱动的不同,所以每种JDBC连接数据库的字符串书写方法也是不一样的。 下面给出几种常见的JDBC与DBMS建立连接的字符串书写格式
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
| Connection con = null; Class.forName( "org.gjt.mm.mysql.Driver" ); con = DriverManager.getConnection( "jdbc:mysql://DbComputerNameOrIPAddr:3306/DatabaseName", UserName, Password ); Connection con = null; Class.forName("org.postgresql.Driver" ); con = DriverManager.getConnection( "jdbc:postgresql://DbComputerNameOrIPAddr/DatabaseName", UserName, Password ); Connection con = null; Class.forName( "oracle.jdbc.driver.OracleDriver" ); con = DriverManager.getConnection( "jdbc:oracle:thin:@DbComputerNameOrIPAddr:1521:DatabaseName", UserName, Password ); Connection con = null; Class.forName( "com.sybase.jdbc2.jdbc.SybDriver" ); con = DriverManager.getConnection( "jdbc:sybase:Tds:DbComputerNameOrIPAddr:2638/DatabaseName", UserName, Password ); Connection con = null; Class.forName( "net.sourceforge.jtds.jdbc.Driver" ); con = DriverManager.getConnection( "jdbc:jtds:sqlserver://DbComputerNameOrIPAddr:1433/DatabaseName", UserName, Password ); Connection con = null; Class.forName( "com.microsoft.jdbc.sqlserver.SQLServerDriver" ); con = DriverManager.getConnection( "jdbc:microsoft:sqlserver://DbComputerNameOrIPAddr:1433;databaseName=master", UserName, Password );
|
发送SQL语句(以SQL server为例),当成功连接数据库之后,就可以发送操作数据的语句并处理结果了。在发送SQL语句之前,首先要创建一个statement对象,statement对象主要工作是把SQL语句发送到DBMS。然后发送SQL语句。对于SELECT操作,使用的是Statement对象的executeQuery(sql )方法,对于一些创建table和修改table的操作,使用的是Statement对象的executeUpdate(sql )方法。
1 2 3 4 5
| // 查询表名为“table_test”的所有内容 String sql = "SELECT * FROM table_test"; // 创建Statement Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(sql);
|
接口回调
调用自己服务器接口返回的数据,需要提供服务器地址,然后通过网络通信技术解析服务器返回的数据。
-Android图片处理、图片缓存机制
说到图片处理,我们得重点学习一下两个类:Bitmap、BitmapFactory。
Bitmap
Bitmap是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
| public void recycle() // 回收位图占用的内存空间,把位图标记为Dead public final boolean isRecycled() //判断位图内存是否已释放 public final int getWidth()//获取位图的宽度 public final int getHeight()//获取位图的高度 public final boolean isMutable()//图片是否可修改 public int getScaledWidth(Canvas canvas)// 获取指定密度转换后的图像的宽度 public int getScaledHeight(Canvas canvas)//获取指定密度转换后的图像的高度 public boolean compress(CompressFormat format, int quality, OutputStream stream)//按指定的图片格式以及画质,将图片转换为输出流。 format:Bitmap.CompressFormat.PNG或Bitmap.CompressFormat.JPEG quality:画质,0-100.0表示最低画质压缩,100以最高画质压缩。对于PNG等无损格式的图片,会忽略此项设置。 public static Bitmap createBitmap(Bitmap src) // 以src为原图生成不可变得新图像 public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)//以src为原图,创建新的图像,指定新图像的高宽以及是否可变。 public static Bitmap createBitmap(int width, int height, Config config)——创建指定格式、大小的位图 public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)以source为原图,创建新的图片,指定起始坐标以及新图像的高宽。
|
BitmapFactory
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
| Option 参数类: public boolean inJustDecodeBounds//如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息。 public int inSampleSize //图片缩放的倍数 public int outWidth//获取图片的宽度值 public int outHeight//获取图片的高度值 public int inDensity//用于位图的像素压缩比 public int inTargetDensity //用于目标位图的像素压缩比(要生成的位图) public byte[] inTempStorage //创建临时文件,将图片存储 public boolean inScaled//设置为true时进行图片压缩,从inDensity到inTargetDensity public boolean inDither //如果为true,解码器尝试抖动解码 public Bitmap.Config inPreferredConfig //设置解码器 public String outMimeType //设置解码图像 public boolean inPurgeable//当存储Pixel的内存空间在系统内存不足时是否可以被回收 public boolean inInputShareable //inPurgeable为true情况下才生效,是否可以共享一个InputStream public boolean inPreferQualityOverSpeed //为true则优先保证Bitmap质量其次是解码速度`` public boolean inMutable //配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段 public int inScreenDensity //当前屏幕的像素密度 工厂方法: public static Bitmap decodeFile(String pathName, Options opts) //从文件读取图片 public static Bitmap decodeFile(String pathName) public static Bitmap decodeStream(InputStream is) //从输入流读取图片 public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) public static Bitmap decodeResource(Resources res, int id) //从资源文件读取图片 public static Bitmap decodeResource(Resources res, int id, Options opts) public static Bitmap decodeByteArray(byte[] data, int offset, int length) //从数组读取图片 public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) public static Bitmap decodeFileDescriptor(FileDescriptor fd)//从文件读取文件 与decodeFile不同的是这个直接调用JNI函数进行读取 效率比较高 public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts) Bitmap.Config inPreferredConfig : 枚举变量 (位图位数越高代表其可以存储的颜色信息越多,图像越逼真,占用内存越大) public static final Bitmap.Config ALPHA_8 //代表8位Alpha位图 每个像素占用1byte内存 public static final Bitmap.Config ARGB_4444 //代表16位ARGB位图 每个像素占用2byte内存 public static final Bitmap.Config ARGB_8888 //代表32位ARGB位图 每个像素占用4byte内存 public static final Bitmap.Config RGB_565 //代表8位RGB位图 每个像素占用2byte内存
|
Android中一张图片(BitMap)占用的内存主要和以下几个因数有关:图片长度,图片宽度,单位像素占用的字节数。一张图片(BitMap)占用的内存 = 图片长度 图片宽度 单位像素占用的字节数。
下面列举几个常用方法的详细介绍:
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
| * 获取缩放后的本地图片 * * @param filePath 文件路径 * @param width 宽 * @param height 高 * @return */ public static Bitmap readBitmapFromFileDescriptor(String filePath, int width, int height) { try { FileInputStream fis = new FileInputStream(filePath); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); float srcWidth = options.outWidth; float srcHeight = options.outHeight; int inSampleSize = 1; if (srcHeight > height || srcWidth > width) { if (srcWidth > srcHeight) { inSampleSize = Math.round(srcHeight / height); } else { inSampleSize = Math.round(srcWidth / width); } } options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); } catch (Exception ex) { } return null; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| /** * 将图片保存至文件 * * @param filePath 文件路径 * @param b bitmap图片对象 * @param quality 图片画质 */ public static void writeBitmapToFile(String filePath, Bitmap b, int quality) { try { File desFile = new File(filePath); FileOutputStream fos = new FileOutputStream(desFile); BufferedOutputStream bos = new BufferedOutputStream(fos); b.compress(Bitmap.CompressFormat.JPEG, quality, bos); bos.flush(); bos.close(); } catch (IOException e) { e.printStackTrace(); } }
|
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
| * 图片压缩 * * @param image 压缩的图片对象 * @return 返回bitmap对象 */ private static Bitmap compressImage(Bitmap image) { if (image == null) { return null; } ByteArrayOutputStream baos = null; try { baos = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] bytes = baos.toByteArray(); ByteArrayInputStream isBm = new ByteArrayInputStream(bytes); Bitmap bitmap = BitmapFactory.decodeStream(isBm); return bitmap; } catch (OutOfMemoryError e) { } finally { try { if (baos != null) { baos.close(); } } catch (IOException e) { } } return null; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| * 图片旋转角度 * * @param b 旋转的图片对象 * @param rotateDegree 旋转角度 * @return 返回bitmap对象 */ private static Bitmap rotateBitmap(Bitmap b, float rotateDegree) { if (b == null) { return null; } Matrix matrix = new Matrix(); matrix.postRotate(rotateDegree); Bitmap rotaBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, true); return rotaBitmap; }
|
三级图片缓存机制
在开发安卓应用中避免不了要使用到网络图片,获取网络图片很简单,但是需要付出一定的代价—流量。对于少数的图片而言问题不大,但如果手机应用中包含大量的图片,这势必会耗费用户的一定流量,如果我们不加以处理,每次打开应用都去网络获取图片,用户可就不乐意了,所以我们得用三级缓存策略(缓存层分为三层:内存层,磁盘层,网络层),如果内存或者本地磁盘中已有所需图片,就不用通过网络层获取图片,减少流量的损耗。
关于缓存层的工作,当我们第一次打开应用获取图片时,先到网络去下载图片,然后依次存入内存缓存,磁盘缓存,当我们再一次需要用到刚才下载的这张图片时,就不需要再重复的到网络上去下载,直接可以从内存缓存和磁盘缓存中找,由于内存缓存速度较快,我们优先到内存缓存中寻找该图片,如果找到则运用,如果没有找到(内存缓存大小有限),那么我们再到磁盘缓存中去找。只要我们合理的去协调这三层缓存运用,便可以提升应用性能和用户体验。
1、内存层:(手机内存)
内存缓存相对于磁盘缓存而言,速度要来的快很多,但缺点容量较小且会被系统回收,这里的实现我用到了LruCache。
LruCache这个类是Android3.1版本中提供的,如果你是在更早的Android版本中开发,则需要导入android-support-v4的jar包。
2、磁盘层:(SD卡)
相比内存缓存而言速度要来得慢很多,但容量很大,这里的实现我用到了DiskLruCache类。
DiskLruCache是非Google官方编写,但获得官方认证的硬盘缓存类,该类没有限定在Android内,所以理论上java应用也可以使用DiskLreCache来缓存。
这是DiskLruCache类的下载地址:http://pan.baidu.com/s/1o6tPjz8
3、网络层:(移动网络,无线网络)
这个就没什么解释的了,就是我们上网用的流量。网络访问实现可以用开源框架Volley。
开源框架Volley是2013年Google I/O大会发布的,Volley是Android平台上的网络通信库,能使网络通信更快,更简单,更健壮。它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。
这是Volley的下载地址:http://pan.baidu.com/s/1kThedX9
计算机基础
-数组和链表的区别
数组结构在通过索引进行查询数据时效率比较高,而对于数组插入和删除操作,则效率会比较低,在第一个位置进行插入数据,其余数据就需要依次向后移动,而第一个数据进行删除,则需要所有数据全部向前移。
链表:为了保证数据插入和删除,不会影响其他数据的移动,保证线性开销,就引出了指针链接,链表是由一系列节点组成的,每个节点都会有一个链点,next链,next链会执行下一个node的引用,所以我们在插入或者删除的时候,需要链表next链的指向地址即可,每个节点不需要内存进行连续存储,这样会减小删除和插入的线性开销。链表结构主要分为两种链表,单向链表和双向链表 ,即单向链表只有一个next链,而双向链表会有next链和pre链。
简单的区别如下:
①数组静态分配内存,链表动态分配内存;②数组在内存中连续,链表不连续;③数组元素在栈区,链表元素在堆区;④数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);⑤数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。
数组与链表的优缺点:
数组:
优点:使用方便 ,查询效率比链表高,内存为一连续的区域 。 缺点:大小固定,不适合动态存储,不方便动态添加。
链表:
优点:可动态添加删除,大小可变。 缺点:只能通过顺次指针访问,查询效率低。
-线程和进程
进程概念
几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
一般而言,进程包含如下三个特征:
1.独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2.动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中是不具备的。
3.并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
这里需要注意:并发性和并行性是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。
线程概念
如何能使多个程序更好地并发执行,同时又尽量减少系统的开销,已成为操作系统所追求的重要目标。主要的改进方向有两点:①并不把其作为调度和分派的基本单位也要同时作为拥有资源的单位,以做到”轻装上阵”;②对于拥有资源的基本单位,应不对之施以频繁的切换。于是,线程应运而生。
线程是进程的组成成分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程的全部资源,因此编程更加方便。但也必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
线程的特征:①调度的基本单位;②并发性;③拥有资源;④独立性;⑤支持多处理机系统。
多线程编程优势:
①进程之间不能共享内存,但线程之间共享内存非常容易;②系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;③Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
创建Java多线程
1、创建Thread的子类
创建Thread子类的一个实例并重写run方法,run方法会在调用start()方法之后被执行。例子如下:
1 2 3 4 5 6 7 8
| public class MyThread extends Thread { public void run(){ System.out.println("MyThread running"); } } MyThread myThread = new MyThread(); myTread.start();
|
也可以如下创建一个Thread的匿名子类:
1 2 3 4 5 6
| Thread thread = new Thread(){ public void run(){ System.out.println("Thread Running"); } }; thread.start();
|
2、实现Runnable接口
第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法可以被线程调用。下面给出例子:
1 2 3 4 5 6 7 8
| public class MyRunnable implements Runnable { public void run(){ System.out.println("MyRunnable running"); } } Thread thread = new Thread(new MyRunnable()); thread.start();
|
同样,也可以创建一个实现了Runnable接口的匿名类,如下所示:
1 2 3 4 5 6 7
| 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中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。
有四种不同的同步块:
- 实例方法
- 静态方法
- 实例方法中的同步块
- 静态方法中的同步块
实例方法同步:Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。
1 2 3
| public synchronized void add(int value){ this.count += value; }
|
静态方法同步:静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。
1 2 3
| public static synchronized void add(int value){ count += value; }
|
实例方法中的同步块:注意Java同步块构造器用括号将对象括起来。下面例子中使用了“this”,即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。
下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。
1 2 3 4 5 6 7 8 9 10 11 12
| 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这个对象上,那么这两个方法可以同时被线程访问。
1 2 3 4 5 6 7 8 9 10 11 12
| 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()实现的线程间通信的共享对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } 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()。