Android开发面试经典题目

Android知识点整理 专栏收录该内容
2 篇文章 0 订阅

下面是整理的Android知识点,可以平时巩固知识点,仅供复习使用。

☆  JAVA技能

  • 有良好的JAVA基础,熟练掌握面向对象思想:

理解面向对象:

面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。面向对象有三大特征:封装性、继承性、多态性,其中封装性指的是隐藏了对象的属性和实现细节,仅对外提供公共的访问方式,这样就隔离了具体的变化,便于使用,提高了复用性和安全性。对于继承性,就是两种事物间存在着一定的所属关系,那么继承的类就可以从被继承的类中获得一些属性和方法;这就 提高了代码的复用性。继承是作为多态的前提的。多态是说父类或接口的引用指向了子类对A象,这就提高了程序的扩展性,也就是说只要实现或继承了同一个接口或类,那么就可以使用父类中相应的方法,提高程序扩展性,但是多态有一点不好之处在于:父类引用不能访问子类中的成员。

举例来说:就是:比如说你要去饭店吃饭,你只需要饭店,找到饭店的服务员,跟她说你要吃什么,然后叫会给你做出来让你吃,你并不需要知道这个饭是怎么错的,你只需要面向这个服务员,告诉他你要吃什么,然后他也只需要面向你吃完收到钱就好,不需要知道你怎么对这个饭进行吃。

1、特点:

1:将复杂的事情简单化。

       2:面向对象将以前的过程中的执行者,变成了指挥者。

       3:面向对象这种思想是符合现在人们思考习惯的一种思想。

2、面向对象的三大特征:封装,继承、多态

1.封装:只隐藏对象的属性和实现细节,仅对外提供公共访问方式

好处:将变化隔离、便于使用、提高复用性、提高安全性

原则:将不需要对外提供的内容隐藏起来;把属性隐藏,提供公共方法对其访问

2.继承:提高代码复用性;继承是多态的前提

注:

①子类中所有的构造函数都会默认访问父类中的空参数的构造函数,默认第一行有super();若无空参数构造函数,子类中需指定;另外,子类构造函数中可自己用this指定自身的其他构造函数。

3.多态

是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象

好处:提高了程序的扩展性

弊端:当父类引用指向子类对象时,虽提高了扩展性,但只能访问父类中具备的方法,不可访问子类中的方法;即访问的局限性。

前提:实现或继承关系;覆写父类方法。

 

  • 熟练使用集合、IO流及多线程

一、集合:

1、特点:存储对象;长度可变;存储对象的类型可不同;

2、集合框架

2Collection

1List有序的;元素可重复,有索引

(add(index, element)、add(index, Collection)、remove(index)、set(index,element)、get(index)、subList(from, to)、listIterator())

ArrayList:底层是数组结构,查询快,增删慢,不同步。

LinkedList:底层是链表结构,增删快,查询慢,不同步

addFist();addLast()  getFirst();getLast()

removeFirst();removeLast() 获取并删除元素,无元素将抛异常:NoSuchElementException

替代的方法(JDK1.6):

offerFirst();offerLast();

peekFirst();peekLast();无元素返回null

pollFirst();pollLast();删除并返回此元素,无元素返回null     

Vector:底层是数组结构,线程同步,被ArrayList取代了

注:了对于判断是否存在,以及删除等操作,以依赖的方法是元素的hashCodeequals方法

ArrayList判断是否存在和删除操作依赖的是equals方法

2Set:无序的,无索引,元素不可重复

HashSet:底层是哈希表,线程不同步,无序、高效

保证元素唯一性:通过元素的hashCode和equals方法。若hashCode值相同,则会判断equals的结果是否为true;hashCode不同,不会调用equals方法

LinkedHashSet:有序,是HashSet的子类

TreeSet:底层是二叉树,可对元素进行排序,默认是自然顺序

       保证唯一性:Comparable接口的compareTo方法的返回值

===TreeSet两种排序方式:两种方式都存在时,以比较器为主

第一种:自然排序(默认排序):

       添加的对象需要实现Comparable接口,覆盖compareTo方法

第二种:比较器

       添加的元素自身不具备比较性或不是想要的比较方式。将比较器作为参数传递进去。

       定义一个类,实现Comparator接口,覆盖compare方法。当主要条件相同时,比较次要条件。

 

3Map集合:

1HashTable:底层数据结构是哈希表,不可存入null键和null值。同步的

       Properties继承自HashTable,可保存在流中或从流中加载,是集合和IO流的结合产物

2HashMap:底层数据结构是哈希表;允许使用null键和null值,不同步,效率高

       TreeMap

              底层数据结构时二叉树,不同步,可排序

              Set很像,Set底层就是使用了Map集合

方法:

V put(K key, V value) ;  void putAll(Map m)

void clear();  V remove(Object key)

boolean containsKey(Object key);  containsValue(Object key);  isEmpty()

V get(Object key); int size(); Collection<V> values()

Set<K> keySet();  Set<Map.Entry<K,V>> entrySet()

 

2.3Map集合两种取出方式:

第一种:Set<K> keySet()

       取出Map集合中的所有键放于Set集合中,然后再通过键取出对应的值

Set<String> keySet = map.keySet();

Iterator<String> it = keySet.iterator();

while(it.hasNext()){

       String key = it.next();

       String value = map.get(key);

//…..

}

第二种:Set<Map.Entry<K,V>> entrySet()

       取出Map集合中键值对的映射放于Set集合中,然后通过Map集合中的内部接口,然后通过其中的方法取出

Set<Map.Entry<String,String>> entrySet = map.entrySet();

Iterator<Map.Entry<String,String>> it = entrySet.iterator();

While(it.hasNext()){

       Map.Entry<String,String> entry = it.next();

       String key = entry.getKey();

       String value = entry.getValue();

       //……

}

 

2.4CollectionMap的区别:

Collection:单列集合,一次存一个元素

Map:双列集合,一次存一对集合,两个元素(对象)存在着映射关系

 

2.5、集合工具类:

Collections:操作集合(一般是list集合)的工具类。方法全为静态的

sort(List list);list集合进行排序; sort(List list, Comparator c) 按指定比较器排序

fill(List list, T obj);将集合元素替换为指定对象;

swap(List list, int I, int j)交换集合指定位置的元素

shuffle(List list); 随机对集合元素排序

reverseOrder() :返回比较器,强行逆转实现Comparable接口的对象自然顺序

reverseOrder(Comparator c):返回比较器,强行逆转指定比较器的顺序

 

2.6CollectionCollections的区别:

Collectionsjava.util下的工具类,实现对集合的查找、排序、替换、线程安全化等操作。

Collection:是java.util下的接口,是各种单列集合的父接口,实现此接口的有ListSet集合,存储对象并对其进行操作。

 

3Arrays

       用于操作数组对象的工具类,全为静态方法

asList():将数组转为list集合

       好处:可通过list集合的方法操作数组中的元素:

isEmpty()contains()indexOf()set()

       弊端:数组长度固定,不可使用集合的增删操作。

如果数组中存储的是基本数据类型,asList会将数组整体作为一个元素存入集合

集合转为数组:Collection.toArray()

       好处:限定了对集合中的元素进行增删操作,只需获取元素

 

二、IO

1、结构:

 

字节流:InputStreamOutputStream

字符流:ReaderWriter

Reader:读取字符流的抽象类

       BufferedReader:将字符存入缓冲区,再读取

              LineNumberReader:带行号的字符缓冲输入流

       InputStreamReader:转换流,字节流和字符流的桥梁,多在编码的地方使用

              FileReader:读取字符文件的便捷类。

 

Writer:写入字符流的抽象类

       BufferedWriter:将字符存入缓冲区,再写入

       OutputStreamWriter:转换流,字节流和字符流的桥梁,多在编码的地方使用

              FileWriter:写入字符文件的便捷类。

 

InputStream:字节输入流的所有类的超类

       ByteArrayInputStream:含缓冲数组,读取内存中字节数组的数据,未涉及流

       FileInputStream:从文件中获取输入字节。媒体文件

              BufferedInputStream:带有缓冲区的字节输入流

              DataInputStream:数据输入流,读取基本数据类型的数据

       ObjectInputStream:用于读取对象的输入流

       PipedInputStream:管道流,线程间通信,与PipedOutputStream配合使用

       SequenceInputStream:合并流,将多个输入流逻辑串联。

OutputStream:此抽象类是表示输出字节流的所有类的超类

       ByteArrayOutputStream:含缓冲数组,将数据写入内存中的字节数组,未涉及流

       FileOutStream:文件输出流,将数据写入文件

              BufferedOutputStream:带有缓冲区的字节输出流

              PrintStream:打印流,作为输出打印

              DataOutputStream:数据输出流,写入基本数据类型的数据

       ObjectOutputStream:用于写入对象的输出流

       PipedOutputStream:管道流,线程间通信,与PipedInputStream配合使用

2、流操作规律:

       明确源和目的:

              数据源:读取,InputStreamReader

              目的:写入:OutStreamWriter

       数据是否是纯文本:

              是:字符流,ReaderWriter

              否:字节流,InputStreamOutStream

       明确数据设备:

              源设备:内存、硬盘、键盘

              目的设备:内存、硬盘、控制台

       是否提高效率:用BufferedXXX

3、转换流:将字节转换为字符,可通过相应的编码表获得

       转换流都涉及到字节流和编码表

 

三、多线程

--à进程和线程:

1)进程是静态的,其实就是指开启的一个程序;而线程是动态的,是真正执行的单元,执行的过程。其实我们平时看到的进程,是线程在执行着,因为线程是作为进程的一个单元存在的。

2)同样作为基本的执行单元,线程是划分得比进程更小的执行单位。

3)每个进程都有一段专用的内存区域。与此相反,线程却共享内存单元(包括代码和数据),通过共享的内存单元来实现数据交换、实时通信与必要的同步操作。

1、创建线程的方式:

创建方式一:继承Thread

    1:定义一个类继承Thread

    2:覆盖Thread中的run方法(将线程运行的代码放入run方法中)。

    3:直接创建Thread的子类对象

    4:调用start方法(内部调用了线程的任务(run方法));作用:启动线程,调用run方法

 

方式二:实现Runnable

    1:定义类实现Runnable接口

    2:覆盖Runnable接口中的run方法,将线程的任务代码封装到run中

    3:通过Thread类创建线程对象

4、并将Runnable接口的子类对象作为Thread类的构造函数参数进行传递

作为参数传递的原因是让线程对象明确要运行的run方法所属的对象。

区别:

       继承方式:线程代码放在Thread子类的run方法中

       实现方式:线程存放在接口的子类run方法中;避免了单继承的局限性,建议使用。

2、线程状态:

新建:start()

临时状态:具备cpu的执行资格,但是无执行权

运行状态:具备CPU的执行权,可执行

冻结状态:通过sleep或者wait使线程不具备执行资格,需要notify唤醒,并处于临时状态。

消亡状态:run方法结束或者中断了线程,使得线程死亡。

3、多线程安全问题:

多个线程共享同一数据,当某一线程执行多条语句时,其他线程也执行进来,导致数据在某一语句上被多次修改,执行到下一语句时,导致错误数据的产生。

因素:多个线程操作共享数据;多条语句操作同一数据

解决:

       原理:某一时间只让某一线程执行完操作共享数据的所有语句。

       办法:使用锁机制:synchronizedlock对象

4、线程的同步:

当两个或两个以上的线程需要共享资源,他们需要某种方法来确定资源在某一刻仅被一个线程占用,达到此目的的过程叫做同步(synchronization)。

同步代码块:synchronized(对象){},将需要同步的代码放在大括号中,括号中的对象即为锁。

同步函数:放于函数上,修饰符之后,返回类型之前。

5waitsleep的区别:(执行权和锁区分)

wait:可指定等待的时间,不指定须由notifynotifyAll唤醒。

       线程会释放执行权,且释放锁。

sleep:必须制定睡眠的时间,时间到了自动处于临时(阻塞)状态。

       即使睡眠了,仍持有锁,不会释放执行权。

 

Android 的进程与线程:

1、进程的生命周期:

1)、进程的创建及回收:

       进程是被系统创建的,当内存不足的时候,又会被系统回收

2)、进程的级别:

Foreground Process       前台进程

Visible Process              可视进程

Service Process             服务进程:可以提高级别的

Background Process       后台进程

Empty Process              空进程(无组件启动,做进程缓存使用,恢复速度快)

 

 

☆  Android技能

  • 熟练掌握Android四大组件,常用的布局文件,自定义控件等

Android中4大组件是:ContentProvider、Activity、BroadcastReceiver和Service

清单文件:

1、所有的应用程序必须要有清单文件

在manifest节点下需要声明当前应用程序的包名

2、包名:声明包的名字,必须唯一

       如果两个应用程序的包名和签名都相同,后安装的会覆盖先安装的

3、声明的程序的组件(4大组件)

       其中比较特殊的是广播接收者,可以不在清单文件中配置,可以通过代码进行注册

4、声明程序需要的权限:保护用户的隐私

5、可以控制服务在单独的进程中的,四大组件都可以配置这个属性process

在组件节点配置process:

       如:android:process="xxx.ooo.xxx"

比如说:处理图片的时候,会很耗内存,就需要在单独的新的进程中,可以减少内存溢出的几率

 

 

一、ContentProvider 内容提供者

1、特点

①、可以将应用中的数据对外进行共享;

②、数据访问方式统一,不必针对不同数据类型采取不同的访问策略;

③、内容提供者将数据封装,只暴露出我们希望提供给其他程序的数据(这点有点类似Javabeans);

④、内容提供者中数据更改可被监听;

 

2、创建内容提供者

  • 定义类继承ContentProvider,根据需要重写其内容方法(6个方法):
  1. onCreate()                                    创建内容提供者时,会调用这个方法,完成一些初始化操作;
  2. crud相应的4个方法              用于对外提供CRUD操作;
  3. getType()                                      返回当前Url所代表数据的MIME类型:

返回的是单条记录:以vnd.android.cursor.item/ 开头,如:vnd.android.cursor.item/person

返回的是多条记录:以vnd.android.cursor.dir/ 开头,如:vnd.android.cursor.dir/person

  • 在清单文件的<application>节点下进行配置,<provider>标签中需要指定name、authorities、exported属性
    1. name:             为全类名;
    2. authorities:   是访问Provider时的路径,要唯一;
    3. exported:      用于指示该服务是否能够被其他应用程序组件调用或跟它交互
  • URI代表要操作的数据,由scheme、authorites、path三部分组成:
  1. content://com.itheima.sqlite.provider/person
  2. scheme:         固定为content,代表访问内容提供者;
  3. authorites:    <provider>节点中的authorites属性;
  4. path:               程序定义的路径,可根据业务逻辑定义;
  • 操作 URI的UriMather与ContentUris工具类:

       当程序调用CRUD方法时会传入Uri

  1. UriMatcher:表示URI匹配器,可用于添加Uri匹配模式,与匹配Uri(见下代码);
  1. ContentUris:用于操作Uri路径后面的ID部分,2个重要的方法:
  1. withAppendedId(uri, id)  为路径加上ID部分;
  2. parseId(uri) 用于从路径中获取ID部分;

 

示例代码(内容提供者类):

public class HeimaProvider extends ContentProvider {

       private static final int PERSON = 1;                   // 匹配码

       private static final int STUDENT = 2;                // 匹配码

       private static final int PERSON_ID = 3;             // 匹配码

       private MyHelper helper;

       /** Uri匹配器 */

       private UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

       @Override

       public boolean onCreate() {

              System.out.println("onCreate...");

              helper = new MyHelper(getContext());

              // == 添加 uri 匹配模式, 设置匹配码(参数3) Uri如果匹配就会返回相应的匹配码 ==

              uriMatcher.addURI("com.itheima.sqlite.provider", "person", PERSON);                                       

              uriMatcher.addURI("com.itheima.sqlite.provider", "#", PERSON_ID);                          // #表示匹配数字,*表示匹配文本

              uriMatcher.addURI("com.itheima.sqlite.provider", "student", STUDENT);

              return true;

       }

       @Override

       public Uri insert(Uri uri, ContentValues values) {

              SQLiteDatabase db = helper.getWritableDatabase();

              switch (uriMatcher.match(uri)) {                                      // 匹配uri

              case PERSON:

                     long id = db.insert("person", "id", values);

                     db.close();

                     return ContentUris.withAppendedId(uri, id);                     // 在原uri上拼上id,生成新的uri并返回;  

              case STUDENT:

                     long insert = db.insert("student", "id", values);

                     System.out.println("数据文件中,没有student表,也不会报错");

                     db.close();

                     return ContentUris.withAppendedId(uri, insert);         // 为路径上,加上ID

              default:

                     throw new IllegalArgumentException(String.format("Uri:%s 不是合法的uri地址", uri));

              }

       }

       @Override

       public int delete(Uri uri, String selection, String[] selectionArgs) {

              SQLiteDatabase db = helper.getWritableDatabase();

              switch (uriMatcher.match(uri)) {                                             // 匹配uri

              case PERSON_ID:

                     long parseId = ContentUris.parseId(uri);                           // 获取传过来的ID值

                     selection = "id=?";                                                          // 设置查询条件

                     selectionArgs = new String[] { parseId + "" };                  // 查询条件值

              case PERSON:

                     int delete = db.delete("person", selection, selectionArgs);

                     db.close();

                     return delete;

              default:

                     throw new IllegalArgumentException(String.format("Uri:%s 不是合法的uri地址", uri));

              }

       }

       @Override

       public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

              SQLiteDatabase db = helper.getWritableDatabase();

              switch (uriMatcher.match(uri)) {

              case PERSON_ID:

                     long parseId = ContentUris.parseId(uri);                           // 获取传过来的ID值

                     selection = "id=?";                                                          // 设置查询条件

                     selectionArgs = new String[] { parseId + "" };                  // 查询条件值

              case PERSON:

                     int update = db.update("person", values, selection, selectionArgs);

                     db.close();

                     return update;

              default:

                     throw new IllegalArgumentException(String.format("Uri:%s 不是合法的uri地址", uri));

              }

       }

       @Override

       public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

              SQLiteDatabase db = helper.getWritableDatabase();

              switch (uriMatcher.match(uri)) {

              case PERSON_ID:

                     // == 根据ID查询 ==

                     long parseId = ContentUris.parseId(uri);                           // 获取传过来的ID值

                     selection = "id=?";                                                          // 设置查询条件

                     selectionArgs = new String[] { parseId + "" };                  // 查询条件值

              case PERSON:

                     Cursor cursor = db.query("person", projection, selection, selectionArgs, null, null, sortOrder);

                     // == 注意:此处的 db与cursor不能关闭 ==

                     return cursor;

              default:

                     throw new IllegalArgumentException(String.format("Uri:%s 不是合法的uri地址", uri));

              }

       }

       // 返回传入URI的类型,可用于测试URI是否正确

       @Override

       public String getType(Uri uri) {

              switch (uriMatcher.match(uri)) {

              case PERSON_ID:

                     return "vnd.android.cursor.item/person";             // 表示单条person记录

              case PERSON:

                     return "vnd.android.cursor.dir/person";        // 表单多个person记录

              default:

                     return null;

              }

       }

}

清单中的配置:

 <provider

     android:exported="true"

     android:name="com.itheima.sqlite.provider.HeimaProvider"

     android:authorities="com.itheima.sqlite.provider" />

authorities 可以配置成如下形式(系统联系人的):

       android:authorities="contacts;com.android.contacts"

“;” 表示的是可使用 contacts, 与 com.android.contacts

 

3、内容解析者ContentResolver

  • 通过Context获得ContentResolver内容访问者对象(内容提供者的解析器对象);
  • 调用ContentResolver对象的方法即可访问内容提供者

测试类代码:

public class HeimaProviderTest extends AndroidTestCase {

       /** 测试添加数据 */

       public void testInsert() {

              ContentResolver resolver = this.getContext().getContentResolver();

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider/person");

              ContentValues values = new ContentValues();

              values.put("name", "小翼");

              values.put("balance", 13000);

              Uri insert = resolver.insert(uri, values);               // 获取返回的uri,如:content://com.itheima.sqlite.provider/7

              System.out.println(insert);

       }

       /** 测试删除 */

       public void testRemove() {

              ContentResolver resolver = this.getContext().getContentResolver();

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider/person");

              int count = resolver.delete(uri, "id=?", new String[] { 3 + "" });

              System.out.println("删除了" + count + "行");

       }

       /** 测试更新 */

       public void testUpdate() {

              ContentResolver resolver = this.getContext().getContentResolver();

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider/person");

              ContentValues values = new ContentValues();

              values.put("name", "小赵 update");

              values.put("balance", 56789);

              int update = resolver.update(uri, values, "id=?", new String[] { 6 + "" });

              System.out.println("更新了" + update + "行");

       }

       /** 测试查询 */

       public void testQueryOne() {

              ContentResolver resolver = this.getContext().getContentResolver();

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider/person");

              Cursor c = resolver.query(uri, new String[] { "name", "balance" }, "id=?", new String[] { 101 + "" }, null);

              if (c.moveToNext()) {

                     System.out.print(c.getString(0));

                     System.out.println(" " + c.getInt(1));

              }

              c.close();

       }

       /**测试查询全部 */

       public void testQueryAll() {

              ContentResolver resolver = this.getContext().getContentResolver();

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider/person");

              Cursor c = resolver.query(uri, new String[] { "id", "name", "balance" }, null, null, "name desc");

              while (c.moveToNext()) {

                     System.out.println(c.getInt(0) + ", " + c.getString(1) + ", " + c.getInt(2));

              }

              c.close();

       }

       /** 测试查询一条 */

       public void testQueryOneWithUriId() {

              ContentResolver resolver = this.getContext().getContentResolver();

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider/3");          // 查询ID为3的记录

              Cursor c = resolver.query(uri, new String[] { "id", "name", "balance" }, null, null, null);

              if (c.moveToNext()) {

                     System.out.println(c.getInt(0) + ", " + c.getString(1) + ", " + c.getInt(2));

              }

              c.close();

       }

       /** 测试获取内容提供者的返回类型 */

       public void testGetType() {

              ContentResolver resolver = this.getContext().getContentResolver();

              System.out.println(resolver.getType(Uri.parse("content://com.itheima.sqlite.provider/2")));

              System.out.println(resolver.getType(Uri.parse("content://com.itheima.sqlite.provider/person")));

       }

}

 

4、监听内容提供者的数据变化

  • 在内容提供者中可以通知其他程序数据发生变化

通过Context的getContentResolver()方法获取ContentResolver

调用其notifyChange()方法发送数据修改通知,发送到系统的公共内存(消息信箱中)

  • 在其他程序中可以通过ContentObserver监听数据变化

通过Context的getContentResolver()方法获取ContentResolver

调用其registerContentObserver()方法指定对某个Uri注册ContentObserver

自定义ContentObserver,重写onChange()方法获取数据

示例代码(发通知部分):

       public int delete(Uri uri, String selection, String[] selectionArgs) {

              SQLiteDatabase db = helper.getWritableDatabase();

                     int delete = db.delete("person", selection, selectionArgs);

                     // == 通过内容访问者对象ContentResolve 发通知给所有的Observer ==

                     getContext().getContentResolver().notifyChange(uri, null);      

                     db.close();

                     return delete;

              }

       }

监听部分:

       // 注册内容观察者事件

       private void initRegisterContentObserver() {

              Uri uri = Uri.parse("content://com.itheima.sqlite.provider");      // 监听的URI

              // == 第2个参数:true表示监听的uri的后代都可以监听到 ==

              getContentResolver().registerContentObserver(uri, true, new ContentObserver(new Handler()) {

                     public void onChange(boolean selfChange) {                    // 接到通知就执行                                                      

                            personList = personDao.queryAll();                                                                                                

                            ((BaseAdapter) personListView.getAdapter()).notifyDataSetChanged();

                     }

              });

       }

 

5、区别Provider/Resolver/Observer

1)ContentProvider:内容提供者

       把一个应用程序的私有数据(如数据库)信息暴露给别的应用程序,让别的应用程序可以访问;

       在数据库中有对应的增删改查的方法,如果要让别的应用程序访问,需要有一个路径uri:

通过content:// 路径对外暴露,uri写法:content://主机名/表名

2)ContentResolver:内容解析者

       根据内容提供者的路径,对数据进行操作(crud);

3)ContentObserver:内容观察者

       可以理解成android系统包装好的回调,数据发送变化时,会执行回调中的方法;

       ContentResolver发送通知,ContentObserver监听通知;

当A的数据发生变化的时候,A就会显示的通知一个内容观察者,不指定观察者,就会发消息给一个路径

 

二、Activity活动

描述:

1)表示用户交互的一个界面(活动),每一个activity对应一个界面

2)是所有View的容器:button,textview,imageview;我们在界面上看到的都是一个个的view

3)有个ActivityManager的管理服务类,用于维护与管理Activity的启动与销毁;

Activity启动时,会把Activity的引用放入任务栈中

4)一个应用程序可以被别的应用程序的activity开启

       此时,是将此应用程序的引用加入到了开启的那个activity的任务栈中了

5) activity是运行在自己的程序进程里面的

       在一个应用程序中,可以申请单独的进程,然此应用程序中的一个组件在新的进程中运行

6)可以在activity里面添加permission标签,调用者必须加入这个权限

       与钱打交道的界面,都不允许被其他应用程序随意打开

如果觉得那个activity比较重要,可以在清单文件中配置,防止别人随意打开,需要配置一个权限

自定义权限:

在清单文件中配置permission,创建一个新的权限

       创建后,就会在清单文件中生成这个权限了

此时,需要开启这个界面,就需要使用这个权限

Tips

       *不可使用中文文本,需要使用字符串,抽取出来

*声明之后,会在gen的目录下,多出来一个文件:Manifest的文件,系统也存在一个这样的文件

 

 

1、创建Activity

1)定义类继承自Activity类;

2)在清单文件中Application节点中声明<activity>节点;

       <activity

            android:name="com.itheima.activity.MainActivity"

            android:label="@string/app_name" >

            <!-- 程序的入口,LAUNCHER表示桌面快捷方式,进入的是此Activity -->

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />  <!—启动时,默认匹配 --

            </intent-filter>

        </activity>

 

2、启动Activity

通过意图(Intent)来启动一个Activity;

  1. 显示启动:

显示启动一般用于自己调用自己的情况(在当前应用找),这样的启动方式比较快速,创建Intent后指定包名和类名;

       Intent intent = new Intent(this, OtherActivity.class);

       startActivity(intent);             // 启动新的Activity

       或者:

       Intent intent = new Intent();

       intent.setClassName("com.itheima.activity", "com.itheima.activity.OtherActivity"); // 包名、全类名

       startActivity(intent);             // 启动新的Activity

2)隐式启动:

一般用于调用别人的Activity,创建Intent后指定动作和数据以及类型;

       // 电话

       Intent intent = new Intent();

       intent.setAction(Intent.ACTION_CALL);                                        // 设置动作

       intent.setData(Uri.parse("tel://123456"));                                 // 设置数据

       // 网页

       intent.setAction(Intent.ACTION_VIEW);

       intent.setData(Uri.parse("http://192.168.1.45:8080/androidWeb"));

       // 音频/视频,设置type

       intent.setAction(Intent.ACTION_VIEW);

       intent.setDataAndType(Uri.parse("file:///mnt/sdcard/daqin.mp3"), "audio/*");  // 设置数据和数据类型,将启动音频播放器(vedio)

3)为隐式启动配置意图过滤器:

    1. 显式意图是指在创建意图时指定了组件,而隐式意图则不指定组件,通过动作、类型、数据匹配对应的组件;
    2. 在清单文件中定义<activity>时需要定义<intent-filter>才能被隐式意图启动;
    3. <intent-filter>中至少配置一个<action>和一个<category>,否则无法被启动;
    4. Intent对象中设置的actioncategorydata在<intent-filter>必须全部包含Activity才能启动;
    5. <intent-filter>中的<action>、<category>、<data>都可以配置多个,Intent对象中不用全部匹配,每样匹配一个即可启动;
    6. 如果一个意图可以匹配多个Activity,Android系统会提示选择;

         <!-- 注册 Activity, lable 表示Activity的标题 -->

        <activity

            android:name="com.itheima.activity.OtherActivity"

            android:label="OtherActivity" >

            <!-- 配置隐式意图,匹配http -->

            <intent-filter>

                <action android:name="android.intent.action.VIEW" />           <!—必须,表示动作为View -->

                <data android:scheme="http" />                                             <!—http开头-->                             

                <category android:name="android.intent.category.DEFAULT" /> <!-- 必须,表示启动时,默认匹配 -->

            </intent-filter>

            <!-- 匹配tel -->

             <intent-filter>

                <action android:name="android.intent.action.CALL" />                  

                <data android:scheme="tel" />                                                                                

                <category android:name="android.intent.category.DEFAULT" /> <!-- 必须,表示启动 -->

            </intent-filter>

            <!-- 匹配 音频、视频 -->

            <intent-filter>

                <action android:name="android.intent.action.VIEW" />                   

                <data android:scheme="file" android:mimeType="audio/*" />   <!—文件协议l类型 -->                                                                         

                <data android:scheme="file" android:mimeType="video/*" />                

                <category android:name="android.intent.category.DEFAULT" /> <!-- 必须,表示启动 -->                                                       

            </intent-filter>

     </activity>

 

3、启动时传递数据

可通过意图Intent对象实现Activity之间的数据传递;

使用Intent.putExtra()方法装入一些数据, 被启动的Activity可在 onCreate方法中getIntent()获取;

可传输的数据类型: a.基本数据类型(数组),  b. String(数组),  c. Bundle(Map),  d. Serializable(Bean), e.Parcelable(放在内存一个共享空间里);

基本类型:

       Intent intent = new Intent(this, OtherActivity.class);

       intent.putExtra("name", "张飞");         // 携带数据

       intent.putExtra("age", 12);

       startActivity(intent);

一捆数据:

       Intent intent = new Intent(this, OtherActivity.class);

       Bundle b1 = new Bundle();

       b1.putString("name", "赵云");

       b1.putInt("age", 25);

       Bundle b2 = new Bundle();

       b2.putString("name", "关羽");

       b2.putInt("age", 44);

       intent.putExtra("b1", b1);

       intent.putExtra("b2", b2);

序列化对象(须实现序列化接口):

       Intent intent = new Intent(this, OtherActivity.class);

       Person p = new Person("张辽", 44);

       intent.putExtra("p", p);

接收数据:

       在OtherActivity 的onCreate()方法,通过 getIntent().get 相关的数据的方法来获取数据;

 

4、关闭时返回数据

基本流程:

  1. 使用startActivityForResult(Intent intent, int requestCode) 方法打开Activity;
  2. 重写onActivityResult(int requestCode, int resultCode, Intent data) 方法;
  3. 新Activity中调用setResult(int resultCode, Intent data) 设置返回数据之后,关闭Activity就会调用上面的onActivityResult方法;

注意:新的Activity的启动模式不能设置成 singleTask(如果已创建,会使用以前创建的)与singleInstance(单例,单独的任务栈),

         不能被摧毁(执行不到finish方法),父Activity中的 onActivityResult方法将不会执行;

finish():表示关闭当前Activity,会调用onDestroy方法;

Activity_A:

       public void openActivityB(View v) {

              Intent intent = new Intent(this, Activity_B.class);

              Person p = new Person("张辽", 44);

              intent.putExtra("p", p);

              startActivityForResult(intent, 100);                                         // 此方法,启动新的Activity,等待返回结果, 结果一旦返回,自动执行onActivityResult()方法

       }

       protected void onActivityResult(int requestCode, int resultCode, Intent data) {

              if(data == null) {                                                           // 没有数据,不执行

                     return;

              }

              System.out.println(requestCode + ", " + resultCode);         // code 可用来区分,哪里返回的数据

              String name = data.getStringExtra("name");

              int age = data.getIntExtra("age", -1);

       }

Activity_B:

       public void close(View v) {

              // == 关闭当前Activity时,设置返回的数据 ==

              Intent intent = new Intent();

              intent.putExtra("name", "典韦");

              intent.putExtra("age", 55);

              setResult(200, intent);   

              finish();                        // 关闭,类似于点击了后退

       }

 

5、生命周期

1)Acitivity三种状态

  1. 运行:activity在最前端运行;
  2. 停止:activity不可见,完全被覆盖;
  3. 暂停:activity可见,但前端还有其他activity<>,注意:在当前Activitiiy弹出的对话框是Activity的一部分,弹出时,不会执行onPause方法;

2)生命周期相关的方法(都是系统自动调用,都以 on 开头):

  1. onCreate:      创建时调用,或者程序在暂停、停止状态下被杀死之后重新打开时也会调用;
  2. onStart:                   onCreate之后或者从停止状态恢复时调用;                                                            
  3. onResume:   onStart之后或者从暂停状态恢复时调用,从停止状态恢复时由于调用onStart,也会调用onResume(界面获得焦点);
  4. onPause:       进入暂停、停止状态,或者销毁时会调用(界面失去焦点);
  5. onStop:          进入停止状态,或者销毁时会调用;
  6. onDestroy:    销毁时调用;
  7. onRestart:    从停止状态恢复时调用;

3)生命周期图解:

 

       应用启动时,执行onCreate onStart onResume,退出时执行:onPause onStop onDestroy;

 

6、横竖屏切换与信息的保存恢复

切换横竖屏时,会自动查找layout-port 、layout-land中的布局文件,默认情况下,

切换时,将执行摧毁onPause onStop onDestroy,再重置加载新的布局onCreate onStart onResume

切换时如果要保存数据, 可以重写: onSaveInstanceState();

恢复数据时, 重写: onRestoreInstanceState();

 

è固定横屏或竖屏:                                 android:screenOrientation="landscape"

è横竖屏切换, 不摧毁界面(程序继续执行) android:configChanges="orientation|keyboardHidden|screenSize"

保存信息状态的相关方法:

  1. onSaveInstanceState:    

在Activity被动的摧毁或停止的时候调用(如横竖屏切换,来电),用于保存运行数据,可以将数据存在在Bundle中;

  1. onRestoreInstanceState:

该方法在Activity被重新绘制的时候调用,例如改变屏幕方向,onSavedInstanceState可onSaveInstanceState保存的数据

7、启动模式

1)任务栈的概念

问:一个手机里面有多少个任务栈?

答:一般情况下,有多少个应用正在运行,就对应开启多少个任务栈;  

       一般情况下,每开启一个应用程序就会创建一个与之对应的任务栈;

       二般情况下,如launchMode为 singleInstance,就创建自己单独的任务栈;

2)任务栈的作用:

它是存放Activity的引用的,Activity不同的启动模式,对应不同的任务栈的存放;

可通过getTaskId()来获取任务栈的ID,如果前面的任务栈已经清空,新开的任务栈ID+1,是自动增长的;

3)启动模式:

在AndroidManifest.xml中的<activity>标签中可以配置android:launchMode属性,用来控制Actvity的启动模式;

在Android系统中我们创建的Acitivity是以的形式呈现的:

①、standard:默认的,每次调用startActivity()启动时都会创建一个新的Activity放在栈顶;

②、singleTop:启动Activity时,指定Activity不在任务栈栈顶就创建,如在栈顶,则不会创建,会调用onNewInstance(),复用已经存在的实例

③、singleTask:在任务栈里面只允许一个实例,如果启动的Activity不存在就创建,如果存在直接跳转到指定的Activity所在位置,

                     如:栈内有ABCD,D想创建A, 即A上的BCD相应的Activity将移除

④、singleInstance:(单例)开启一个新的任务栈来存放这个Activity的实例在整个手机操作系统里面只有一个该任务栈的实例存在,此模式开启的Activity是运行在自己单独的任务栈中的

 

4)应用程序、进程、任务栈的区别

①、应用程序:

四大组件的集合

在清单文件中都放在application节点下

对于终端用户而言,会将其理解为activity

②、进程:

操作系统分配的独立的内存空间,一般情况下,一个应用程序会对应一个进程,特殊情况下,会有多个进程

一个应用程序会对应一个或多个进程

③、任务栈:task stack(back stack)后退栈

       记录用户的操作步骤,维护用户的操作体验,

       专门针对于activity而言的,只用于activity

       一般使用standard,其他情况用别的

 

5)启动模式的演示

1、创建两个activity,布局中设置两个按钮,分别开启两个activity

第一、standard启动模式的:开启几个就会在任务栈中存在几个任务

01和02都是存在于一个任务栈中的

 

第二、在清单文件中将02的启动模式改为singletop,

此时02处于栈顶,就只会创建一个02的任务,再开启02,也不会创建新的

 

第三、将02的启动模式改为singletask

       如果02上面有其他任务栈,就会将其他的清除掉,利用这个已经创建的02

       当开启02的时候,即先将01清除,然后利用下面的02

 

第四、将02的启动模式改为singleinstance

       可以通过打印任务栈的id(调用getTaskId()方法)得知,两个activity不在同一个任务栈中

若先开启三个01,在开启02,任务栈如图:

 

再开启01,任务栈的示意图如下:

 

此时按返回键,会先一层一层清空01,最后再清空02

空进程:任务栈清空,意味着程序退出了,但进程留着,这个就是空进程,容易被系统回收;

8、内存管理

       Android系统在运行多个进程时,如果系统资源不足,会强制结束一些进程,优先选择哪个进程来结束是有优先级的。

会按照以下顺序杀死:

①、空:  进程中没有任何组件;

②、后台:进程中只有停止状态的Activity;

③、服务:进程中有正在运行的服务;

④、可见:进程中有一个暂停状态的Activity;

⑤、前台:进程中正在运行一个Activity;

Activity在退出的时候进程不会销毁, 会保留一个空进程方便以后启动. 但在内存不足时进程会被销毁;

Activity中不要在Activity做耗时的操作, 因为Activity切换到后台之后(Activity停止了), 内存不足时, 也容易被销毁;

 

三、BroadcastReceiver 广播接收者

系统的一些事件,比如来电,来短信,等等,会发广播;可监听这些广播,并进行一些处理;

Android3.2以后,为了安全起见,对于刚安装的应用,需要通过点击进入应用(界面,用户确认之后),接收者才能起作用;

以后即使没有启动其界面,也能接收到广播;

1、定义广播接收者

1)定义类继承BroadcastReceiver,重写onReceive方法

2)清单文件中声明<receiver>,需要在其中配置<intent-filter>指定接收广播的类型;

3)当接收到匹配广播之后就会执行onReceive方法;

4)有序广播中,如果要控制多个接收者之间的顺序,可在<intent-filter>配置priority属性,系统默认为0,值越大,优先级越高;

5)BroadcastReceiver除了在清单文件中声明,也可以在代码中声明,使用registerReceiver方法注册Receiver;

 <!-- 配置广播接收者,监听播出电话 -->

     <receiver android:name="com.itheima.ipdialer.CallReceiver" >

          <intent-filter>

               <action android:name="android.intent.action.NEW_OUTGOING_CALL" />

           </intent-filter>

 </receiver>

 

2、广播的分类

1)普通广播:

普通广播不可中断,不能互相传递数据;

2)有序广播:

广播可中断,通过调用abortBroadcast()方法;

接收者之间可以传递数据;

 

3、广播接收者的注册方式

4大组件中,只有广播接收者是一个非常特殊的组件,其他3大组件都需要在清单文件中注册;

广播接收者,有2中注册方式:清单文件与代码方式,区别:

1)清单文件注册广播接收者,只要应用程序被部署到手机上,就立刻生效,不管进程是否处于运行状态;

2)代码方式,如果代码运行了,广播接收者才生效,如果代码运行结束,广播接收者,就失效;

这属于动态注册广播,临时用一下,用的时候,register,不用时unregister;

代码方式示例:

       // 广播接收者

       private class InnerReceiver extends BroadcastReceiver {

              @Override

              public void onReceive(Context context, Intent intent) {

                     String phone = getResultData();

                     String address = AddressDao.queryAddress(getApplicationContext(), phone);

                     showAddress(address);

              }

       }

       // 注册示例代码

       public void onCreate() {

              // == 服务启动时,注册广播接收者 ==

              innerReceiver = new InnerReceiver();

              // 指定意图过滤器

              IntentFilter filter = new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL);

              this.registerReceiver(innerReceiver, filter);

       }

       // 销毁  

       public void onDestroy() {

              // == 服务停止时,移除广播接收者 ==

              this.unregisterReceiver(innerReceiver);

              innerReceiver = null;

              super.onDestroy();

       }

 

4、发送广播

1)发送普通广播

①、使用sendBroadcast()方法可发送普通广播;

②、通过Intent确定广播类型,可携带数据,所有接收者都可以接收到数据,数据不能被修改,不会中断;

接收者无序(试验测试,是按照安装顺序来接收的);

③、广播时,可设置接收者权限,仅当接收者含有权限才能接收;

④、接收者的<receiver>也可设置发送方权限,只接受含有相应权限应用的广播;

发送者:

       Intent intent = new Intent("com.itheima.broadcast.TEST");       // 指定动作;接收者,需要配置 intent filter才能接受到此广播

       intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);  // 包含未启动的过的应用(也可以收到广播),默认为不包含

       intent.putExtra("data", "这是来着广播发送者发来的贺电");           // 广播发送者intent中的数据,接收者,修改不了

       sendBroadcast(intent, null);                                                    // 发送无序广播,异步获取数据,不可中断,接收者之间不可传数据

 

接收者:

       public class AReceiver extends BroadcastReceiver {

              public void onReceive(Context context, Intent intent) {

                     System.out.println("AReceiver: " + intent.getStringExtra("data"));

              }

       }

      <receiver android:name="com.itheima.a.AReceiver">

            <intent-filter android:priority="2" >

                <action android:name="com.itheima.broadcast.TEST" />   <!—接收指定动作的广播 -->

            </intent-filter>

      </receiver>

注意:

       如果要在广播接收者中打开Activity,需要设置一下Intent.FLAG_ACTIVITY_NEW_TASK因为广播接收者是没有Activity任务栈的

所以需要加上这个标记,方能在广播接收者中打开Activity,如:

       public void onReceive(Context context, Intent intent) {

              Log.i(TAG, "打电话了。。。");

              String phone = this.getResultData();

              if ("2008".equals(phone)) {

                     // == 打开手机防盗功能界面 ==

                     Intent safeIntent = new Intent(context, LostFindActivity.class);

                     safeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);                   // 使Activity也能在Receiver中启动

                     context.startActivity(safeIntent);

                     abortBroadcast();                                // 中断广播

                     setResultData(null);                            // 把电话号码设为null,就没有了通话记录

              }

       }

 

2)发送有序广播

  1. sendOrderedBroadcast() 发送有序广播;
  2. 通过Intent确定广播类型, 携带数据,Intent的数据同样修改无效;
  3. 跟普通广播一样,也可以设置相应的权限;
  4. 接收者可在<intent-filter>定义android:priority定义优先级,数字越大,优先级越高;
  5. 有序广播会被接收者逐个接收,中途可以中断,或添加、修改数据;
  6. 可以指定一个自己的广播接收者, 这个接收者将最后一个收到广播、不会被中断、不需要任何权限、不需要配置;
  7. 可以指定一个Handler用来在自己的接收者中进行线程通信;

 

发送者:

       Intent intent = new Intent("com.itheima.broadcast.TEST");       // 指定动作;接收者,需要配置 intent filter才能接受到此广播

       intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);  // 包含未启动的过的应用(也可以收到广播),默认为不包含

       intent.putExtra("data", "这是来着广播发送者发来的贺电");           // 广播发送者的intent中的数据,接收者,修改不了

       // == 有序广播时,传递的数据可修改 ==

       Bundle bundle = new Bundle();

       bundle.putString("name", "关羽");

       bundle.putInt("age", 22);

       /* 定义权限,要求接收者,要有 com.itheima.permission.broadcast.RECEIVE 才能接收;

        * 配置了最后接收者,Creceiver,无论你们怎么弄,我都可以收到广播,而且我不要配置,不要权限

       *  handle为null,表示使用系统默认的

       *  传递了数据 1, “MainActivity”, bundle 这些都是可以在接收者修改的

       */

       this.sendOrderedBroadcast(intent, "com.itheima.permission.broadcast.RECEIVE", new CReceiver(), null, 1, "MainActivity", bundle);

    <!-- 定义一个权限 -->

    <permission android:name="com.itheima.permission.broadcast.RECEIVE" >

</permission>

<!- 使用该权限-->

<uses-permission android:name="com.itheima.permission.broadcast.RECEIVE" />

 

接收者AReceive

       public void onReceive(Context context, Intent intent) {

              System.out.println("AReceiver: " + intent.getStringExtra("data"));

              Bundle bundle = this.getResultExtras(true);         // 设置为true,表示即使没有传递Bundle数据,不会出现空指针

              String message = String.format("%s : %s : %s, %s", getResultCode(), getResultData(), bundle.getString("name"), bundle.getInt("age"));

              System.out.println(message);                             // 如果优先级高于其他接收者,将打印发送者的数据

 

              // == 修改有序发送者,发来的数据 ==

              bundle.putString("name", "赵子龙");

              bundle.putInt("age", 222);

              this.setResult(2, "AReceiver", bundle);

 

              // == 修改Intent中的数据,无效 ==

              intent.putExtra("data", "AReceiver 修改了数据");

 

              this.setResultData("这是来自AReceiver的信息");

              // this.abortBroadcast();                                           // 中断,比它优先级低的接收者,将不能接收到广播了

       }

       <!-- 要求广播发送者必须有对应的权限,我才收 -->

      <receiver  android:name="com.itheima.a.AReceiver"

            android:permission="com.itheima.permission.broadcast.RECEIVE" >     

            <intent-filter android:priority="2" >

                <action android:name="com.itheima.broadcast.TEST" />

            </intent-filter>

        </receiver>

 

接收者BReceive:代码及配置与上类似,只是优先级比A的低

 

5、广播的生命周期

  1. 广播接收者的生命周期非常短暂的,在接收到广播的时候创建,onReceive()方法结束之后销毁;
  2. 广播接收者中不要做一些耗时的工作,否则会弹出Application No Response错误对话框;
  3. 最好也不要在广播接收者中创建子线程做耗时的工作,因为广播接收者被销毁后进程就成为了空进程,很容易被系统杀掉;
  4. 耗时的较长的工作最好放在服务中完成;

 

四、Service服务

Service是一种在后台长期运行的,没有界面的组件,由其他组件调用开始运行;

服务不太会被kill,即使在内存不足时被kill,当内存恢复时,服务会自动复活,例如下载就可以放入服务中来做,下载时,启动服务,完成时,关闭服务;

 

1、创建与使用Service

1)、定义类继承Service, 清单文件中声明<service>,同样也可以配置意图过滤;

2)、使用Intent来开启Service,在其他组件中调用startService方法;

3)、停止Service,调用stopService方法;

 

2、生命周期

Service中的生命周期方法(Context调用执行):

1)startService()             如果没创建就先onCreate()再startCommand(), 如果已创建就只执行startCommand();

2)stopService()              执行onDestroy()

3)bindService()              如果没有创建就先onCreate()再onBind()

4)unbindService()          如果服务是在绑定时启动的, 先执行onUnbind()再执行onDestroy(). 如果服务在绑定前已启动, 那么只执行onUnbind();

3、开启服务的2种方式

2种不同开启方式的区别:

1)startService:

       开启服务,服务一旦开启,就长期就后台运行,即使调用者退出来,服务还会长期运行;

       资源不足时,被杀死,资源足够时,又会复活;

2)bindService:

       绑定服务,绑定服务的生命周期会跟调用者关联起来,调用者退出,服务也会跟着销毁;

       通过绑定服务,可以间接的调用服务里面的方法(onBind返回IBinder);

 

4、服务混合调用生命周期

一般的调用顺序:

①、start  -> stop                    开启 –> 结束

②、bind  -> unbind                 绑定(服务开启) -> 解绑(服务结束)

混合调用:

①、start –> bind -> stop->unbind->ondestroy                通常不会使用这种模式

       开启 –> 绑定 –> 结束(服务停不了)->解除绑定(服务才可停掉)

②、start –> bind -> unbind -> stop                          经常使用

       开启 –> 绑定 –> 解绑(服务继续运行)-> stop(不用时,再停止服务)

         这样保证了服务长期后台运行,又可以调用服务中的方法

五、Android四大组件

1.    ContentProvider

              共享应用程序内的数据, 在数据修改时可以监听

  1. Activity

              供用户操作的界面

  1. BroadcastReceiver

              用来接收广播, 可以根据系统发生的一些时间做出一些处理

  1. Service

              长期在后台运行的, 没有界面的组件, 用来在后台执行一些耗时的操作

 

 

 

  • 熟悉掌握ListView的优化及异步任务加载网络数据

一、异步任务加载网络数据:

在Android中提供了一个异步任务的类AsyncTask,简单来说,这个类中的任务是运行在后台线程中的,并可以将结果放到UI线程中进行处理,它定义了三种泛型,分别是Params、Progress和Result,分别表示请求的参数、任务的进度和获得的结果数据。

1、使用原因:

1)是其中使用了线程池技术,而且其中的方法很容易实现调用

2)可以调用相关的方法,在开启子线程前和后,进行界面的更新

3)一旦任务多了,不用每次都new新的线程,可以直接使用

2、执行的顺序:

onPreExecute()【执行前开启】--- > doInBackground() --- > onProgressUpdate() --- > onPostExecute()

3、执行过程:

当一个异步任务开启后,执行过程如下:

1)、onPreExecute():

这个方法是执行在主线程中的。这步操作是用于准备好任务的,作为任务加载的准备工作。建议在这个方法中弹出一个提示框。

2)、doInBackground():

这个方法是执行在子线程中的。在onPreExecute()执行完后,会立即开启这个方法,在方法中可以执行耗时的操作。需要将请求的参数传递进来,发送给服务器,并将获取到的数据返回,数据会传给最后一步中;这些值都将被放到主线程中,也可以不断的传递给下一步的onProgressUpdate()中进行更新。可以通过不断调用publishProgress(),将数据(或进度)不断传递给onProgressUpdate()方法,进行不断更新界面。

3)、onProgressUpdate():

这个方法是执行在主线程中的。publishProgress()在doInBackground()中被调用后,才开启的这个方法,它在何时被开启是不确定的,执行这个方法的过程中,doInBackground()是仍在执行的,即子线程还在运行着。

4)、onPostExecute():

这个方法是执行在主线程中的。当后台的子线程执行完毕后才调用此方法。doInBackground()返回的结果会作为参数被传递过来。可以在这个方法中进行更新界面的操作。

5)、execute():

       最后创建AsyncTask自定义的类,开启异步任务。

 

3、实现原理:

1)、线程池的创建:

在创建了AsyncTask的时候,会默认创建一个线程池ThreadPoolExecutor,并默认创建出5个线程放入到线程池中,最多可防128个线程;且这个线程池是公共的唯一一份。

  1. 、任务的执行:

在execute中,会执行run方法,当执行完run方法后,会调用scheduleNext()不断的从双端队列中轮询,获取下一个任务并继续放到一个子线程中执行,直到异步任务执行完毕。

3)、消息的处理:

在执行完onPreExecute()方法之后,执行了doInBackground()方法,然后就不断的发送请求获取数据;在这个AsyncTask中维护了一个InternalHandler的类,这个类是继承Handler的,获取的数据是通过handler进行处理和发送的。在其handleMessage方法中,将消息传递给onProgressUpdate()进行进度的更新,也就可以将结果发送到主线程中,进行界面的更新了。

 

4、需要注意的是:

①、这个AsyncTask类必须由子类调用

②、虽然是放在子线程中执行的操作,但是不建议做特别耗时的操作,如果操作过于耗时,建议使用线程池ThreadPoolExecutor和FutureTask

示例代码:

private class DownloadFilesTask extends AsyncTask&lt;URL, Integer, Long&gt; {

    protected Long doInBackground(URL... urls) {

        int count = urls.length;

        long totalSize = 0;

        for (int i = 0; i < count; i++) {

            totalSize += Downloader.downloadFile(urls[i]);

            publishProgress((int) ((i / (float) count) * 100));

            // Escape early if cancel() is called

            if (isCancelled()) break;

        }

        return totalSize;

    }

 

    protected void onProgressUpdate(Integer... progress) {

        setProgressPercent(progress[0]);

    }

 

    protected void onPostExecute(Long result) {

        showDialog("Downloaded " + result + " bytes");

    }

}

new DownloadFilesTask().execute(url1, url2, url3);

 

二、ListView优化:

ListView的工作原理

首先来了解一下ListView的工作原理(可参见http://mobile.51cto.com/abased-410889.htm),如图:

1、如果你有几千几万甚至更多的选项(item)时,其中只有可见的项目存在内存(内存内存哦,说的优化就是说在内存中的优化!!!)中,其他的在Recycler中

2、ListView先请求一个type1视图(getView)然后请求其他可见的项目。convertView在getView中是空(null)的

3、当item1滚出屏幕,并且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1。你只需设定新的数据然后返回convertView,不必重新创建一个视图

 

 

 

一、复用convertView,减少findViewById的次数

1、优化一:复用convertView

Android系统本身为我们考虑了ListView的优化问题,在复写的Adapter的类中,比较重要的两个方法是getCount()和getView()。界面上有多少个条显示,就会调用多少次的getView()方法;因此如果在每次调用的时候,如果不进行优化,每次都会使用View.inflate(….)的方法,都要将xml文件解析,并显示到界面上,这是非常消耗资源的:因为有新的内容产生就会有旧的内容销毁,所以,可以复用旧的内容。

优化:

在getView()方法中,系统就为我们提供了一个复用view的历史缓存对象convertView,当显示第一屏的时候,每一个item都会新创建一个view对象,这些view都是可以被复用的;如果每次显示一个view都要创建一个,是非常耗费内存的;所以为了节约内存,可以在convertView不为null的时候,对其进行复用

2、优化二:缓存item条目的引用——ViewHolder

   findViewById()这个方法是比较耗性能的操作,因为这个方法要找到指定的布局文件,进行不断地解析每个节点:从最顶端的节点进行一层一层的解析查询,找到后在一层一层的返回,如果在左边没找到,就会接着解析右边,并进行相应的查询,直到找到位置(如图)。因此可以对findViewById进行优化处理,需要注意的是:

》》》》特点:xml文件被解析的时候,只要被创建出来了,其孩子的id就不会改变了。根据这个特点,可以将孩子id存入到指定的集合中,每次就可以直接取出集合中对应的元素就可以了。

 

优化:

在创建view对象的时候,减少布局文件转化成view对象的次数;即在创建view对象的时候,把所有孩子全部找到,并把孩子的引用给存起来

①定义存储控件引用的类ViewHolder

这里的ViewHolder类需要不需要定义成static,根据实际情况而定,如果item不是很多的话,可以使用,这样在初始化的时候,只加载一次,可以稍微得到一些优化

不过,如果item过多的话,建议不要使用。因为static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(比如Context的情况最多),这时就要尽量避免使用了。

   class ViewHolder{

                 //定义item中相应的控件

          }

②创建自定义的类:ViewHolder holder = null;

③将子view添加到holder中:

在创建新的listView的时候,创建新的ViewHolder,把所有孩子全部找到,并把孩子的引用给存起来

通过view.setTag(holder)将引用设置到view中

通过holder,将孩子view设置到此holder中,从而减少以后查询的次数

④在复用listView中的条目的时候,通过view.getTag(),将view对象转化为holder,即转化成相应的引用,方便在下次使用的时候存入集合。

  通过view.getTag(holder)获取引用(需要强转)

 

示例代码:

public class ActivityDemo extends Activity {

    private ListView listview1;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

           super.onCreate(savedInstanceState);

           listview1 = (ListView) findViewById(R.id.listview1);

           MyAdapter adapter = new MyAdapter();

           listview1.setAdapter(adapter);

    }

    private class MyAdapter extends BaseAdapter{

           @Override

           public int getCount() {

                  return 40;

           }

           @Override

           public Object getItem(int position) {

                  return position;

           }

           @Override

           public long getItemId(int position) {

                  return position;

           }

           @Override

           public View getView(int position, View convertView, ViewGroup parent) {

                  ViewHolder holder = null;

                  if(convertView!=null && convertView instanceof RelativeLayout){    //注意:这里不一定用RelativeLayout,根据XML文件中的根节点来确定

                         holder = (ViewHolder) convertView.getTag();

                  }else{

                         //1、复用历史缓存view对象,检索布局问转化成view对象的次数

                         convertView = View.inflate(ActivityDemo.this, R.layout.item, null);

                         //2、在创建view对象的时候,把所有的子view找到,把子view的引用存起来

                         holder = new ViewHolder();

                         holder.ivIcon = (ImageView) convertView.findViewById(R.id.iv_icon);

                         holder.tvContent = (TextView) convertView.findViewById(R.id.tv_content);

                         convertView.setTag(holder);

                         /*    实现存储子view引用的另一种方式:

                                convertView.setTag(holder.ivIcon);

                                convertView.setTag(holder.tvContent);  */

                  }

                  //直接复用系统提供的历史缓存对象convertView

                  return convertView;

           }

    }

   

class ViewHolder{

           public ImageView ivIcon;

           public TextView tvContent;

    }

}

 

二、ListView中数据的分批及分页加载:

需求:

ListView有一万条数据,如何显示;如果将十万条数据加载到内存,很消耗内存

解决办法:

优化查询的数据:先获取几条数据显示到界面上

进行分批处理---à优化了用户体验

进行分页处理---à优化了内存空间

说明:

一般数据都是从数据库中获取的,实现分批(分页)加载数据,就需要在对应的DAO中有相应的分批(分页)获取数据的方法,如findPartDatas ()

1、准备数据:

   在dao中添加分批加载数据的方法:findPartDatas ()

   在适配数据的时候,先加载第一批的数据,需要加载第二批的时候,设置监听检测何时加载第二批

2、设置ListView的滚动监听器:setOnScrollListener(new OnScrollListener{….})

①、在监听器中有两个方法:滚动状态发生变化的方法(onScrollStateChanged)和listView被滚动时调用的方法(onScroll)

②、在滚动状态发生改变的方法中,有三种状态:

手指按下移动的状态:                 SCROLL_STATE_TOUCH_SCROLL: // 触摸滑动

惯性滚动(滑翔(flgin)状态):  SCROLL_STATE_FLING: // 滑翔

静止状态:                                   SCROLL_STATE_IDLE: // 静止

3、对不同的状态进行处理:

分批加载数据,只关心静止状态:关心最后一个可见的条目,如果最后一个可见条目就是数据适配器(集合)里的最后一个,此时可加载更多的数据。在每次加载的时候,计算出滚动的数量,当滚动的数量大于等于总数量的时候,可以提示用户无更多数据了。

 

示例代码:(详细代码请参见【6.3-ListView数据的分批加载.doc】)

// 给listview注册一个滚动的监听器.

lv_call_sms_safe.setOnScrollListener(new OnScrollListener() {

   // 当滚动状体发生变化的时候调用的方法

   @Override

   public void onScrollStateChanged(AbsListView view, int scrollState) {

          switch (scrollState) {

          case SCROLL_STATE_FLING: // 滑翔

                 break;

          case SCROLL_STATE_IDLE: // 静止

                 // 在静止状态下 关心最后一个可见的条目 如果最后一个可见条目就是 数据适配器里面的最后一个 , 加载更多数据.

                 int position = lv_call_sms_safe.getLastVisiblePosition(); // 位置从0开始

                 int size = blackNumbers.size();// 从1开始的.

                 if (position == (size - 1)) {

                        Log.i(TAG, "拖动到了最后一个条目,加载更多数据");

                        startIndex += maxNumber;

                        if(startIndex>=totalCount){

                               Toast.makeText(getApplicationContext(), "没有更多数据了..", 0).show();

                               return;

                        }

                        fillData();

                        break;

                 }

                 break;

          case SCROLL_STATE_TOUCH_SCROLL: // 触摸滑动

                 break;

          }

   }

   // 当listview被滚动的时候 调用的方法

   @Override

   public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

   }

});

 

/**

* 填充数据

*/

private void fillData() {

   // 通知用户一下正在获取数据

   ll_loading.setVisibility(View.VISIBLE);

   new Thread() {

          public void run() {

                 // 获取全部的黑名单号码

                 if (blackNumbers != null) {

                        blackNumbers.addAll(dao.findPartBlackNumbers(startIndex,maxNumber));

                 } else {

                        blackNumbers = dao.findPartBlackNumbers(startIndex,maxNumber);

                 }

                 handler.sendEmptyMessage(0);

                 // lv_call_sms_safe.setAdapter(new CallSmsSafeAdapter());

          };

   }.start();

}

 

 

三、复杂ListView的处理:(待进一步总结)

说明:

   listView的界面显示是通过getCount和getView这两个方法来控制的

   getCount:返回有多少个条目

   getView:返回每个位置条目显示的内容

提供思路:

   对于含有多个类型的item的优化处理:由于ListView只有一个Adapter的入口,可以定义一个总的Adapter入口,存放各种类型的Adapter

以安全卫士中的进程管理的功能为例。效果如图:

1、定义两个(或多个)集合

   每个集合中存入的是对应不同类型的内容(这里为:用户程序(userAppinfos)和系统程序的集合(systemAppinfos))

2、在初始化数据(填充数据)中初始化两个集合

   如,此处是在fillData方法中初始化

3、在数据适配器中,复写对应的方法

   getCount():计算所有需要显示的条目个数,这里包括listView和textView

   getView():对显示在不同位置的条目进行if处理

4、数据类型的判断

   需要注意的是,在复用view的时候,需要对convertView进行类型判断,是因为这里含有各种不同类型的view,在view滚动显示的时候,对于不同类型的view不能复用,所有需要判断

 

示例代码:

获取条目个数

public int getCount() {

   // 用户程序个数 + 系统程序个数

   return userAppinfos.size() + 1 + systemAppinfos.size() + 1;

}

 

类型判断:

if (convertView != null && convertView instanceof RelativeLayout) {

   view = convertView;

   holder = (ViewHolder) view.getTag();

} else {

   //……..

}

 

getView中条目位置的选择:

if (position == 0) {// 显示一个textview的标签 , 告诉用户用户程序有多少个

          TextView tv = new TextView(getApplicationContext());

          tv.setBackgroundColor(Color.GRAY);

          tv.setTextColor(Color.WHITE);

          tv.setText("用户程序:" + userAppinfos.size() + "个");

          return tv;

   } else if (position == (userAppinfos.size() + 1)) {

          TextView tv = new TextView(getApplicationContext());

          tv.setBackgroundColor(Color.GRAY);

          tv.setTextColor(Color.WHITE);

          tv.setText("系统程序:" + systemAppinfos.size() + "个");

          return tv;

   } else if (position <= userAppinfos.size()) {// 用户程序

          appInfo = userAppinfos.get(position - 1);

   } else {// 系统程序

          appInfo = systemAppinfos.get(position - 1 - userAppinfos.size() - 1);

   }

 

 

四、ListView中图片的优化:

1、处理图片的方式:

如果自定义Item中有涉及到图片等等的,一定要狠狠的处理图片,图片占的内存是ListView项中最恶心的,处理图片的方法大致有以下几种:

①、不要直接拿路径就去循环decodeFile();使用Option保存图片大小、不要加载图片到内存去

②、拿到的图片一定要经过边界压缩

③、在ListView中取图片时也不要直接拿个路径去取图片,而是以WeakReference(使用WeakReference代替强引用。

比如可以使用WeakReference mContextRef)、SoftReference、WeakHashMap等的来存储图片信息,是图片信息不是图片哦!

④、在getView中做图片转换时,产生的中间变量一定及时释放

2、异步加载图片基本思想:

(待进一步总结,详见曹睿新闻案例【E:\JAVA\SOURCE\TSource\lessons\Android\PROJECT\PhoneLottory\day06\Optimization】)

1)、 先从内存缓存中获取图片显示(内存缓冲)

2)、获取不到的话从SD卡里获取(SD卡缓冲)

3)、都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示(视情况看是否要显示)

原理:

优化一:先从内存中加载,没有则开启线程从SD卡或网络中获取,这里注意从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅。

优化二:于此同时,在adapter里有个busy变量,表示listview是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话无需再开启线程去外存或网络获取图片。

优化三:ImageLoader里的线程使用了线程池,从而避免了过多线程频繁创建和销毁,有的童鞋每次总是new一个线程去执行这是非常不可取的,好一点的用的AsyncTask类,其实内部也是用到了线程池。在从网络获取图片时,先是将其保存到sd卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存。

Tips:这里可能出现图片乱跳(错位)的问题:

图片错位问题的本质源于我们的listview使用了缓存convertView,假设一种场景,一个listview一屏显示九个item,那么在拉出第十个item的时候,事实上该item是重复使用了第一个item,也就是说在第一个item从网络中下载图片并最终要显示的时候,其实该item已经不在当前显示区域内了,此时显示的后果将可能在第十个item上输出图像,这就导致了图片错位的问题。所以解决之道在于可见则显示,不可见则不显示。在ImageLoader里有个imageViewsmap对象,就是用于保存当前显示区域图像对应的url集,在显示前判断处理一下即可。

 

Adapter示例代码:

public class LoaderAdapter extends BaseAdapter{

        private static final String TAG = "LoaderAdapter";

        private boolean mBusy = false;             //是否处于滑动中

        public void setFlagBusy(boolean busy) {

                this.mBusy = busy;

        }

         

        private ImageLoader mImageLoader;

        private int mCount;

        private Context mContext;

        private String[] urlArrays;

        

        public LoaderAdapter(int count, Context context, String []url) {

                this.mCount = count;

                this.mContext = context;

                urlArrays = url;

                mImageLoader = new ImageLoader(context);

        }

        public ImageLoader getImageLoader(){

                return mImageLoader;

        }

        @Override

        public int getCount() {

                return mCount;

        }

         @Override

        public Object getItem(int position) {

                return position;

        }

         @Override

        public long getItemId(int position) {

                return position;

        }

        @Override

        public View getView(int position, View convertView, ViewGroup parent) {

                 ViewHolder viewHolder = null;

                if (convertView == null) {  //加载新创建的view

                        convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

                        viewHolder = new ViewHolder();

                        viewHolder.mTextView = (TextView) convertView.findViewById(R.id.tv_tips);

                        viewHolder.mImageView = (ImageView) convertView.findViewById(R.id.iv_image);

                        convertView.setTag(viewHolder);

                } else {

                        viewHolder = (ViewHolder) convertView.getTag();

                }

                String url = "";

                url = urlArrays[position % urlArrays.length];

viewHolder.mImageView.setImageResource(R.drawable.ic_launcher);

               

                if (!mBusy) {

                        mImageLoader.DisplayImage(url, viewHolder.mImageView, false);

                        viewHolder.mTextView.setText("--" + position + "--IDLE ||TOUCH_SCROLL");

                } else {

                        mImageLoader.DisplayImage(url, viewHolder.mImageView, true);               

                        viewHolder.mTextView.setText("--" + position + "--FLING");

                }

//复用历史缓存view

                return convertView;

        }

 

        static class ViewHolder {

                TextView mTextView;

                ImageView mImageView;

        }

}

 

3、内存缓冲机制:

首先限制内存图片缓冲的堆内存大小,每次有图片往缓存里加时判断是否超过限制大小,超过的话就从中取出最少使用的图片并将其移除。

当然这里如果不采用这种方式,换做软引用也是可行的,二者目的皆是最大程度的利用已存在于内存中的图片缓存,避免重复制造垃圾增加GC负担;OOM溢出往往皆因内存瞬时大量增加而垃圾回收不及时造成的。只不过二者区别在于LinkedHashMap里的图片缓存在没有移除出去之前是不会被GC回收的,而SoftReference里的图片缓存在没有其他引用保存时随时都会被GC回收。所以在使用LinkedHashMap这种LRU算法缓存更有利于图片的有效命中,当然二者配合使用的话效果更佳,即从LinkedHashMap里移除出的缓存放到SoftReference里,这就是内存的二级缓存。

 

本例采用的是LRU算法,先看看MemoryCache的实现

public class MemoryCache {

        private static final String TAG = "MemoryCache";

        // 放入缓存时是个同步操作

        // LinkedHashMap构造方法的最后一个参数true代表这个map里的元素将按照最近使用次数由少到多排列,即LRU

        // 这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率

        private Map<String, Bitmap> cache = Collections

                        .synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));

        // 缓存中图片所占用的字节,初始0,将通过此变量严格控制缓存所占用的堆内存

        private long size = 0;// current allocated size

        // 缓存只能占用的最大堆内存

        private long limit = 1000000;// max memory in bytes

         public MemoryCache() {

                // use 25% of available heap size

                setLimit(Runtime.getRuntime().maxMemory() / 10);

        }

        public void setLimit(long new_limit) {

                limit = new_limit;

                Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");

        }

        public Bitmap get(String id) {

                try {

                        if (!cache.containsKey(id))

                                return null;

                        return cache.get(id);

                } catch (NullPointerException ex) {

                        return null;

                }

        }

        public void put(String id, Bitmap bitmap) {

                try {

                        if (cache.containsKey(id))

                                size -= getSizeInBytes(cache.get(id));

                        cache.put(id, bitmap);

                        size += getSizeInBytes(bitmap);

                        checkSize();

                } catch (Throwable th) {

                        th.printStackTrace();

                }

        }

         /**

         * 严格控制堆内存,如果超过将首先替换最近最少使用的那个图片缓存

         *

         */

        private void checkSize() {

                Log.i(TAG, "cache size=" + size + " length=" + cache.size());

                if (size > limit) {

                        // 先遍历最近最少使用的元素

                        Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();

                        while (iter.hasNext()) {

                                Entry<String, Bitmap> entry = iter.next();

                                size -= getSizeInBytes(entry.getValue());

                                iter.remove();

                                if (size <= limit)

                                        break;

                        }

                        Log.i(TAG, "Clean cache. New size " + cache.size());

                }

        }

        public void clear() {

          cache.clear();

        }

 

        /**

         * 图片占用的内存

             * <a href="\"http://www.eoeandroid.com/home.php?mod=space&uid=2768922\"" target="\"_blank\"">@Param</a> bitmap

            * @return

         */

        long getSizeInBytes(Bitmap bitmap) {

                if (bitmap == null)

                        return 0;

                return bitmap.getRowBytes() * bitmap.getHeight();

        }

}

 

五、ListView的其他优化:

1、尽量避免在BaseAdapter中使用static 来定义全局静态变量:

static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(比如Context的情况最多),这时就要尽量避免使用了。

2、尽量使用getApplicationContext:

如果为了满足需求下必须使用Context的话:Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题

3、尽量避免在ListView适配器中使用线程:

因为线程产生内存泄露的主要原因在于线程生命周期的不可控制。之前使用的自定义ListView中适配数据时使用AsyncTask自行开启线程的,这个比用Thread更危险,因为Thread只有在run函数不 结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了线程执行池(ThreadPoolExcutor),这个类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。解决办法如下:

①、将线程的内部类,改为静态内部类。

②、在线程内部采用弱引用保存Context引用

 

示例代码:

public abstract class WeakAsyncTask extends  AsyncTask {

protected WeakReference mTarget;

public WeakAsyncTask(WeakTarget target) { 

mTarget = new WeakReference(target); 

}

@Override

protected final void onPreExecute() {

final WeakTarget target = mTarget.get(); 

if (target != null) { 

this.onPreExecute(target); 

}

 

   @Override 

protected final Result doInBackground(Params... params) { 

final WeakTarget target = mTarget.get();

if (target != null) { 

return this.doInBackground(target, params); 

} else {

return null; 

}

@Override

protected final void onPostExecute(Result result) {

 final WeakTarget target = mTarget.get(); 

if (target != null) { 

this.onPostExecute(target, result);   

}

 protected void onPreExecute(WeakTarget target) {

// No default action

 }   

   protected abstract Result doInBackground(WeakTarget target, Params... params);   

protected void onPostExecute(WeakTarget target, Result result) {   

 // No default action

}

 } 

 

六、ScrollView和ListView的冲突问题:【摘自网络】

解决方法之一:

在ScrollView添加一个ListView会导致listview控件显示不全,这是因为两个控件的滚动事件冲突导致。所以需要通过listview中的item数量去计算listview的显示高度,从而使其完整展示,如下提供一个方法供大家参考。

示例代码:

public void setListViewHeightBasedOnChildren(ListView listView) { 

ListAdapter listAdapter = listView.getAdapter();

if (listAdapter == null) { 

return; 

 

int totalHeight = 0; 

for (int i = 0; i < listAdapter.getCount(); i++) { 

View listItem = listAdapter.getView(i, null, listView); 

listItem.measure(0, 0); 

totalHeight += listItem.getMeasuredHeight(); 

 

ViewGroup.LayoutParams params = listView.getLayoutParams(); 

params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); 

params.height += 5;//if without this statement,the listview will be a little short 

listView.setLayoutParams(params); 

}

 

 

  • 熟悉XML/JSON解析数据,以及数据存储方式

数据的存储方式包括:File、SheredPreferences、XML/JSON、数据库、网络

 

XML/JSON解析数据:

一、XML解析

1.解析 *****

              获取解析器: Xml.newPullParser()

              设置输入流: setInput()

              获取当前事件类型: getEventType()

              解析下一个事件, 获取类型: next()

              获取标签名: getName()

              获取属性值: getAttributeValue()

              获取下一个文本: nextText()

              获取当前文本: getText()

              5种事件类型: START_DOCUMENT, END_DOCUMENT, START_TAG, END_TAG, TEXT

 

示例代码:

public List<Person> getPersons(InuptStream in){      

       XmlPullParser parser=Xml.newPullParser();//获取解析器

       parser.setInput(in,"utf-8");

       for(int type=){   //循环解析

      

       }    

}

 

2.生成 *

              获取生成工具: Xml.newSerializer()

              设置输出流: setOutput()

              开始文档: startDocument()

              结束文档: endDocument()

              开始标签: startTag()

              结束标签: endTag()

              属性: attribute()

              文本: text()   

示例代码:

XmlSerializer serial=Xml.newSerializer();//获取xml序列化工具

serial.setOuput(put,"utf-8");

serial.startDocument("utf-8",true);

serial.startTag(null,"persons");

for(Person p:persons){

       serial.startTag(null,"persons");     

       serial.attribute(null,"id",p.getId().toString());

      

       serial.startTag(null,"name"); 

       serial.attribute(null,"name",p.getName().toString());

       serial.endTag(null,"name");  

      

       serial.startTag(null,"age");    

       serial.attribute(null,"age",p.getAge().toString());

       serial.endTag(null,"age");

      

       serial.endTag(null,"persons");      

      

}

 

(二)JSON解析

1、JSON书写格式:

1)JSON的规则很简单:对象是一个无序的“‘名称/值’对”集合。

一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’对”之间使用“,”(逗号)分隔。

2)规则如下:

①映射用冒号(“:”)表示。名称:值

②并列的数据之间用逗号(“,”)分隔。名称1:值1,名称2:值2

③映射的集合(对象)用大括号(“{}”)表示。{名称1:值,名称2:值2}

④并列数据的集合(数组)用方括号(“[]”)表示。

  [

    {名称1:值,名称2:值2},

    {名称1:值,名称2:值2}

  ]

⑤元素值可具有的类型:string, number, object, array, true, false, null

2、举例:

1)JSON对象(键值对或键值对的集合)

例1、{ "name": "Obama"}

例2、{ "name": "Romney","age": 56}

例3、{ "city":{"name": "bj"},"weatherinfo":{"weather": "sunny"}}

例4、{

         "city":{"name": "北京",“city_id”:"101010100"},

         "weatherinfo":{"weather": "sunny","temp":"29度"}

      }

2)JSON数组

例1、

[

   { "name": "张三", "age":22, "email": "zhangsan@qq.com" },

   { "name": "李四", "age":23, "email": "lisi@qq.com"},

   { "name": "王五", "age":24, "email": "wangwu@qq.com" }

]

例2、

{ "student":

   [

      { "name": "张三", "age":22, "email": "zhangsan@qq.com" },

      { "name": "李四", "age":23, "email": "lisi@qq.com"},

      { "name": "王五", "age":24, "email": "wangwu@qq.com" }

    ]

}

[ {

     title : "国家发改委:台湾降油价和大陆没可比性",

     description : "国家发改委副主任朱之鑫",

     image : "http://192.168.1.101/Web/img/a.jpg",

     comment : 163

},{

     title : "国家发改委:台湾降油价和大陆没可比性",

     description : "国家发改委副主任朱之鑫",

     image : "http://192.168.1.101/Web/img/b.jpg",

     comment : 0

}, {

     title : "国家发改委:台湾降油价和大陆没可比性",

     description : "国家发改委副主任朱之鑫",

     image : "http://192.168.1.101/Web/img/c.jpg",

     comment : 0

} ];

 

3、在Android中使用json

在Android中内置了JSON的解析API,在org.json包中包含了如下几个类:

       JSONArray、JSONObject、JSONStringer、JSONTokener和一个异常类JSONException

 

4、JSON解析:

解析步骤

1)、读取html文件源代码,获取一个json字符串

       InputStream in = conn.getInputStream();

       String jsonStr = DataUtil.Stream2String(in);//将流转换成字符串的工具类

2)、将字符串传入响应的JSON构造函数中

①、通过构造函数将json字符串转换成json对象

       JSONObject  jsonObject = new JSONObject(jsonStr);

②、通过构造函数将json字符串转换成json数组

JSONArray array = new JSONArray(jsonStr);

3)、解析出JSON中的数据信息:

①、从json对象中获取你所需要的键所对应的值

       JSONObject  json=jsonObject.getJSONObject("weatherinfo");

       String city = json.getString("city");

       String temp = json.getString("temp")

②、遍历JSON数组,获取数组中每一个json对象,同时可以获取json对象中键对应的值

       for (int i = 0; i < array.length(); i++) {

              JSONObject obj = array.getJSONObject(i);

              String title=obj.getString("title");

              String description=obj.getString("description");

       }

 

注意:

①json数组并非全是由json对象组成的数组

②json数组中的每一个元素数据类型可以不相同

如:[94043,90210]或者["zhangsan",24]类似于javascript中的数组

 

5、生成JSON对象和数组:

1)生成JSON:

方法1、创建一个map,通过构造方法将map转换成json对象

       Map<String, Object> map = new HashMap<String, Object>();

       map.put("name", "zhangsan");

       map.put("age", 24);

       JSONObject json = new JSONObject(map);

方法2、创建一个json对象,通过put方法添加数据

       JSONObject json=new JSONObject();

       json.put("name", "zhangsan");

       json.put("age", 24);

 

2)生成JSON数组:

方法1、创建一个list,通过构造方法将list转换成json对象

       Map<String, Object> map1 = new HashMap<String, Object>();

       map1.put("name", "zhangsan");

       map1.put("age", 24);

       Map<String, Object> map2 = new HashMap<String, Object>();

       map2.put("name", "lisi");

       map2.put("age", 25);

       List<Map<String, Object>> list=new ArrayList<Map<String,Object>>();

       list.add(map1);

       list.add(map2);

       JSONArray array=new JSONArray(list);

       System.out.println(array.toString());

 

  • 精通Android下的Hendler机制,并能熟练使用

Message:消息;其中包含了消息ID,消息对象以及处理的数据等,由MessageQueue统一列队,终由Handler处理

Handler:处理者;负责Message发送消息及处理。Handler通过与Looper进行沟通,从而使用Handler时,需要实现handlerMessage(Message msg)方法来对特定的Message进行处理,例如更新UI等(主线程中才行)

MessageQueue:消息队列;用来存放Handler发送过来的消息,并按照FIFO(先入先出队列)规则执行。当然,存放Message并非实际意义的保存,而是将Message以链表的方式串联起来的,等Looper的抽取。

Looper:消息泵,不断从MessageQueue中抽取Message执行。因此,一个线程中的MessageQueue需要一个Looper进行管理。Looper是当前线程创建的时候产生的(UI Thread即主线程是系统帮忙创建的Looper,而如果在子线程中,需要手动在创建线程后立即创建Looper[调用Looper.prepare()方法])。也就是说,会在当前线程上绑定一个Looper对象。

Thread:线程;负责调度消息循环,即消息循环的执行场所。

知识要点 

一、说明

1、handler应该由处理消息的线程创建。

2、handler与创建它的线程相关联,而且也只与创建它的线程相关联。handler运行在创建它的线程中,所以,如果在handler中进行耗时的操作,会阻塞创建它的线程。

二、一些知识点

1、Android的线程分为有消息循环的线程和没有消息循环的线程,有消息循环的线程一般都会有一个Looper。主线程(UI线程)就是一个消息循环的线程。

2、获取looper:

Looper.myLooper();      //获得当前的Looper

Looper.getMainLooper () //获得UI线程的Lopper

3、Handle的初始化函数(构造函数),如果没有参数,那么他就默认使用的是当前的Looper,如果有Looper参数,就是用对应的线程的Looper。

4、如果一个线程中调用Looper.prepare(),那么系统就会自动的为该线程建立一个消息队列,然后调用 Looper.loop();之后就进入了消息循环,这个之后就可以发消息、取消息、和处理消息。

 

消息处理机制原理:

 

 

一、大致流程:

在创建Activity之前,当系统启动的时候,先加载ActivityThread这个类,在这个类中的main函数,调用了Looper.prepareMainLooper();方法进行初始化Looper对象;然后创建了主线程的handler对象(Tips:加载ActivityThread的时候,其内部的Handler对象[静态的]还未创建);随后才创建了ActivityThread对象;最后调用了Looper.loop();方法,不断的进行轮询消息队列的消息。也就是说,在ActivityThread和Activity创建之前(同样也是Handler创建之前,当然handler由于这两者初始化),就已经开启了Looper的loop()方法,不断的进行轮询消息。需要注意的是,这个轮询的方法是阻塞式的,没有消息就一直等待(实际是等着MessageQueue的next()方法返回消息)。在应用一执行的时候,就已经开启了Looper,并初始化了Handler对象。此时,系统的某些组件或者其他的一些活动等发送了系统级别的消息,这个时候主线程中的Looper就可以进行轮询消息,并调用msg.target.dispatchMessage(msg)(msg.target即为handler)进行分发消息,并通过handler的handleMessage方法进行处理;所以会优于我们自己创建的handler中的消息而处理系统消息。

 

0、准备数据和对象:

①、如果在主线程中处理message(即创建handler对象),那么如上所述,系统的Looper已经准备好了(当然,MessageQueue也初始化了),且其轮询方法loop已经开启。【系统的Handler准备好了,是用于处理系统的消息】。【Tips:如果是子线程中创建handler,就需要显式的调用Looper的方法prepare()和loop(),初始化Looper和开启轮询器】

②、通过Message.obtain()准备消息数据(实际是从消息池中取出的消息)

③、创建Handler对象,在其构造函数中,获取到Looper对象、MessageQueue对象(从Looper中获取的),并将handler作为message的标签设置到msg.target上

1、发送消息:sendMessage():通过Handler将消息发送给消息队列

2、给Message贴上handler的标签:在发送消息的时候,为handler发送的message贴上当前handler的标签

3、开启HandlerThread线程,执行run方法。

4、在HandlerThread类的run方法中开启轮询器进行轮询:调用Looper.loop()方法进行轮询消息队列的消息

Tips:这两步需要再斟酌,个人认为这个类是自己手动创建的一个线程类,Looper的开启在上面已经详细说明了,这里是说自己手动创建线程(HandlerThread)的时候,才会在这个线程中进行Looper的轮询的】

5、在消息队列MessageQueue中enqueueMessage(Message msg, long when)方法里,对消息进行入列,即依据传入的时间进行消息入列(排队)

6、轮询消息:与此同时,Looper在不断的轮询消息队列

7、在Looper.loop()方法中,获取到MessageQueue对象后,从中取出消息(Message msg = queue.next()

8、分发消息:从消息队列中取出消息后,调用msg.target.dispatchMessage(msg);进行分发消息

9、将处理好的消息分发给指定的handler处理,即调用了handler的dispatchMessage(msg)方法进行分发消息。

10、在创建handler时,复写的handleMessage方法中进行消息的处理

11、回收消息:在消息使用完毕后,在Looper.loop()方法中调用msg.recycle(),将消息进行回收,即将消息的所有字段恢复为初始状态

 

测试代码:

/**

 * Handler 构造函数测试

 * @author zhaoyu 2013-10-5 上午9:56:38

 */

public class HandlerConstructorTest extends Activity {

       private Handler handler1 = new Handler(new Callback() {

              @Override

              public boolean handleMessage(Message msg) {

                     System.out.println("使用了Handler1中的接口Callback");

                     return false;           // 此处,如果返回 false,下面的 handlerMessage方法会执行,true ,下面的不执行

              }

       });

      

       private Handler handler2 = new Handler() {

              public void handleMessage(Message msg) {

                     System.out.println("Handler2");

              }

       };

 

       protected void onCreate(Bundle savedInstanceState) {

              super.onCreate(savedInstanceState);

              //消息1

Message obtain1 = Message.obtain();

              obtain1.obj = "sendMessage";

              obtain1.what = 1;

              handler1.sendMessage(obtain1);

              //消息2

              Message obtain2 = handler2.obtainMessage();

              handler2.sendMessage(obtain2);    //①

//            handler2.dispatchMessage(obtain2);      //②

       }

}

 

 

二、详细解释:

1、准备Looper对象

两种情况初始化Looper对象:

1)在主线程中不需要显式的创建Looper对象,直接创建Handler对象即可;因为在主线程ActivityThread的main函数中已经自动调用了创建Looper的方法:Looper.prepareMainLooper();,并在最后调用了Looper.loop()方法进行轮询。

2)如果在子线程中创建Handler对象,需要创建Looper对象,即调用显式的调用Looper.prepare()

初始化Looper的工作:

1)初始化Looper对象:通过调用Looper.prepare()初始化Looper对象,在这个方法中,新创建了Looper对象

2)将Looper绑定到当前线程:在初始化中,调用sThreadLocal.set(new Looper(quitAllowed))方法,将其和ThreadLocal进行绑定

在ThreadLocal对象中的set方法,是将当前线程和Looper绑定到一起:首先获取到当前的线程,并获取线程内部类Values,通过Thread.Values的put方法,将当前线程和Looper对象进行绑定到一起。即将传入的Looper对象挂载到当前线程上。

Tips:在Looper对象中,可以通过getThread()方法,获取到当前线程,即此Looper绑定的线程对象。

源代码:

Looper中:

public static void prepare() {

        prepare(true);

    }

    private static void prepare(boolean quitAllowed) {

        if (sThreadLocal.get() != null) {

            throw new RuntimeException("Only one Looper may be created per thread");

        }

        sThreadLocal.set(new Looper(quitAllowed));

    }

ThreadLocal中:

public void set(T value) {

        Thread currentThread = Thread.currentThread();

        Values values = values(currentThread);

        if (values == null) {

            values = initializeValues(currentThread);

        }

        values.put(this, value);

    }

2、创建消息Message

消息的创建可以通过两种方式:

1)new Message()

2)Message.obtain():【当存在多个handler的时候,可以通过Message.obtain(Handler handler)创建消息,指定处理的handler对象】

Tips:建议使用第二种方式更好一些。原因:

       因为通过第一种方式,每有一个新消息,都要进行new一个Message对象,这会创建出多个Message,很占内存。

       而如果通过obtain的方法,是从消息池sPool中取出消息。每次调用obtain()方法的时候,先判断消息池是否有消息(if (sPool != null)),没有则创建新消息对象,有则从消息池中取出消息,并将取出的消息从池中移除【具体看obtain()方法】

public static Message obtain() {

        synchronized (sPoolSync) {

            if (sPool != null) {

                Message m = sPool;

                sPool = m.next;

                m.next = null;

                sPoolSize--;

                return m;

            }

        }

        return new Message();

    }

 

public Message() {

    }

3、创建Handler对象

两种形式创建Handler对象:

1)创建无参构造函数的Handler对象:

2)创建指定Looper对象的Handler对象

最终都会调用相应的含有Callback和boolean类型的参数的构造函数

【这里的Callback是控制是否分发消息的,其中含有一个返回值为boolean的handleMessage(Message msg)方法进行判断的;

  boolean类型的是参数是判断是否进行异步处理,这个参数默认是系统处理的,我们无需关心】

在这个构造函数中,进行了一系列的初始化工作:

①、获取到当前线程中的Looper对象

②、通过Looper对象,获取到消息队列MessageQueue对象

③、获取Callback回调对象

④、获取异步处理的标记

源代码:

①、创建无参构造函数的Handler对象:

public Handler() {

        this(null, false);

    }

public Handler(Callback callback, boolean async) {

        if (FIND_POTENTIAL_LEAKS) {

            final Class<? extends Handler> klass = getClass();

            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && (klass.getModifiers() & Modifier.STATIC) == 0) {

                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +klass.getCanonicalName());

            }

        }

        mLooper = Looper.myLooper();

        if (mLooper == null) {

            throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");

        }

        mQueue = mLooper.mQueue;

        mCallback = callback;

        mAsynchronous = async;

    }

 

②、创建指定Looper对象的Handler对象

public Handler(Looper looper) {

        this(looper, null, false);

    }

public Handler(Looper looper, Callback callback, boolean async) {

        mLooper = looper;

        mQueue = looper.mQueue;

        mCallback = callback;

        mAsynchronous = async;

    }

 

4、Handler对象发送消息:

1)Handler发送消息给消息队列:

Handler对象通过调用sendMessage(Message msg)方法,最终将消息发送给消息队列进行处理

这个方法(所有重载的sendMessage)最终调用的是enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)

(1)先拿到消息队列:在调用到sendMessageAtTime(Message msg, long uptimeMillis)方法的时候,获取到消息队列(在创建Handler对象时获取到的)

(2)当消息队列不为null的时候(为空直接返回false,告知调用者处理消息失败),再调用处理消息入列的方法:

enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)

这个方法,做了三件事:

①、为消息打上标签:msg.target = this;:将当前的handler对象这个标签贴到传入的message对象上,为Message指定处理者

②、异步处理消息:msg.setAsynchronous(true);,在asyn为true的时候设置

③、将消息传递给消息队列MessageQueue进行处理:queue.enqueueMessage(msg, uptimeMillis);

 

public final boolean sendMessage(Message msg){

        return sendMessageDelayed(msg, 0);

    }

public final boolean sendMessageDelayed(Message msg, long delayMillis){

        if (delayMillis < 0) {

            delayMillis = 0;

        }

        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);

    }

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {

        MessageQueue queue = mQueue;

        if (queue == null) {

            RuntimeException e = new RuntimeException(

                    this + " sendMessageAtTime() called with no mQueue");

            Log.w("Looper", e.getMessage(), e);

            return false;

        }

        return enqueueMessage(queue, msg, uptimeMillis);

    }

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {

        msg.target = this;

        if (mAsynchronous) {

            msg.setAsynchronous(true);

        }

        return queue.enqueueMessage(msg, uptimeMillis);

    }

 

2)MessageQueue消息队列处理消息:

在其中的enqueueMessage(Message msg, long when)方法中,工作如下:

在消息未被处理且handler对象不为null的时候,进行如下操作(同步代码块中执行)

①、将传入的处理消息的时间when(即为上面的uptimeMillis)赋值为当前消息的when属性。

②、将next()方法中处理好的消息赋值给新的消息引用:Message p = mMessages;

       在next()方法中:不断的从消息池中取出消息,赋值给mMessage,当没有消息发来的时候,Looper的loop()方法由于是阻塞式的,就一直等消息传进来

③、当传入的时间为0,且next()方法中取出的消息为null的时候,将传入的消息msg入列,排列在消息队列上,此时为消息是先进先出的

       否则,进入到死循环中,不断的将消息入列,根据消息的时刻(when)来排列发送过来的消息,此时消息是按时间的先后进行排列在消息队列上的

final boolean enqueueMessage(Message msg, long when) {

        if (msg.isInUse()) {

            throw new AndroidRuntimeException(msg + " This message is already in use.");

        }

        if (msg.target == null) {

            throw new AndroidRuntimeException("Message must have a target.");

        }

        boolean needWake;

        synchronized (this) {

            if (mQuiting) {

                RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread");

                Log.w("MessageQueue", e.getMessage(), e);

                return false;

            }

 

            msg.when = when;

            Message p = mMessages;

            if (p == null || when == 0 || when < p.when) {

                // New head, wake up the event queue if blocked.

                msg.next = p;

                mMessages = msg;

                needWake = mBlocked;

            } else {

                needWake = mBlocked && p.target == null && msg.isAsynchronous();

                Message prev;

                for (;;) {

                    prev = p;

                    p = p.next;

                    if (p == null || when < p.when) {

                        break;

                    }

                    if (needWake && p.isAsynchronous()) {

                        needWake = false;

                    }

                }

                msg.next = p; // invariant: p == prev.next

                prev.next = msg;

            }

        }

        if (needWake) {

            nativeWake(mPtr);

        }

        return true;

    }

 

5、轮询Message

1)开启loop轮询消息

当开启线程的时候,执行run方法,在HandlerThread类中,调用的run方法中将开启loop进行轮询消息队列:

在loop方法中,先拿到MessageQueue对象,然后死循环不断从队列中取出消息,当消息不为null的时候,通过handler分发消息:msg.target.dispatchMessage(msg)。消息分发完之后,调用msg.recycle()回收消息,

2)回收消息:

在Message的回收消息recycle()这个方法中:首先调用clearForRecycle()方法,将消息的所有字段都恢复到原始状态【如flags=0,what=0,obj=null,when=0等等】

然后在同步代码块中将消息放回到消息池sPool中,重新利用Message对象

源代码:

Looper.loop()

public static void loop() {

        final Looper me = myLooper();

        if (me == null) {

            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");

        }

        final MessageQueue queue = me.mQueue;

        Binder.clearCallingIdentity();

        final long ident = Binder.clearCallingIdentity();

        for (;;) {

            Message msg = queue.next(); // might block

            if (msg == null) {

               return;

            }

            // This must be in a local variable, in case a UI event sets the logger

            Printer logging = me.mLogging;

            if (logging != null) {

                logging.println(">>>>> Dispatching to " + msg.target + " " +

                        msg.callback + ": " + msg.what);

            }

            msg.target.dispatchMessage(msg);

            if (logging != null) {

                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

            }

            final long newIdent = Binder.clearCallingIdentity();

            if (ident != newIdent) {

                Log.wtf(TAG, “……”);

            }

            msg.recycle();

        }

    }

 

msg.recycle();

public void recycle() {

        clearForRecycle();

        synchronized (sPoolSync) {

            if (sPoolSize < MAX_POOL_SIZE) {

                next = sPool;

                sPool = this;

                sPoolSize++;

            }

        }

    }

 

/*package*/ void clearForRecycle() {

        flags = 0;

        what = 0;

        arg1 = 0;

        arg2 = 0;

        obj = null;

        replyTo = null;

        when = 0;

        target = null;

        callback = null;

        data = null;

    }

 

6、处理Message

在Looper.loop()方法中调用了msg.target.dispatchMessage(msg);的方法,就是调用了Handler中的dispatchMessage(Message msg)方法:

1)依据Callback中的handleMessage(msg)的真假判断是否要处理消息,如果是真则不进行消息分发,则不处理消息,否则进行处理消息

2)当Callback为null或其handleMessage(msg)的返回值为false的时候,进行分发消息,即调用handleMessage(msg)处理消息【这个方法需要自己复写】

 

/**

     * Subclasses must implement this to receive messages.

     */

    public void handleMessage(Message msg) {

    }

   

    /**

     * Handle system messages here.

     */

    public void dispatchMessage(Message msg) {

        if (msg.callback != null) {

            handleCallback(msg);

        } else {

            if (mCallback != null) {

                if (mCallback.handleMessage(msg)) {

                    return;

                }

            }

            handleMessage(msg);

        }

    }

 

==========

场景一:

在主线程中创建Handler,其中复写了handlerMessager方法(处理message,更新界面)

然后创建子线程,其中创建Message对象,并设置消息,通过handler发送消息

示例代码:

public class MainActivity2 extends Activity implements OnClickListener{

    private Button bt_send;

       private TextView tv_recieve;

       private Handler handler = new Handler(){

              @Override

              public void handleMessage(Message msg) {

                     super.handleMessage(msg);

                     tv_recieve.setText((String) msg.obj);

              }

       };

 

       @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        bt_send = (Button) findViewById(R.id.bt_send);

        tv_recieve = (TextView) findViewById(R.id.tv_recieve);

        bt_send.setOnClickListener(this);

        tv_recieve.setOnClickListener(this);

       }

       @Override

       public void onClick(View v) {

              switch (v.getId()) {

              case R.id.bt_send:

                     new Thread(){

                            public void run() {

                                   Message msg = new Message();

                                   msg.obj = "消息来了"+ System.currentTimeMillis();

                                   handler.sendMessage(msg);

                            }

                     }.start();

                     break;

              }

       }

}

执行过程:

1、Looper.prepare()

在当前线程(主线程)中准备一个Looper对象,即轮询消息队列MessageQueue的对象;此方法会创建一个Looper,在Looper的构造函数中,初始化的创建了一个MessageQueue对象(用于存放消息),并准备好了一个线程供调用

2、new Hnader():

在当前线程中创建出Handler,需要复写其中的handleMessage(Message msg),对消息进行处理(更新UI)。在创建Handler中,会将Looper设置给handler,并随带着MessageQueue对象;其中Looper是通过调用其静态方法myLooper(),返回的是ThreadLocal中的currentThread,并准备好了MessageQueue【mQueue】

3、Looper.loop():

无限循环,对消息队列进行不断的轮询,如果没有获取到消息,就会结束循环;如果有消息,直接从消息队列中取出消息,并通过调用msg.target.dispatchMessage(msg)进行分发消息给各个控件进行处理。

[其中的msg.target实际就是handler]。

4、创建子线程,handler.sendMessage(msg)

在handler.sendMessage(msg)方法中,实际上最终调用sendMessageAtTime(Message msg,long uptimeMillis)方法[sendMessageXXX方法都是最终调用的sendMessageAtTime方法];此方法返回的enqueueMessage(queue,msg,uptimeMillis),实际上返回的是MessageQueue中的enqueueMessage(msg,uptimeMillis),其中进行的操作时对存入的消息进行列队,即根据接收到的消息的时间先后进行排列[使用的单链形式];然后将消息就都存入到了消息队列中,等待着handler进行处理。

 

 

 

 

 

场景二:

创建两个子线程,一个线程中创建Handler并进行处理消息,另一个线程使用handler发送消息。

示例代码:

public class MainActivity extends Activity implements OnClickListener{

 

   private Button bt_send;

       private TextView tv_recieve;

       private Handler handler;

 

       @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        bt_send = (Button) findViewById(R.id.bt_send);

        tv_recieve = (TextView) findViewById(R.id.tv_recieve);

        bt_send.setOnClickListener(this);

        tv_recieve.setOnClickListener(this);

        new Thread(){

               public void run() {

                      //Looper.prepare();

                      handler = new Handler(Looper.getMainLooper()){

                             @Override

                             public void handleMessage(Message msg) {

                                    super.handleMessage(msg);

                                    tv_recieve.setText((String) msg.obj);

                                   

                             }

                      };

                      //Looper.loop();

               }

        }.start();

    }

 

       @Override

       public void onClick(View v) {

              switch (v.getId()) {

              case R.id.bt_send:

                     new Thread(){

                            public void run() {

                                   Message msg = new Message();

                                   msg.obj = "消息来了"+ System.currentTimeMillis();

                                   handler.sendMessage(msg);

                            }

                     }.start();

                     break;

              }

       }

}

 

简单说明执行过程:

说明:在子线程中是不能更新界面的操作的,只能放在主线程中进行更新。所以必须将处理的消息放到主线程中,才能进行更新界面,否则会报错

1、子线程中创建Handler,并处理消息

1)创建Handler:

源码如下:

public Handler(Looper looper, Callback callback, boolean async) {

  mLooper = looper;

  mQueue = looper.mQueue;

  mCallback = callback;

  mAsynchronous = async;

}

这个构造函数做了一下几步工作:

①、创建轮询器:

由于新创建的子线程中没有轮询器,就需要创建一个轮询器,才能进行消息的轮询处理。传入的是主线程的轮询器,就已经将这个looper绑定到主线程上了【传入哪个线程的Looper,就绑定在哪个线程上】

②、将消息队列加入到轮询器上。

消息队列MessageQueue是存放handler发来的消息的,等着Looper进行轮询获取;在一个线程中的MessageQueue需要一个Looper进行管理,所以两者需要同在一个线程中。

③、回调和异步加载。(此处不做分析[其实我还没分析好])

需要注意的是界面更新:

上面说到了,在子线程中是不可以进行更新界面的操作的,这就需要使用带有轮询器参数的handler构造函数进行创建,传入主线程的轮询器:Looper.getMainLooper(),从而将消息加入到主线程的消息队列之中。因此就可进行在handleMessage方法中进行处理消息更新界面了。

2)、消息处理:

复写其中的handleMessage(Message msg),对消息进行处理(更新UI)。在创建Handler中,会将Looper设置给handler,并随带着MessageQueue对象;其中Looper是通过调用其静态方法myLooper(),返回的是ThreadLocal中的currentThread,并准备好了MessageQueue【mQueue】

虽然是在子线程中编写的代码,但是由于传入的是主线程的looper,所以,Looper从MessageQueue队列中轮询获取消息、再进行更新界面的操作都是在主线程中执行的。

3)、Looper.loop():

说明:由于传入的是主线程的Looper,而在主线程中已经有这一步操作了,所以这里就不需要进行显示的调用了。但是主线程在这个时候是做了这个轮询的操作的。

无限循环,对消息队列进行不断的轮询,如果没有获取到消息,就会结束循环;如果有消息,直接从消息队列中取出消息,并通过调用msg.target.dispatchMessage(msg)进行分发消息给各个控件进行处理。

[其中的msg.target实际就是handler]。

2、创建子线程,发送消息handler.sendMessage(msg)

新开一个子线程,发送消息给另一个子线程

在handler.sendMessage(msg)方法中,实际上最终调用sendMessageAtTime(Message msg,long uptimeMillis)方法[sendMessageXXX方法都是最终调用的sendMessageAtTime方法]

此方法返回的enqueueMessage(queue,msg,uptimeMillis),实际上返回的是MessageQueue中的enqueueMessage(msg,uptimeMillis),其中进行的操作时对存入的消息进行列队,即根据接收到的消息的时间先后进行排列[使用的单链形式];然后将消息就都存入到了消息队列中,等待着handler进行处理。

 

 

 

 

  • 对各种引用的简单了解

1.1 临界状态的处理

  • 临界状态:

       当缓存内容过多,同时系统,内存又相对较低时的状态;

  • 临界状态处理:
  1. 低内存预警:

       每当进行数据缓存时需要判断当前系统的内存值是否低于应用预设的最低内存;

       如果是,提示用户应用将在低内存环境下运行;

       Tips

                     Intent.ACTION_DEVICE_STORAGE_LOW;

                     设备内存不足时发出的广播,此广播只能由系统使用,其它APP不可用;

                     Intent.ACTION_DEVICE_STORAGE_OK;

                     设备内存从不足到充足时发出的广播,此广播只能由系统使用,其它APP不可用;

  1. 构建高速缓存(扩展)

1.2对象的引用的级别

       在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。即只有对象处于可触及(reachable)状态,程序才能使用它。

       从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。

这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用

 

1.2.1强引用(StrongReference)

如:Object object = new Object();

特点:

  1. 强引用是使用最普遍的引用;
  2. 如果一个对象具有强引用,那垃圾回收器绝不会回收它,内存不足时,宁抛异常OOM,程序终止也不回收;

1.2.2软引用(SoftReference)

JDK提供创建软引用的类SoftReference

       通过“袋子”(sr) 来拿“内容”(object);

       系统发现不足时,会将“袋子”中的“内容”回收,这时,将拿到null了,此时,这个“壳”也没有用了,需要干掉;

       Object object = new Object();                                 // 占用系统内容较多的对象            (内容)

       SoftReference sr = new SoftReference(object);         // 将object对象的引用级别降低      (袋子)

SoftReference的特点是它的实例保存对一个Java对象的引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。

一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。  

另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null;

特点:

  1. 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;
  2. 如果内存空间不足了,就会回收这些对象的内存,会在抛出OOM之前回收掉;

       c.  只要垃圾回收器没有回收它,该对象就可以被程序使用,软引用可用来实现内存敏感的高速缓存

       d.    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,

              Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

 

  • 说明一下软引用:

       Object object = new Object();                                 // 占用系统内容较多的对象            (内容)

       SoftReference sr = new SoftReference(object);         // 将object对象的引用级别降低      (袋子)

此时,对于这个Object对象,有两个引用路径:

  1. 一个是来自SoftReference对象的软引用;
  2. 一个来自变量object的强引用,所以这个Object对象是强可及对象;

随即,我们可以结束object对这个Object实例的强引用:

       object = null;

此后,这个Object对象成为了软可及对象;

如果垃圾收集线程进行内存垃圾收集,并不会因为有一个SoftReference对该对象的引用而始终保留该对象;

 

Java虚拟机的垃圾收集线程对软可及对象和其他一般Java对象进行了区别对待:

       软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。

       也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,

       对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留。

       在回收这些对象之前,我们可以通过,Object anotherRef=(Object)aSoftRef.get(),重新获得对该实例的强引用。

       回收之后,调用get()方法就只能得到null了。

1.2.3弱引用(WeakReference)

       弱引用与软引用的区别:只具有弱引用的对象拥有更短暂的生命周期。

特点:

  1. 生命周期比软引用更短;
  2. 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
  3. 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象;
  4. 类似于软引用,弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,

Java虚拟机就会把这个弱引用加入到与之关联的引用队列中

1.2.4虚引用(PhantomReference)

       “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

特点:

  1. 形同虚设;
  2. 可用来跟踪对象被垃圾回收器回收的活动;
  3. 虚引用与软引用和弱引用的一个区别在于:

虚引用必须和引用队列 (ReferenceQueue)联合使用,

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中;

1.3 ReferenceQueue与软引用结合使用

  • ReferenceQueue的作用:

引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中;

       利用ReferenceQueue的特性,即用来清除失去了软引用对象的SoftReference;

  • 为什么需要ReferenceQueue

       作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。

       所以,当软可及对象(SoftReference袋中对象)被回收之后,虽然这个SoftReference对象的get()方法返回null;

       但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。

 

       这时候需要用到ReferenceQueue类;

       如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如:

              Object object = new Object();                                                      // 占用系统内容较多的对象            (内容)

              ReferenceQueue queue = new ReferenceQueue();                                 // SoftReference的队列

              SoftReference sr=new SoftReference(object, queue);                    

那么当这个SoftReference所软引用的object被垃圾收集器回收的同时,sr所强引用的SoftReference对象被列入ReferenceQueue。

也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。

另外从ReferenceQueue这个名字也可以看出,它是一个队列;

当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。

       在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。

       如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

       利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。

       于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。

 

示例:

 

一、当界面显示较多的时候,内存就会占用很多,会导致手机内容不足

在UIManager中:

1、判断手机当前的可用内存(如10M——看成应用需要的最大内存(峰值内存))

       在创建的BASEVIEWS的map时处理:

       ①、BASEVIEWS显示大小:要动态的配置,依据内存大小变动,不好

       ②、降低对象的引用级别  <== 目的

              关于Java对象引用级别:强引用、软引用、弱引用、虚引用【1.2以后出现】

                     关于强引用:当new出来一个,为强引用;GC在内存不足时,宁可抛出OOM(内存溢出)的异常也不回收强引用的对象

                     使用软引用:在出现异常之前,让GC在OOM之前就把引用回收掉。软引用:SoftReference

       采用②方案:关于返回键的处理——直接返回到首页,同时提示用户应用在低内存下运行

 

手机彩票代码处理:

1、在一加载的时候,进行判断,当内存够用的时候,创建出强引用集合,不足时,创建软引用的map

private static Map<String, BaseView> BASEVIEWS;// key:子类的简单名称

static {

              if (MemoryManager.hasAcailMemory()) {

                     BASEVIEWS = new HashMap<String, BaseView>();// key:子类的简单名称

              } else {

                     BASEVIEWS = new SoftHashMap<String, BaseView>();// 软引用的map

              }

       }

2、处理返回键:

由于占有内存空间最大的就是BASEVIEWS这个存放界面的集合,当内存不够的时候,回收掉这个集合,即将显示过的界面都清除掉

但是新显示的界面不会有问题,因为是新创建的,不会被干掉。但是当点击返回键的时候,是找不到集合中的内容的,就会出现空指针异常

所以,在处理返回键的时候,就需要特别注意:就直接创建出首页,并返回,提示用户。【主要针对测试用的】

è 当目标view为空的时候,即不存在历史view 的时候,返回主页

①、提示用户:在低内存下运行:

       PromptManager.showToast(getContext(), "应用在低内存下运行");

②、清空返回键

       clear();

③、显示首页

       changeView(Hall.class, null);

 

二、方案二的具体实现:创建软引用的集合:

一)分析:

1、创建出软引用的集合SoftHashMap:

public class SoftHashMap<K, V> extends HashMap<K, V>

2、目的:降低对象的引用级别

①、将V的引用级别降低

②、回收“空袋子”:即存储Object的软引用SoftReference

3、SoftReference(T referent, ReferenceQueue<? super T> q):

       参数1:强引用,相当于手机

       参数2:队列,存“袋子”的队列

①、指定好ReferenceQueue,是存“袋子”的队列,会依据V(强引用(手机))存对应的软引用(袋子);

②、当GC回收的时候,ReferenceQueue会进行查询,如果不为空,会将“空袋子”(没有强引用的软引用)存入到这个队列中

如果队列中有值,说明有“空袋子”。这样,只需要循环这个队列,就可以将“空袋子”进行回收掉了

二)具体实现:

1、创建集合:

①、临时的HashMap:

private HashMap<K, SoftValue<K, V>> temp;

将HashMap中的V包装了一层,就类似于给V加了个软引用【类似给手机加了个袋子】,让系统可以把V回收掉

②、创建存软引用的队列(里面是装V的袋子):

       private ReferenceQueue<V> queue;

 

2、在构造函数中初始化集合:

Tips:

       new一个对象,是作为强引用存在的;

将这个强引用对象(类比为手机)放到SoftReference(类比于袋子)中,就相当于将手机放入袋子中

这样就实现了降低对象的引用级别

当内存不足的时候,GC会将占用空间较多的Object回收,而不会将sr回收掉

如:

//Object object = new Object();// 占有系统内存较多的对象

       // SoftReference sr = new SoftReference(object);// 将object的对象引用级别降低了

①、初始化两个集合:

@temp = new HashMap<K, SoftValue<K, V>>();     

//在操作的时候,是操作的temp这个Map,即存入到这个集合中的对象,而不是系统中的东西,

       @queue = new ReferenceQueue<V>();

       // ReferenceQueue<V>是一个队列,里面放的是软引用(“空袋子”),依据V存的

 

需要重写用到的方法:

       但凡涉及到了super(HashMap中的数据,都不能使用,因为没有软引用的功能)

3、重写put方法:

①、创建出V的对象:SoftReference<V> sr = new SoftReference<V>(value);

②、创建软引用,将强引用对象封装到软引用中(类似于将手机装入袋子): temp.put(key, sr);

不能调用super.put(key,value),这样就调用了HashMap这个集合了;我们需要调用的是temp这个集合(含有软引用的集合)

直接put到temp这个集合中,才能操作到这个集合中的对象

③、返回的为null,或者返回put方法的返回值也可以

 

4、重写get方法:

Tips:也不能用super.get(key),因为没存到父类集合中

①、通过temp这个集合获取到,获取到的是装强引用的软引用对象(即装手机的袋子):

SoftReference<V> sr = temp.get(key);

②、返回软引用的对象:sr.get();

 

5、重写containsKey方法:

①、如果V(手机)被GC回收了,此方法就没有意义了,就无法调用临时map(temp)的containsKey

       所以,只需要判断V是否为空,就能得到是否包含了对应的key的值。

因此,判断获得的强引用的值是否为空,不为空,才调用containsKey

V v = get(key);

              boolean isContain = false;

              if (v != null) {

                     isContain = true;

              }

              return isContain;

 

6、回收软引用:

Tips:

       虽然软引用(袋子)占用内存不多,但是在低内存状态下运行的

如果软引用中都没有引用的“强引用的对象”了,就无需这个软引用的集合了(即“空袋子”)

即:强引用(手机)都没了,要这个软引用(空袋子)也没什么用处了

①、回收“空袋子”:

@方案1:循环temp中的所有内存,若发现V==null,再temp中删除对应的空袋子

              没必要循环temp集合,循环清空每个“空袋子”:

*因为当还没有到OOM(内存溢出)的时候,这个循环没有意义,因为没有强引用被回收掉,所以不会回收掉“袋子”的

*当内存充足的时候,是不会执行这个清空方法的,也没必要清空

@方案2:让GC记录一下回收的内容(集合中:存储空袋子的引用),如果GC回收内容了,集合的size>0,再循环回收

*进行轮询,获取“空袋子”,

                     poll():会轮询此队列,查看是否存在可用的引用对象;如果有的话,进行移除并返回;没有返回null

                            SoftValue<K, V> poll = (SoftValue<K, V>) queue.poll();//获取到的是值

              *循环中再次获取poll,直到集合中没有元素了,就不在循环了

                     集合中的remove方法:remove(key)是没有依据值(value)进行删除的【因为poll返回的是value】

                     这时,就需要改造一下这个remove,创建加强版的“袋子”(见下):

                     当poll(poll()方法返回的值)不为空,再次循环,直到为空,说明“空袋子”都清空了:

                     while (poll != null) {

                            temp.remove(poll.key);

                            poll = (SoftValue<K, V>) queue.poll();

                     }

 

 

创建加强版的“袋子”:存储一下key

       因为系统中的集合中没有直接依据删除指定的元素

       只能remove(Object key),现在是通过加强功能,直接通过“袋子”的key(类似标签)进行删除;

       因此要加强存“袋子”的队列ReferenceQueue<V>

①、创建自定义的类SoftValue<K, V>,继承ReferenceQueue<V>

②、创建构造函数

       通过构造传递key,从而可以获取对应的value

/**

        * 加强版的袋子:存储一下key

        */

       private class SoftValue<K, V> extends SoftReference<V> {

              private Object key;

              public SoftValue(K key, V r, ReferenceQueue<? super V> q) {

                     super(r, q);

                     this.key = key;

              }

       }

 

示例代码:

import java.lang.ref.ReferenceQueue;

import java.lang.ref.SoftReference;

import java.util.HashMap;

 

/**

 * 软引用的map

 *

 * @author Administrator

 *

 * @param <K>

 * @param <V>

 */

public class SoftHashMap<K, V> extends HashMap<K, V> {

       // 降低对象的引用级别

       // ①将V的应用级别降低

       // ②回收"空袋子"

 

       private HashMap<K, SoftValue<K, V>> temp;

 

       private ReferenceQueue<V> queue;// 装V的袋子

 

       public SoftHashMap() {

              // Object object = new Object();// 占有系统内存较多的对象

              // SoftReference sr = new SoftReference(object);// 将object的对象引用级别降低了

 

              temp = new HashMap<K, SoftValue<K, V>>();

              queue = new ReferenceQueue<V>();

       }

 

       @Override

       public V put(K key, V value) {

              SoftValue<K, V> sr = new SoftValue<K, V>(key, value, queue);

              temp.put(key, sr);

              return null;

       }

 

       @Override

       public V get(Object key) {

              clearNullSR();// 清理空袋子

              SoftValue<K, V> sr = temp.get(key);// 如果是空袋子——已经被回收了,获取到的对象为null

              if (sr != null) {

                     return sr.get();

              } else {

                     return null;

              }

       }

 

       @Override

       public boolean containsKey(Object key) {

              // temp.containsKey(key);//如果V(手机)被GC回收了

              clearNullSR();

              return temp.containsKey(key);

       }

 

       /**

        * 回收"空袋子"

        */

       private void clearNullSR() {

              // 方案一:循环temp中所有的内容,如果发现V=null,在temp中删除对应空袋子

              // 方案二:让GC,记录一下回收的内容(集合中:存储空袋子的引用),如果GC回收内容了,集合的size>0

              SoftValue<K, V> poll = (SoftValue<K, V>) queue.poll();

              while (poll != null) {

                     temp.remove(poll.key);

                     poll = (SoftValue<K, V>) queue.poll();

              }

       }

 

       /**

        * 加强版的袋子:存储一下key

        */

       private class SoftValue<K, V> extends SoftReference<V> {

              private Object key;

 

              public SoftValue(K key, V r, ReferenceQueue<? super V> q) {

                     super(r, q);

                     this.key = key;

              }

 

       }

 

}

 

  • 图片的缓存

高效加载大图片

我们在编写Android程序的时候经常要用到许多图片,不同图片总是会有不同的形状、不同的大小,但在大多数情况下,这些图片都会大于我们程序所需要的大小。比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,这些图片的分辨率会比我们手机屏幕的分辨率高得多。大家应该知道,我们编写的应用程序都是有一定内存限制的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。我们可以通过下面的代码看出每个应用程序最高可用内存是多少。

  1. int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
  2. Log.d("TAG""Max memory is " + maxMemory + "KB");  

因此在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。下面我们就来看一看,如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。

BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmapnull了,但是BitmapFactory.OptionsoutWidthoutHeightoutMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:

[java] view plaincopy

  1. BitmapFactory.Options options = new BitmapFactory.Options();  
  2. options.inJustDecodeBounds = true;  
  3. BitmapFactory.decodeResource(getResources(), R.id.myimage, options);  
  4. int imageHeight = options.outHeight;  
  5. int imageWidth = options.outWidth;  
  6. String imageType = options.outMimeType;  

为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。

现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:

  • 预估一下加载整张图片所需占用的内存。
  • 为了加载这一张图片你所愿意提供多少内存。
  • 用于展示这张图片的控件的实际大小。
  • 当前设备的屏幕尺寸和分辨率。

比如,你的ImageView只有128*96像素的大小,只是为了显示一张缩略图,这时候把一张1024*768像素的图片完全加载到内存中显然是不值得的。

那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.OptionsinSampleSize的值就可以实现。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

[java] view plaincopy

  1. public static int calculateInSampleSize(BitmapFactory.Options options,  
  2.         int reqWidth, int reqHeight) {  
  3.     // 源图片的高度和宽度  
  4.     final int height = options.outHeight;  
  5.     final int width = options.outWidth;  
  6.     int inSampleSize = 1;  
  7.     if (height > reqHeight || width > reqWidth) {  
  8.         // 计算出实际宽高和目标宽高的比率  
  9.         final int heightRatio = Math.round((float) height / (float) reqHeight);  
  10.         final int widthRatio = Math.round((float) width / (float) reqWidth);  
  11.         // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高  
  12.         // 一定都会大于等于目标的宽和高。  
  13.         inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
  14.     }  
  15.     return inSampleSize;  
  16. }  

使用这个方法,首先你要将BitmapFactory.OptionsinJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。

[java] view plaincopy

  1. public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,  
  2.         int reqWidth, int reqHeight) {  
  3.     // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小  
  4.     final BitmapFactory.Options options = new BitmapFactory.Options();  
  5.     options.inJustDecodeBounds = true;  
  6.     BitmapFactory.decodeResource(res, resId, options);  
  7.     // 调用上面定义的方法计算inSampleSize  
  8.     options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
  9.     // 使用获取到的inSampleSize值再次解析图片  
  10.     options.inJustDecodeBounds = false;  
  11.     return BitmapFactory.decodeResource(res, resId, options);  
  12. }  

下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。

[java] view plaincopy

  1. mImageView.setImageBitmap(  
  2.     decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100100));  

使用图片缓存技术

在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM

为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。

这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。

内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:

  • 你的设备可以为每个应用程序分配多大的内存?
  • 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
  • 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
  • 图片的尺寸和大小,还有每张图片会占据多少内存空间。
  • 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
  • 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。

并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。

下面是一个使用 LruCache 来缓存图片的例子:

[java] view plaincopy

  1. private LruCache<String, Bitmap> mMemoryCache;  
  2.   
  3. @Override  
  4. protected void onCreate(Bundle savedInstanceState) {  
  5.     // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。  
  6.     // LruCache通过构造函数传入缓存值,以KB为单位。  
  7.     int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
  8.     // 使用最大可用内存值的1/8作为缓存的大小。  
  9.     int cacheSize = maxMemory / 8;  
  10.     mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
  11.         @Override  
  12.         protected int sizeOf(String key, Bitmap bitmap) {  
  13.             // 重写此方法来衡量每张图片的大小,默认返回图片数量。  
  14.             return bitmap.getByteCount() / 1024;  
  15.         }  
  16.     };  
  17. }  
  18.   
  19. public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
  20.     if (getBitmapFromMemCache(key) == null) {  
  21.         mMemoryCache.put(key, bitmap);  
  22.     }  
  23. }  
  24.   
  25. public Bitmap getBitmapFromMemCache(String key) {  
  26.     return mMemoryCache.get(key);  
  27. }  

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4(32/8)的缓存空间。一个全屏幕的 GridView 使用4 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。

[java] view plaincopy

  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     final String imageKey = String.valueOf(resId);  
  3.     final Bitmap bitmap = getBitmapFromMemCache(imageKey);  
  4.     if (bitmap != null) {  
  5.         imageView.setImageBitmap(bitmap);  
  6.     } else {  
  7.         imageView.setImageResource(R.drawable.image_placeholder);  
  8.         BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
  9.         task.execute(resId);  
  10.     }  
  11. }  

BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。

[java] view plaincopy

  1. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {  
  2.     // 在后台加载图片。  
  3.     @Override  
  4.     protected Bitmap doInBackground(Integer... params) {  
  5.         final Bitmap bitmap = decodeSampledBitmapFromResource(  
  6.                 getResources(), params[0], 100100);  
  7.         addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);  
  8.         return bitmap;  
  9.     }  
  10. }  

掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了!不过仅仅是理论地介绍不知道大家能不能完全理解,在后面的文章中我会演示如何在实际程序中灵活运用上述技巧来避免程序OOM

Android照片墙的实现:

照片墙这种功能现在应该算是挺常见了,在很多应用中你都可以经常看到照片墙的身影。它的设计思路其实也非常简单,用一个GridView控件当作,然后随着GridView的滚动将一张张照片贴在上,这些照片可以是手机本地中存储的,也可以是从网上下载的。制作类似于这种的功能的应用,有一个非常重要的问题需要考虑,就是图片资源何时应该释放。因为随着GridView的滚动,加载的图片可能会越来越多,如果没有一种合理的机制对图片进行释放,那么当图片达到一定上限时,程序就必然会崩溃。

今天我们照片墙应用的实现,重点也是放在了如何防止由于图片过多导致程序崩溃上面。主要的核心算法使用了Android中提供的LruCache类,这个类是3.1版本中提供的,如果你是在更早的Android版本中开发,则需要导入android-support-v4jar包。

第一个要考虑的问题就是,我们从哪儿去收集这么多的图片呢?这里我从谷歌官方提供的Demo里将图片源取了出来,我们就从这些网址中下载图片,代码如下所示:

新建或打开activity_main.xml作为程序的主布局,加入如下代码:

[html] view plaincopy

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="wrap_content"  
  4.     android:layout_height="wrap_content" >  
  5.       
  6.     <GridView   
  7.         android:id="@+id/photo_wall"  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:columnWidth="90dip"  
  11.         android:stretchMode="columnWidth"  
  12.         android:numColumns="auto_fit"  
  13.         android:verticalSpacing="10dip"  
  14.         android:gravity="center"  
  15.         ></GridView>  
  16.       
  17. </LinearLayout>  

可以看到,我们在这个布局文件中仅加入了一个GridView,这也就是我们程序中的,所有的图片都将贴在这个上。

接着我们定义GridView中每一个子View的布局,新建一个photo_layout.xml布局,加入如下代码:

[html] view plaincopy

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="wrap_content"  
  4.     android:layout_height="wrap_content" >  
  5.   
  6.     <ImageView   
  7.         android:id="@+id/photo"  
  8.         android:layout_width="90dip"  
  9.         android:layout_height="90dip"  
  10.         android:src="@drawable/empty_photo"  
  11.         android:layout_centerInParent="true"  
  12.         />  
  13.   
  14. </RelativeLayout>  

在每一个子View中我们就简单使用了一个ImageView来显示一张图片。这样所有的布局就已经定义好了。
 

接下来新建PhotoWallAdapter做为GridView的适配器,代码如下所示:

[java] view plaincopy

  1. public class PhotoWallAdapter extends ArrayAdapter<String> implements OnScrollListener {  
  2.   
  3.     /** 
  4.      * 记录所有正在下载或等待下载的任务。 
  5.      */  
  6.     private Set<BitmapWorkerTask> taskCollection;  
  7.   
  8.     /** 
  9.      * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。 
  10.      */  
  11.     private LruCache<String, Bitmap> mMemoryCache;  
  12.   
  13.     /** 
  14.      * GridView的实例 
  15.      */  
  16.     private GridView mPhotoWall;  
  17.   
  18.     /** 
  19.      * 第一张可见图片的下标 
  20.      */  
  21.     private int mFirstVisibleItem;  
  22.   
  23.     /** 
  24.      * 一屏有多少张图片可见 
  25.      */  
  26.     private int mVisibleItemCount;  
  27.   
  28.     /** 
  29.      * 记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。 
  30.      */  
  31.     private boolean isFirstEnter = true;  
  32.   
  33.     public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects,  
  34.             GridView photoWall) {  
  35.         super(context, textViewResourceId, objects);  
  36.         mPhotoWall = photoWall;  
  37.         taskCollection = new HashSet<BitmapWorkerTask>();  
  38.         // 获取应用程序最大可用内存  
  39.         int maxMemory = (int) Runtime.getRuntime().maxMemory();  
  40.         int cacheSize = maxMemory / 8;  
  41.         // 设置图片缓存大小为程序最大可用内存的1/8  
  42.         mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
  43.             @Override  
  44.             protected int sizeOf(String key, Bitmap bitmap) {  
  45.                 return bitmap.getByteCount();  
  46.             }  
  47.         };  
  48.         mPhotoWall.setOnScrollListener(this);  
  49.     }  
  50.   
  51.     @Override  
  52.     public View getView(int position, View convertView, ViewGroup parent) {  
  53.         final String url = getItem(position);  
  54.         View view;  
  55.         if (convertView == null) {  
  56.             view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);  
  57.         } else {  
  58.             view = convertView;  
  59.         }  
  60.         final ImageView photo = (ImageView) view.findViewById(R.id.photo);  
  61.         // ImageView设置一个Tag,保证异步加载图片时不会乱序  
  62.         photo.setTag(url);  
  63.         setImageView(url, photo);  
  64.         return view;  
  65.     }  
  66.   
  67.     /** 
  68.      * ImageView设置图片。首先从LruCache中取出图片的缓存,设置到ImageView上。如果LruCache中没有该图片的缓存, 
  69.      * 就给ImageView设置一张默认图片。 
  70.      *  
  71.      * @param imageUrl 
  72.      *            图片的URL地址,用于作为LruCache的键。 
  73.      * @param imageView 
  74.      *            用于显示图片的控件。 
  75.      */  
  76.     private void setImageView(String imageUrl, ImageView imageView) {  
  77.         Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);  
  78.         if (bitmap != null) {  
  79.             imageView.setImageBitmap(bitmap);  
  80.         } else {  
  81.             imageView.setImageResource(R.drawable.empty_photo);  
  82.         }  
  83.     }  
  84.   
  85.     /** 
  86.      * 将一张图片存储到LruCache中。 
  87.      *  
  88.      * @param key 
  89.      *            LruCache的键,这里传入图片的URL地址。 
  90.      * @param bitmap 
  91.      *            LruCache的键,这里传入从网络上下载的Bitmap对象。 
  92.      */  
  93.     public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
  94.         if (getBitmapFromMemoryCache(key) == null) {  
  95.             mMemoryCache.put(key, bitmap);  
  96.         }  
  97.     }  
  98.   
  99.     /** 
  100.      * LruCache中获取一张图片,如果不存在就返回null 
  101.      *  
  102.      * @param key 
  103.      *            LruCache的键,这里传入图片的URL地址。 
  104.      * @return 对应传入键的Bitmap对象,或者null 
  105.      */  
  106.     public Bitmap getBitmapFromMemoryCache(String key) {  
  107.         return mMemoryCache.get(key);  
  108.     }  
  109.   
  110.     @Override  
  111.     public void onScrollStateChanged(AbsListView view, int scrollState) {  
  112.         // 仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务  
  113.         if (scrollState == SCROLL_STATE_IDLE) {  
  114.             loadBitmaps(mFirstVisibleItem, mVisibleItemCount);  
  115.         } else {  
  116.             cancelAllTasks();  
  117.         }  
  118.     }  
  119.   
  120.     @Override  
  121.     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,  
  122.             int totalItemCount) {  
  123.         mFirstVisibleItem = firstVisibleItem;  
  124.         mVisibleItemCount = visibleItemCount;  
  125.         // 下载的任务应该由onScrollStateChanged里调用,但首次进入程序时onScrollStateChanged并不会调用,  
  126.         // 因此在这里为首次进入程序开启下载任务。  
  127.         if (isFirstEnter && visibleItemCount > 0) {  
  128.             loadBitmaps(firstVisibleItem, visibleItemCount);  
  129.             isFirstEnter = false;  
  130.         }  
  131.     }  
  132.   
  133.     /** 
  134.      * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageViewBitmap对象, 
  135.      * 如果发现任何一个ImageViewBitmap对象不在缓存中,就会开启异步线程去下载图片。 
  136.      *  
  137.      * @param firstVisibleItem 
  138.      *            第一个可见的ImageView的下标 
  139.      * @param visibleItemCount 
  140.      *            屏幕中总共可见的元素数 
  141.      */  
  142.     private void loadBitmaps(int firstVisibleItem, int visibleItemCount) {  
  143.         try {  
  144.             for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {  
  145.                 String imageUrl = Images.imageThumbUrls[i];  
  146.                 Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);  
  147.                 if (bitmap == null) {  
  148.                     BitmapWorkerTask task = new BitmapWorkerTask();  
  149.                     taskCollection.add(task);  
  150.                     task.execute(imageUrl);  
  151.                 } else {  
  152.                     ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);  
  153.                     if (imageView != null && bitmap != null) {  
  154.                         imageView.setImageBitmap(bitmap);  
  155.                     }  
  156.                 }  
  157.             }  
  158.         } catch (Exception e) {  
  159.             e.printStackTrace();  
  160.         }  
  161.     }  
  162.   
  163.     /** 
  164.      * 取消所有正在下载或等待下载的任务。 
  165.      */  
  166.     public void cancelAllTasks() {  
  167.         if (taskCollection != null) {  
  168.             for (BitmapWorkerTask task : taskCollection) {  
  169.                 task.cancel(false);  
  170.             }  
  171.         }  
  172.     }  
  173.   
  174.     /** 
  175.      * 异步下载图片的任务。 
  176.      *  
  177.      * @author guolin 
  178.      */  
  179.     class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {  
  180.   
  181.         /** 
  182.          * 图片的URL地址 
  183.          */  
  184.         private String imageUrl;  
  185.   
  186.         @Override  
  187.         protected Bitmap doInBackground(String... params) {  
  188.             imageUrl = params[0];  
  189.             // 在后台开始下载图片  
  190.             Bitmap bitmap = downloadBitmap(params[0]);  
  191.             if (bitmap != null) {  
  192.                 // 图片下载完成后缓存到LrcCache  
  193.                 addBitmapToMemoryCache(params[0], bitmap);  
  194.             }  
  195.             return bitmap;  
  196.         }  
  197.   
  198.         @Override  
  199.         protected void onPostExecute(Bitmap bitmap) {  
  200.             super.onPostExecute(bitmap);  
  201.             // 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。  
  202.             ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);  
  203.             if (imageView != null && bitmap != null) {  
  204.                 imageView.setImageBitmap(bitmap);  
  205.             }  
  206.             taskCollection.remove(this);  
  207.         }  
  208.   
  209.         /** 
  210.          * 建立HTTP请求,并获取Bitmap对象。 
  211.          *  
  212.          * @param imageUrl 
  213.          *            图片的URL地址 
  214.          * @return 解析后的Bitmap对象 
  215.          */  
  216.         private Bitmap downloadBitmap(String imageUrl) {  
  217.             Bitmap bitmap = null;  
  218.             HttpURLConnection con = null;  
  219.             try {  
  220.                 URL url = new URL(imageUrl);  
  221.                 con = (HttpURLConnection) url.openConnection();  
  222.                 con.setConnectTimeout(5 * 1000);  
  223.                 con.setReadTimeout(10 * 1000);  
  224.                 con.setDoInput(true);  
  225.                 con.setDoOutput(true);  
  226.                 bitmap = BitmapFactory.decodeStream(con.getInputStream());  
  227.             } catch (Exception e) {  
  228.                 e.printStackTrace();  
  229.             } finally {  
  230.                 if (con != null) {  
  231.                     con.disconnect();  
  232.                 }  
  233.             }  
  234.             return bitmap;  
  235.         }  
  236.   
  237.     }  
  238.   
  239. }  

PhotoWallAdapter是整个照片墙程序中最关键的一个类了,这里我来重点给大家讲解一下。首先在PhotoWallAdapter的构造函数中,我们初始化了LruCache类,并设置了最大缓存容量为程序最大可用内存的1/8,接下来又为GridView注册了一个滚动监听器。然后在getView()方法中,我们为每个ImageView设置了一个唯一的Tag,这个Tag的作用是为了后面能够准确地找回这个ImageView,不然异步加载图片会出现乱序的情况。之后调用了setImageView()方法为ImageView设置一张图片,这个方法首先会从LruCache缓存中查找是否已经缓存了这张图片,如果成功找到则将缓存中的图片显示在ImageView上,否则就显示一张默认的空图片。
 

看了半天,那到底是在哪里下载图片的呢?这是在GridView的滚动监听器中进行的,在onScrollStateChanged()方法中,我们对GridView的滚动状态进行了判断,如果当前GridView是静止的,则调用loadBitmaps()方法去下载图片,如果GridView正在滚动,则取消掉所有下载任务,这样可以保证GridView滚动的流畅性。在loadBitmaps()方法中,我们为屏幕上所有可见的GridView子元素开启了一个线程去执行下载任务,下载成功后将图片存储到LruCache当中,然后通过Tag找到相应的ImageView控件,把下载好的图片显示出来。

由于我们使用了LruCache来缓存图片,所以不需要担心内存溢出的情况,当LruCache中存储图片的总大小达到容量上限的时候,会自动把最近最少使用的图片从缓存中移除。

最后新建或打开MainActivity作为程序的主Activity,代码如下所示:

[java] view plaincopy

  1. public class MainActivity extends Activity {  
  2.   
  3.     /** 
  4.      * 用于展示照片墙的GridView 
  5.      */  
  6.     private GridView mPhotoWall;  
  7.   
  8.     /** 
  9.      * GridView的适配器 
  10.      */  
  11.     private PhotoWallAdapter adapter;  
  12.   
  13.     @Override  
  14.     protected void onCreate(Bundle savedInstanceState) {  
  15.         super.onCreate(savedInstanceState);  
  16.         setContentView(R.layout.activity_main);  
  17.         mPhotoWall = (GridView) findViewById(R.id.photo_wall);  
  18.         adapter = new PhotoWallAdapter(this0, Images.imageThumbUrls, mPhotoWall);  
  19.         mPhotoWall.setAdapter(adapter);  
  20.     }  
  21.   
  22.     @Override  
  23.     protected void onDestroy() {  
  24.         super.onDestroy();  
  25.         // 退出程序时结束所有的下载任务  
  26.         adapter.cancelAllTasks();  
  27.     }  
  28.   
  29. }  

MainActivity中的代码非常简单,没什么需要说明的了,在Activity被销毁时取消掉了所有的下载任务,避免程序在后台耗费流量。另外由于我们使用了网络功能,别忘了在AndroidManifest.xml中加入网络权限的声明。

 

  • 能够对图片的优化进行相应的处理

1、利用“三级缓存”实现图片的优化

第一次是要从服务器获取的,以后每次显示图片,首先判断内存中获取,如果没有,再从本地缓存目录中获取,如果还没有,最后就需要发送请求从服务器获取。服务器端下载的图片是使用Http的缓存机制,每次执行将本地图片的时间发送给服务器,如果返回码是304,说明服务端的图片和本地的图片是相同的,直接使用本地保存的图片,如果返回码是200,则开始下载新的图片并实现缓存。在从服务器获取到图片后,需要再在本地和内存中分别存一份,这样下次直接就可以从内存中直接获取了,这样就加快了显示的速度,提高了用户的体验。

2、图片过大导致内存溢出:

模拟器的RAM比较小,由于每张图片先前是压缩的情况,放入到Bitmap的时候,大小会变大,导致超出RAM内存

★android 中用bitmap 时很容易内存溢出,报如下错误:Java.lang.OutOfMemoryError : bitmap size exceeds VM budget

解决:

方法1主要是加上这段:等比例缩小图片

BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;

1)通过getResource()方法获取资源:

       //解决加载图片 内存溢出的问题

        //Options 只保存图片尺寸大小,不保存图片到内存

       BitmapFactory.Options opts = new BitmapFactory.Options();

       //缩放的比例,缩放是很难按准备的比例进行缩放的,其值表明缩放的倍数,SDK中建议其值是2的指数值,值越大会导致图片不清晰

       opts.inSampleSize = 2;

       Bitmap bmp = null;

       bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);                             

       ... 

       //回收

       bmp.recycle();

2)通过Uri取图片资源

private ImageView preview;

BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一

Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);

preview.setImageBitmap(bitmap);

以上代码可以优化内存溢出,但它只是改变图片大小,并不能彻底解决内存溢出。

3)通过路径获取图片资源

private ImageView preview;

private String fileName= "/sdcard/DCIM/Camera/2010-05-14 16.01.44.jpg";

BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一

Bitmap b = BitmapFactory.decodeFile(fileName, options);

preview.setImageBitmap(b);

filePath.setText(fileName);

 

方法2:对图片采用软引用,及时地进行recyle()操作

SoftReference<Bitmap> bitmap;

bitmap = new SoftReference<Bitmap>(pBitmap);

if(bitmap != null){

if(bitmap.get() != null && !bitmap.get().isRecycled()){

bitmap.get().recycle();

bitmap = null;

}

}

1、为何使用软引用:

       由于创建的集合中存的都是强引用的对象,对于强引用的对象,垃圾回收器是绝对不会回收这个强引用的对象的,除非手动将对象置为null,垃圾回收器才会在适当的时候将其回收掉。当内存不足的时候,即使抛出了OOM异常,程序终止了也不会回收这个强引用对象。所以为了避免在低内存下缓存图片而导致OOM异常的出现,需要降低对象的引用级别,这就涉及到了软引用。

简单来说,软引用就相当于一个“袋子”,而强引用就相当于袋子中的内容,可以比喻为将手机(内容)存入到袋子中,即用软引用包裹强引用。软引用SoftReference的特点就在于它的实例保存了对一个java对象的软引用,该软引用的存在并不妨碍垃圾回收线程对该java对象的回收。SoftReference保存了对一个java对象的软引用后,在垃圾回收此java对象之前,SoftReference所提供的的get()方法返回的是java对象的强引用;一旦垃圾线程回收该java对象之后,get()返回的是null。

2、软引用的特点:

1)、在内存空间充足时,即使一个对象具有软引用,垃圾回收器也不会回收它。

2)、当内存不足时,会在出现OOM异常之前回收掉这些对象的内存空间。

3)、只要垃圾回收器没回收这个对象,则程序就可以继续使用这个对象,因此软引用可用来实现内存敏感的高速缓存。

4)、软引用和引用队列ReferenceQueue联合使用时,当软引用所引用的对象被垃圾回收器回收后,java虚拟机就会将这个软引用加入到与之关联的引用队列中。

需要说明的是,对于强引用对象(如new一个对象),如果被软引用所引用后,不为null时是作为强可及对象存在的,如果为null后,是作为软可及对象存在的;当垃圾回收的时候,不会因为这个对象被软引用所引用而保留该对象的。而是会在OOM异常之前优先回收掉长时间闲置的软可及对象,尽可能保留“新”的软可及对象。如果想重新获得对该实例的强引用,可以通过调用软引用的get()方法获得对象后继续使用。

3、引用队列ReferenceQueue

虽然SoftReference对象具有保存软引用的特性,但是也还是具有java对象的一般性的。也就是说当软引用所引用的对象被回收之后,这个软引用的对象实际上并没有什么存在的价值了,这就需要一个适当的清除机制,避免由于大量的SoftReference对象的存在而带来新的内存泄露的问题。这就需要将这些“空袋子”回收掉,这就需要使用引用队列ReferenceQueue这个类。

使用方法:就是将创建的强引用和引用队列作为参数传递到软引用的构造方法中,从而实现在SoftReference所引用的object在被垃圾回收器回收的同时,这个软引用对象会被加入到引用队列ReferenceQueue之中。我们可以通过ReferenceQueue的poll()方法监控到是否有非强可及对象被回收了。当队列为空时,说明没有软引用加入到队列中,即没有非强可及引用被回收,否则poll()方法会返回队列中前面一个Reference对象。通过这个方法,我们就可以将无引用对象的软引用SoftReference回收掉,这就避免了大量SoftReference未被回收导致的内存泄露。

4、构建高级缓存:

在应用之中,当我们将图片加载到内存中,需要考虑内存的优化问题,同样会涉及到OOM的问题。如果图片过多的话,内存就吃不消了,这就需要考虑应用如何在低内存的情况下运行了,需要为应用构建高级缓存,来保证在低内存的情况下也能正常运行。首先,在缓存图片的时候,需要判断手机的当前可用内存是否充足,如果内存不足,就需要使用缓存的Map来存储,保证在高速缓存下运行。但是在java中并没有提供软引用的Map集合,只提供了一个WeakHashMap这个针对弱引用提供的实现类。这时候就需要我们手动创建一个具体的实现类,作为软引用的集合来使用。

1、自定义软引用集合,继承HashMap<K, V>,这就相当于一个“袋子”

2、在构造函数中初始化:

       临时集合:创建一个临时的HashMap,其中的V应当作为软引用存在,即使用自定义的软引用的类[此类是加入队列的软引用]。可以理解为将手机(强引用)放入袋子(软引用)中,在将袋子贴上一个标签,放入队列中存储。

       引用队列:创建存放软引用的队列ReferenceQueue。

3、重写用到集合的方法:put、get、以及containsKey等用到的方法(即在实际使用中用到了哪些方法),在put方法中,需要先创建一个软引用对象,接收传入的value,即包裹强引用对象;将这个软引用加入到临时的集合中,这样就可以操作软引用的集合了。同样的get方法也是从这个临时存储软引用的集合中取值。

4、回收“空袋子”。当强引用对象被回收后,软引用也需要被回收掉:通过调用引用队列中的poll方法,不断的循环,检测队列是否为空,即检测队列中是否有“空袋子”软引用,如果有则从临时的集合中移除掉。在get和containKey方法中调用此回收方法。】

 

 

 

  • 掌握OOM异常的处理,并可以对应用进行相应的优化

一、内存溢出如何产生的

Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M,有的机器为24M。因此我们所能利用的内存空间是有限的。如果我们的内存占用超过了一定的水平就会出现OutOfMemory的错误。

内存溢出的几点原因总结:

1、资源释放问题:

程序代码的问题,长期保持某些资源(如Context)的引用,造成内存泄露,资源得不到释放

2、对象内存过大问题:

保存了多个耗用内存过大的对象(如Bitmap),造成内存超出限制

3、static:

static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。

public class ClassName { 

     private static Context mContext; 

     //省略 

以上的代码是很危险的,如果将Activity赋值到mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放。

我们举Android文档中的一个例子。

private static Drawable sBackground; 

@Override 

      protected void onCreate(Bundle state) { 

super.onCreate(state); 

TextView label = new TextView(this); 

label.setText("Leaks are bad"); 

if (sBackground == null) { 

sBackground = getDrawable(R.drawable.large_bitmap); 

label.setBackgroundDrawable(sBackground); 

setContentView(label); 

}

    sBackground, 是 一个静态的变量,但是我们发现,我们并没有显式的保存Contex的引用,但是,当Drawable与View连接之后,Drawable就将View设 置为一个回调,由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。这个引用链如下:

    Drawable->TextView->Context

    所以,最终该Context也没有得到释放,发生了内存泄露。

针对static的解决方案:

第一、应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。

    第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。

    第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef;

    该部分的详细内容也可以参考Android文档中Article部分。

 

4、线程导致内存溢出:

线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。

public class MyActivity extends Activity { 

    @Override 

    public void onCreate(Bundle savedInstanceState) { 

        super.onCreate(savedInstanceState); 

        setContentView(R.layout.main); 

        new MyThread().start(); 

    } 

    private class MyThread extends Thread{ 

@Override 

        public void run() { 

            super.run(); 

            //do somthing 

        } 

    } 

这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设MyThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一 般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。

    由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。

有些人喜欢用Android提供的AsyncTask,但事实上AsyncTask的问题更加严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。

针对这种线程导致的内存泄露问题的解决方案:

    第一、将线程的内部类,改为静态内部类。

    第二、在线程内部采用弱引用保存Context引用。

 

二、避免内存溢出的方案:

1、图片过大导致内存溢出:

模拟器的RAM比较小,由于每张图片先前是压缩的情况,放入到Bitmap的时候,大小会变大,导致超出RAM内存

★android 中用bitmap 时很容易内存溢出,报如下错误:Java.lang.OutOfMemoryError : bitmap size exceeds VM budget

解决:

方法1主要是加上这段:等比例缩小图片

BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;

1)通过getResource()方法获取资源:

       //解决加载图片 内存溢出的问题

        //Options 只保存图片尺寸大小,不保存图片到内存

       BitmapFactory.Options opts = new BitmapFactory.Options();

       //缩放的比例,缩放是很难按准备的比例进行缩放的,其值表明缩放的倍数,SDK中建议其值是2的指数值,值越大会导致图片不清晰

       opts.inSampleSize = 2;

       Bitmap bmp = null;

       bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);                             

       ... 

       //回收

       bmp.recycle();

2)通过Uri取图片资源

private ImageView preview;

BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一

Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);

preview.setImageBitmap(bitmap);

以上代码可以优化内存溢出,但它只是改变图片大小,并不能彻底解决内存溢出。

3)通过路径获取图片资源

private ImageView preview;

private String fileName= "/sdcard/DCIM/Camera/2010-05-14 16.01.44.jpg";

BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一

Bitmap b = BitmapFactory.decodeFile(fileName, options);

preview.setImageBitmap(b);

filePath.setText(fileName);

 

方法2:对图片采用软引用,及时地进行recyle()操作

SoftReference<Bitmap> bitmap;

bitmap = new SoftReference<Bitmap>(pBitmap);

if(bitmap != null){

if(bitmap.get() != null && !bitmap.get().isRecycled()){

bitmap.get().recycle();

bitmap = null;

}

}

具体见“各种引用的简单了解”中的示例

2、复用listView

方法:对复杂的listview进行合理设计与编码:

Adapter中:

@Override

public View getView(int position, View convertView, ViewGroup parent) {

    ViewHolder holder;

    if(convertView!=null && convertView instanceof LinearLayout){

           holder = (ViewHolder) convertView.getTag();

    }else{

           convertView = View.inflate(MainActivity.this, R.layout.item, null);

           holder = new ViewHolder();

           holder.tv = (TextView) convertView.findViewById(R.id.tv);

           convertView.setTag(holder);

    }

    holder.tv.setText("XXXX");

    holder.tv.setTextColor(Color.argb(180, position*4, position*5, 255-position*2));

    return convertView;

}

      

class ViewHolder{

       private TextView tv;

}

 

3、界面切换

方法1:单个页面,横竖屏切换N次后 OOM

1、看看页面布局当中有没有大的图片,比如背景图之类的。

去除xml中相关设置,改在程序中设置背景图(放在onCreate()方法中):

          Drawable bg = getResources().getDrawable(R.drawable.bg);

          XXX.setBackgroundDrawable(rlAdDetailone_bg);

          在Activity destory时注意,bg.setCallback(null); 防止Activity得不到及时的释放

 2. 跟上面方法相似,直接把xml配置文件加载成view 再放到一个容器里

然后直接调用 this.setContentView(View view);方法,避免xml的重复加载

 

方法2在页面切换时尽可能少地重复使用一些代码

比如:重复调用数据库,反复使用某些对象等等......

 

4、内存分配:

方法1:Android堆内存也可自己定义大小和优化Dalvik虚拟机的堆内存分配 

    注意若使用这种方法:project build target 只能选择 <= 2.2 版本,否则编译将通不过。 所以不建议用这种方式 

    private final static int CWJ_HEAP_SIZE= 6*1024*1024;

    private final static float TARGET_HEAP_UTILIZATION = 0.75f;

    VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);

    VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);

 

常见的内存使用不当的情况

1、查询数据库没有关闭游标

程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

Cursor cursor = null;

try {

  cursor = getContentResolver().query(uri ...);

  if (cursor != null && cursor.moveToNext()) {

  ... ... 

  }

} finally {

  if (cursor != null) {

      try { 

             cursor.close();

      } catch (Exception e) {

             //ignore this

      }

  }

 

2、构造Adapter时,没有使用缓存的 convertView

以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:

public View getView(int position, View convertView, ViewGroup parent)

来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的 view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。

由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list item的view对象的过程可以查看:

public View getView(int position, View convertView, ViewGroup parent) {

  View view = null;

  if (convertView != null) {

  view = convertView;

  populate(view, getItem(position));

  ...

  } else {

  view = new Xxx(...);

  ...

  }

  return view;

}

 

3、Bitmap对象不在使用时调用recycle()释放内存

有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的,视情况而定。可以看一下代码中的注释:

 

4、释放对象的引用

当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。

示例A:

public class DemoActivity extends Activity {

  ... ...

  private Handler mHandler = ...

  private Object obj;

  public void operation() {

  obj = initObj();

  ...

  [Mark]

  mHandler.post(new Runnable() {

       public void run() {

       useObj(obj);

       }

  });

  }

}

  我们有一个成员变量 obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是 mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。 所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:

... ...

public void operation() {

  obj = initObj();

  ...

  final Object o = obj;

  obj = null;

  mHandler.post(new Runnable() {

  public void run() {

  useObj(o);

  }

  }

}

... ...

 示例B:

  假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个 PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界 面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

  但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃 圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得 system_process进程挂掉。

 

5、其他

  Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、 onDestroy()方法中需要适当的释放资源的情况。由于此情况很基础,在此不详细说明,具体可以查看官方文档对Activity生命周期的介绍,以 明确何时应该释放哪些资源。

 

三、Android性能优化的一些方案

1、优化Dalvik虚拟机的堆内存分配

1)首先内存方面,可以参考 Android堆内存也可自己定义大小和优化Dalvik虚拟机的堆内存分配

对于Android平台来说,其托管层使用的Dalvik JavaVM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法:

private final static floatTARGET_HEAP_UTILIZATION = 0.75f;

在程序onCreate时就可以调用:

VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);

2)Android堆内存也可自己定义大小

对于一些大型Android项目或游戏来说在算法处理上没有问题外,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感。

除了上次Android开发网提到的优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:

private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;

VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //设置最小heap内存为6MB大小

当然对于内存吃紧来说还可以通过手动干涉GC去处理,我们将在下次提到具体应用。

2、基础类型上,因为Java没有实际的指针,在敏感运算方面还是要借助NDK来完成。

Android123提示游戏开发者,这点比较有意思的是Google 推出NDK可能是帮助游戏开发人员,比如OpenGL ES的支持有明显的改观,本地代码操作图形界面是很必要的。

3、图形对象优化:

这里要说的是Android上的Bitmap对象销毁,可以借助recycle()方法显示让GC回收一个Bitmap对象,通常对一个不用的Bitmap可以使用下面的方式,如

if(bitmapObject.isRecycled()==false) //如果没有回收  

              bitmapObject.recycle();   

4、处理GIF动画:

目前系统对动画支持比较弱智对于常规应用的补间过渡效果可以,但是对于游戏而言一般的美工可能习惯了GIF方式的统一处理

目前Android系统仅能预览GIF的第一帧,可以借助J2ME中通过线程和自己写解析器的方式来读取GIF89格式的资源。

5、对于大多数Android手机没有过多的物理按键可能我们需要想象下了做好手势识别 GestureDetector 和重力感应来实现操控。通常我们还要考虑误操作问题的降噪处理。

 

四、图片占用进程的内存算法简介

android中处理图片的基础类是Bitmap,顾名思义,就是位图。占用内存的算法如下:

图片的width*height*Config。

如果Config设置为ARGB_8888,那么上面的Config就是4。一张480*320的图片占用的内存就是480*320*4 byte。

在默认情况下android进程的内存占用量为16M,因为Bitmap除了java中持有数据外,底层C++的 skia图形库还会持有一个SKBitmap对象,因此一般图片占用内存推荐大小应该不超过8M。这个可以调整,编译源代码时可以设置参数。

 

五、内存监测工具 DDMS --> Heap

  无论怎么小心,想完全避免bad code是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。Android tools中的DDMS就带有一个很不错的内存监测工具Heap(这里我使用eclipse的ADT插件,并以真机为例,在模拟器中的情况类似)。

用 Heap监测应用进程使用内存情况的步骤如下:

1. 启动eclipse后,切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的;

2. 将手机通过USB链接至电脑,链接时需要确认手机是处于“USB调试”模式,而不是作为“Mass Storage”;

3. 链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;

4. 点击选中想要监测的进程,比如system_process进程;

5. 点击选中Devices视图界面中最上方一排图标中的“Update Heap”图标;

6. 点击Heap视图中的“Cause GC”按钮;

7. 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。

 说明:

a) 点击“Cause GC”按钮相当于向虚拟机请求了一次gc操作;

b) 当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;

c) 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。

  如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

a) 不断的操作当前应用,同时注意观察data object的Total Size值;

b) 正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对 象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;

c) 反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大,

  直到到达一个上限后导致进程被kill掉。

d) 此处已system_process进程为例,在我的测试环境中system_process进程所占用的内存的data object的Total Size正常情况下会稳定在2.2~2.8之间,而当其值超过3.55后进程就会被kill。

 

  总之,使用DDMS的Heap视图工具可以很方便的确认我们的程序是否存在内存泄漏的可能性。

 

  • 熟悉Android中的动画,选择器,样式和主题的使用

一、动画:

1、动画的分类:

1)、Tween动画:这种实现方式可以使视图组件移动、放大、缩小以及产生透明度的变化;

2)、Frame动画:传统的动画方法,通过顺序的播放排列好的图片来实现,类似电影。

1)Frame 帧动画 AnimationDrawable

【参考api文档实现示例:/sdk/docs/guide/topics/resources/animation-resource.html#Frame】

1、使用AnimationDrawable来操作:

在res目录下,新建drawable与anim目录:

drawable放入帧动画图片

anim目录下新建帧动画xml文件来表示帧动画;

布局文件:

     <ImageView       

        android:id="@+id/iv"

        android:onClick="start"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

帧动画文件rocket.xml

      <?xml version="1.0" encoding="utf-8"?>

       <!-- oneshot 是否只播放一次 -->

       <animation-list xmlns:android="http://schemas.android.com/apk/res/android"

              android:oneshot="false" >

      <item  android:drawable="@drawable/girl_1" android:duration="200"/>

      <item  android:drawable="@drawable/girl_2" android:duration="200"/>

      <item  android:drawable="@drawable/girl_3" android:duration="200"/>

      <item  android:drawable="@drawable/girl_4" android:duration="200"/>

      <item  android:drawable="@drawable/girl_5" android:duration="200"/>

     <item  android:drawable="@drawable/girl_6" android:duration="200"/>

     <item  android:drawable="@drawable/girl_7" android:duration="200"/>

      <item  android:drawable="@drawable/girl_8" android:duration="200"/>

      <item  android:drawable="@drawable/girl_9" android:duration="200"/>

      <item  android:drawable="@drawable/girl_10" android:duration="200"/>

      <item  android:drawable="@drawable/girl_11" android:duration="200"/>

       </animation-list>

代码:

 public class MainActivity extends Activity {

       private ImageView iv;

       private AnimationDrawable anim;       

       @Override

       protected void onCreate(Bundle savedInstanceState) {

              super.onCreate(savedInstanceState);

              setContentView(R.layout.activity_main);

              iv = (ImageView) findViewById(R.id.iv);

              iv.setBackgroundResource(R.anim.rocket);                 // 把动画设置为背景

              anim = (AnimationDrawable) iv.getBackground();      // 获取背景

       }

       public void start(View v) {

              if(anim.isRunning()) {

                     anim.stop();

              }

              anim.start();

       }

}

 

2)Tween动画:

①、有点类似以前弄的图片,处理,如旋转,缩放等,但Tween动画,注重的是动画过程,而不是结果;

②、创建方法:

使用xml文件来定义动画,然后通过AnimationUtils来加载,获取动画对象

使用代码方法,如:

       // 旋转动画(这里设置:围绕自己的中心点旋转)

       RotateAnimation ra = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);

       ra.setDuration(1500);                                        // 旋转一次时间

       ra.setRepeatCount(Animation.INFINITE);          // 重复次数无限

       iv_scan.startAnimation(ra);                               // 开启动画

 

分类:

1、透明动画(alpha.xml)

<set xmlns:android="http://schemas.android.com/apk/res/android"

    android:shareInterpolator="false" >

    <!-- 透明动画 -->

    <alpha

        android:repeatMode="reverse"             // 反转,播放动画

          android:repeatCount="infinite"              // 重复播放

        android:duration="1000"

        android:fromAlpha="1"

        android:toAlpha="0.2" />

</set>

 

2、缩放动画(scale.xml)

    <!-- 缩放动画 -->

 <set>  

      <scale

        android:duration="1000"

        android:fromXScale="1.0"                  // 起始x缩放级别,

        android:fromYScale="1.0"                  // 起始y缩放级别

          android:toXScale="2"                           // 目标x缩放级别, 这里设置为放大一倍

        android:toYScale="2"

        android:pivotX="0"                                   // 动画中心点设置;0 基于左上角;50%基于自身中央,50%p基于父容器中央, 大于0基于此像素

        android:pivotY="0"

        android:repeatCount="infinite"

        android:repeatMode="reverse"/>

</set>

 

3、位移动画(translate.xml)

    <!-- 位移动画 -->

    <translate

        android:duration="1000"

        android:fromXDelta="0"                            // 起始位移位置

        android:fromYDelta="0"

        android:repeatCount="infinite"

        android:repeatMode="reverse"

        android:toXDelta="100%"               // 移动到哪里,这里设置为,移动自身的右下角位置 100%

        android:toYDelta="100%" />

 

4、旋转动画(rotate.xml)

     <!-- 旋转动画 -->

    <rotate

        android:duration="1000"

        android:fromDegrees="0"            // 旋转角度范围设置

          android:toDegrees="360"

        android:pivotX="50%"                // 动画中心点设置

        android:pivotY="50%"

        android:repeatCount="infinite"

        android:repeatMode="restart"

       />

 

5、组合动画(all.xml)

      <!-- 组合动画:旋转 + 缩放 + 透明 -->

    <rotate

        android:duration="1000"

        android:fromDegrees="0"

        android:interpolator="@android:anim/linear_interpolator"         // 动画篡改器,设置匀速转动,不出现完成后,停顿

        android:pivotX="50%"

        android:pivotY="50%"

        android:repeatCount="infinite"

        android:repeatMode="restart"

        android:toDegrees="360" />

    <scale

        android:duration="1000"

        android:fromXScale="1.0"

        android:fromYScale="1.0"

        android:pivotX="50%"

        android:pivotY="50%"

        android:repeatCount="infinite"

        android:repeatMode="reverse"

        android:toXScale="2"

        android:toYScale="2" />

    <alpha

        android:duration="1000"

        android:fromAlpha="1"

        android:repeatCount="infinite"

        android:repeatMode="reverse"

        android:toAlpha="0.2" />

代码:

 public class MainActivity extends Activity {            

       private ImageView imageView;

       @Override

       protected void onCreate(Bundle savedInstanceState) {

              super.onCreate(savedInstanceState);

              setContentView(R.layout.activity_main);

              imageView = (ImageView) findViewById(R.id.imageView);

       }

 

       public void onClick(View v) {

              Animation anim = null;         // 动画对象

              switch (v.getId()) {

                     case R.id.alphaBT:                              // 透明动画

                            anim = AnimationUtils.loadAnimation(this, R.anim.alpha);              // 根据xml获取动画对象

                            break;

                     case R.id.rorateBT:                             // 旋转动画

                            anim = AnimationUtils.loadAnimation(this, R.anim.rotate);

                            break;

                     case R.id.scaleBT:                               // 缩放动画

                            anim = AnimationUtils.loadAnimation(this, R.anim.scale);

                            break;

                     case R.id.transalteBT:                          // 位移动画

                            anim = AnimationUtils.loadAnimation(this, R.anim.translate);

                            break;

                     case R.id.all:

                            anim = AnimationUtils.loadAnimation(this, R.anim.all);

                            break;

              }

              if (anim != null) {

                     imageView.startAnimation(anim);        // 启动动画

              }

       }

}

 

动画篡改器interpolator

       Interpolator 定义了动画的变化速度,可以实现匀速、正加速、负加速、无规则变加速等;有以下几类(更多参考API):

AccelerateDecelerateInterpolator,延迟减速,在动作执行到中间的时候才执行该特效。

AccelerateInterpolator, 会使慢慢以(float)的参数降低速度。

LinearInterpolator,平稳不变的,上面旋转动画中使用到了;

DecelerateInterpolator,在中间加速,两头慢

CycleInterpolator,曲线运动特效,要传递float型的参数。

 

API Demo View 中有对应的动画插入器示例,可供参考;

 

xml实现动画插入器:

1、动画定义文件 /res/anim/目录下shake.xml :

       <translate xmlns:android="http://schemas.android.com/apk/res/android"

                  android:duration="500"

                  android:fromXDelta="0"

                  android:interpolator="@anim/cycle_3"

                  android:toXDelta="10" />

2、interpolator 指定动画按照哪一种方式进行变化, cycle_3文件如下:

       <cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android" android:cycles="3" />

       表示循环播放动画3次;

3、使用动画的,程序代码:

      Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake);

       et_phone.startAnimation(shake);

二、样式与主题

1、样式

1)、定义样式

设置样式,在values文件夹下的任意文件中的<resources>中配置<style>标签

<style name="itheima1">

    <item name="android:layout_width">match_parent</item>

    <item name="android:layout_height">wrap_content</item>

    <item name="android:textColor">#ff0000</item>

    <item name="android:textSize">30sp</item>

</style>

2)、继承样式,在<style>标签中配置属性parent

<style name="itheima2" parent="itheima1">

    <item name="android:gravity">center</item>

    <item name="android:textColor">#00ff00</item>

</style>

 

<style name="itheima3" parent="itheima2">

    <item name="android:gravity">right</item>

    <item name="android:textColor">#0000ff</item>

</style>

3)、使用样式

在layout文件的标签中配置style属性

<TextView

    style="@style/itheima1"

    android:text="一段文本" />

2、主题

styles.xml中也可以为Activity定义属性

<style name="AppTheme" parent="AppBaseTheme">

    <item name="android:windowNoTitle">true</item>

    <item name="android:windowFullscreen">true</item>

</style>

在AndroidManifest.xml文件中<activity>或者<application>节点上可以使用theme属性引用

<activity

    android:name="com.itheima.style.MainActivity"

    android:theme="@style/AppTheme" />

 

三、选择器:

一)、创建xml文件:

在drawable/xxx.xml下常见xml文件,在同目录下记得要放相关图片

<?xml version="1.0" encoding="utf-8" ?>   

<selector xmlns:android="http://schemas.android.com/apk/res/android"> 

<!-- 默认时的背景图片-->  

  <item android:drawable="@drawable/pic1" />    

<!-- 没有焦点时的背景图片 -->  

  <item android:state_window_focused="false"   

        android:drawable="@drawable/pic1" />   

<!-- 非触摸模式下获得焦点并单击时的背景图片 -->  

  <item android:state_focused="true" android:state_pressed="true"   android:drawable= "@drawable/pic2" /> 

<!-- 触摸模式下单击时的背景图片-->  

<item android:state_focused="false" android:state_pressed="true"   android:drawable="@drawable/pic3" />  

<!--选中时的图片背景-->  

  <item android:state_selected="true"   android:drawable="@drawable/pic4" />   

<!--获得焦点时的图片背景-->  

  <item android:state_focused="true"   android:drawable="@drawable/pic5" />   

</selector>

二)使用xml文件:

1、使用方法:

1)、方法一:

(1)在listview中配置android:listSelector="@drawable/xxx

(2)在listview的item中添加属性android:background="@drawable/xxx"

2)、方法二:

Drawable drawable = getResources().getDrawable(R.drawable.xxx);  

       ListView.setSelector(drawable);

但是这样会出现列表有时候为黑的情况,需要加上:

android:cacheColorHint="@android:color/transparent"使其透明。

 

2、相关属性:

android:state_selected :是选中

android:state_focused :是获得焦点

android:state_pressed :是点击

android:state_enabled :是设置是否响应事件,指所有事件

根据这些状态同样可以设置button的selector效果。也可以设置selector改变button中的文字状态。

 

3、Button文字效果

1)以下是配置button中的文字效果:

drawable/button_font.xml

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_selected="true" android:color="#FFF" />

    <item android:state_focused="true" android:color="#FFF" />

    <item android:state_pressed="true" android:color="#FFF" />

    <item android:color="#000" />

</selector>

2)Button还可以实现更复杂的效果,例如渐变

drawable/button_color.xml

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">        / 

<item android:state_pressed="true">//定义当button 处于pressed 状态时的形态。 

<shape>

<gradient  android:startColor="#8600ff" /> 

<stroke   android:width="2dp" android:color="#000000" /> 

<corners android:radius="5dp" />  

<padding android:left="10dp" android:top="10dp" 

android:bottom="10dp" android:right="10dp"/>  

</shape> 

</item> 

<item android:state_focused="true">//定义当button获得 focus时的形态 

<shape> 

<gradient android:startColor="#eac100"/> 

<stroke android:width="2dp" android:color="#333333"  color="#ffffff"/> 

<corners android:radius="8dp" />   

<padding android:left="10dp" android:top="10dp" 

android:bottom="10dp" android:right="10dp"/>

</shape> 

</item>

</selector> 

 

3)最后,需要在包含 button的xml文件里添加两项。

例如main.xml 文件,需要在<Button />里加两项

android:focusable="true"

android:background="@drawable/button_color"

三)语法示例:

1、文件位置:

res/color/filename.xml,文件名被做资源的ID

2、语法示例

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_selected="true" android:color="@color/white" />

    <item android:state_focused="true" android:color="@color/white" />

    <item android:state_pressed="true" android:color="@color/white" />

    <item android:state_enabled="true" android:color="@color/black"/>

    <item android:state_enabled="false" android:color="@color/white"/>

    <item android:state_window_focused="false" android:color="@color/black"/>

    <item android:color="@color/black" />

</selector>

 

3、属性

android:color十六进制颜色,必须的。颜色是用RGB值来指定的,并且可选择alpha通道。

    这个值始终是用#字符开头,后面跟的是Appha-Red-Green-Blue信息,格式如下:

        #RGB

        #ARGB

        #RRGGBB

        #AARRGGBB

android:state_pressed一个布尔值

如果这个项目是在对象被按下时使用,那么就要设置为true。(如,按钮被触摸或点击时。)false应该用于默认的非按下状态。

android:state_focused一个布尔值

如果这个项目是在对象获取焦点时使用,那么就要设置为true。如,一个选项标签被打开时。

如果这个项目要用于对象没有被被选择的时候,那么就要设置为false。

android:state_checkable一个布尔值

如果这个项目要用于对象的可选择状态,那么就要设置为true。

如果这个项目要用于不可选状态,那么就要设置为false。(它只用于一个对象在可选和不可选之间的转换)。

android:state_checked一个布尔值

如果这个项目要用于对象被勾选的时候,那么就要设置为true。否者设为false。

android:state_enabled一个布尔值

如果这个项目要用于对象可用状态(接受触摸或点击事件的能力),那么就要设置为true,否者设置为false。

android:state_window_focused一个布尔值

如果这个项目要用于应用程序窗口的有焦点状态(应用程序是在前台),那么就要设置为true,否者设置false。

4、注意

A:要记住,状态列表中一个与对象当前状态匹配的项目会被使用。因此,如果列表中的第一项没有包含以上任何一种状态属性,那么每次都会使用这个项目,因此默认设置应该始终被放到最后。

B:如果出现失去焦点,背景色延迟的情况,不要使用magin。

C:drawable下的selector可是设置状态背景列表(可以让view的背景在不同状态时变化)说明:也可以定义状态背景列表,但是是定义在drawable文件夹下,用的不是color属性,而是drawable属性。

四)

、自定义选择器

(shape和选择器如何同时使用。例如:如何让一个按钮即是圆角的,又能在点击的时候出现颜色变化。)

1、定义xml文件,Root Element选择shape

①创建view被按下的布局文件:

进行相应的属性配置,如:

<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"

    android:shape="rectangle" >

 <!-- 此处表示是一个矩形 -->

    <corners android:radius="3dp" />

 <!-- 此处表示是一个圆角 -->

    <solid android:color="#33000000" />

</shape>

 

②创建view正常显示的布局(新建一个xml同),配置如下:

<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"

    android:shape="rectangle" >

 <!-- 此处表示是一个矩形 -->

    <corners android:radius="3dp" />

 <!-- 此处表示是一个圆角 -->

    <solid android:color="#00000000" />

</shape>

 

2、创建背景选择器:(Root Element为selector)

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/bg_pressed" android:state_pressed="true"/>

    <!-- pressed -->

    <item android:drawable="@drawable/bg_pressed" android:state_focused="true"/>

    <!-- focused -->

    <item android:drawable="@drawable/bg_normal"/>

    <!-- 默认 -->

</selector>

      

3、将上面定义好的布局文件设定到选择器中(红字)

在需要使用背景资源的布局文件中选择上面创建的背景选择器(selector)

设置布局的clickable为true,并设置点击事件

 

此时在界面中点击控件时就会变化颜色

 

 

  • 熟悉android系统下消息推送机制

一、推送方式简介:

当前随着移动互联网的不断加速,消息推送的功能越来越普遍,不仅仅是应用在邮件推送上了,更多的体现在手机的APP上。当我们开发需要和服务器交互的应用程序时,基本上都需要获取服务器端的数据,比如《地震应急通》就需要及时获取服务器上最新的地震信息。

1、概念:

所谓的消息推送就是从服务器端向移动终端发送连接,传输一定的信息。比如一些新闻客户端,每隔一段时间收到一条或者多条通知,这就是从服务器端传来的推送消息;还比如常用的一些IM软件如微信、GTalk等,都具有服务器推送功能。

推送技术通过自动传送信息给用户,来减少用于网络上搜索的时间。它根据用户的兴趣来搜索、过滤信息,并将其定期推给用户,帮助用户高效率地发掘有价值的信息。

2、要获取服务器上不定时更新的信息,一般来说有两种方法:

第一种是客户端使用Pull(拉)的方式,就是隔一段时间就去服务器上获取一下信息,看是否有更新的信息出现。

第二种就是 服务器使用Push(推送)的方式,当服务器端有新信息了,则把最新的信息Push到客户端上。这样,客户端就能自动的接收到消息。

虽然Pull和Push两种方式都能实现获取服务器端更新信息的功能,但是明显来说Push方式比Pull方式更优越。因为Pull方式更费客户端的网络流量,更主要的是费电量,还需要我们的程序不停地去监测服务端的变化。

 

二、常见消息推送方案的原理:

1、轮询(Pull)方式:

客户端定时向服务器发送询问消息,一旦服务器有变化则立即同步消息。应用程序应当阶段性的与服务器进行连接并查询是否有新的消息到达,你必须自己实现与服务器之间的通信,例如消息排队等。而且你还要考虑轮询的频率,如果太慢可能导致某些消息的延迟,如果太快,则会大量消耗网络带宽和电池。

2、SMS(Push)方式:

通过拦截SMS消息并且解析消息内容来了解服务器的命令。这个方案的好处是,可以实现完全的实时操作;但是问题是这个方案的成本相对比较高,且依赖于运营商。

3、持久连接(Push)方式:

客户端和服务器之间建立长久连接,这样就可以实现消息的及时行和实时性。

这个方案可以解决由轮询带来的性能问题,但是还是会消耗手机的电池。我们需要开一个服务来保持和服务器端的持久连接(苹果就和谷歌的C2DM是这种机制)。但是对于Android系统,当系统可用资源较低,系统会强制关闭我们的服务或者是应用,这种情况下连接会强制中断。(Apple的推送服务之所以工作的很好,是因为每一台手机仅仅保持一个与服务器之间的连接,事实上C2DM也是这么工作的。即所有的推送服务都是经由一个代理服务器完成的,这种情况下只需要和一台服务器保持持久连接即可。C2DM=Cloud to Device Messaging)。

  相比之下第三种还是最可行的。为软件编写系统服务或开机启动功能;或者如果系统资源较低,服务被关闭后可以在onDestroy ()方法里面再重启该服务,进而实现持久连接的方式。

 

三、消息推送解决方案概述

1C2DM云端推送方案

在Android手机平台上,Google提供了C2DM(Cloudto Device Messaging)服务。Android Cloud to Device Messaging (C2DM)是一个用来帮助开发者从服务器向Android应用程序发送数据的服务。该服务提供了一个简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据。C2DM服务负责处理诸如消息排队等事务并向运行于目标设备上的应用程序分发这些消息。

C2DM操作过程示例图:

 

 

这个服务存在很大的问题:  

1)C2DM内置于Android的2.2系统上,无法兼容老的1.6到2.1系统。

2)C2DM需要依赖于Google官方提供的C2DM服务器,由于国内的网络环境,这个服务经常不可用,如果想要很好的使用,我们的App Server必须也在国外,这个恐怕不是每个开发者都能够实现的。

3) 不像在iPhone中,他们把硬件系统集成在一块了。所以对于我们开发者来说,如果要在我们的应用程序中使用C2DM的推送功能,因为对于不同的这种硬件厂商平台,比如摩托罗拉、华为、中兴做一个手机,他们可能会把Google的这种服务去掉,尤其像在国内就很多这种,把Google这种原生的服务去掉。买了一些像什么山寨机或者是华为这种国产机,可能Google的服务就没有了。而像在国外出的那些可能会内置。

即然C2DM无法满足我们的要求,那么我们就需要自己来实现Android手机客户端与App Server之间的通信协议,保证在App Server想向指定的Android设备发送消息时,Android设备能够及时的收到。

 

2MQTT协议实现Android推送

  采用MQTT协议实现Android推送功能也是一种解决方案。MQTT是一个轻量级的消息发布/订阅协议,它是实现基于手机客户端的消息推送服务器的理想解决方案。

  wmqtt.jar 是IBM提供的MQTT协议的实现。我们可以从如下站点下载http://www-01.ibm.com/support/docview.wss?rs=171&uid=swg24006006)它。可以将该jar包加入自己的Android应用程序中。可以从这里(https://github.com/tokudu/AndroidPushNotificationsDemo)下载该项目的实例代码,并且可以找到一个采用PHP书写的服务器端实现(https://github.com/tokudu/PhpMQTTClient)。

架构如下图所示:

 

 

3RSMB实现推送功能

  Really Small Message Broker (RSMB) ,是一个简单的MQTT代理,同样由IBM提供,其查看地址是:http://www.alphaworks.ibm.com/tech/rsmb。缺省打开1883端口,应用程序当中,它负责接收来自服务器的消息并将其转发给指定的移动设备。SAM是一个针对MQTT写的PHP库。我们可以从这个http://pecl.php.net/package/sam/download/0.2.0地址下载它。send_mqtt.php是一个通过POST接收消息并且通过SAM将消息发送给RSMB的PHP脚本。

 

4XMPP协议实现Android推送

1)XMPP

XMPP全称Extensible Messaging and Presence Protocol,前身是Jabber项目,是一种以XML为基础的开放式即时通讯协议。XMPP因为被Google Talk和网易泡泡应用而被广大网民所接触。XMPP的关键特色是,分散式的即时通讯系统,以及使用XML串流。XMPP目前被IETF国际标准组织完成了标准化工作。

2Android push notification(androidpn)

AndroidPN 是一个基于XMPP协议的java开源实现,它包含了完整的客户端和服务器端。该服务器端基本是在另外一个开源工程openfire基础上修改实现的。

androidpn实现意图如下图所示:

 

androidpn客户端需要用到一个基于java的开源XMPP协议包asmack,这个包同样也是基于openfire下的另外一个开源项目smack,不过我们不需要自己编译,可以直接把androidpn客户端里面的asmack.jar拿来使用。客户端利用asmack中提供的XMPPConnection类与服务器建立持久连接,并通过该连接进行用户注册和登录认证,同样也是通过这条连接,接收服务器发送的通知。

3)androidpn服务器端:

androidpn服务器端也是java语言实现的,基于openfire开源工程,不过它的Web部分采用的是spring框架,这一点与openfire是不同的。Androidpn服务器包含两个部分,一个是侦听在5222端口上的XMPP服务,负责与客户端的XMPPConnection类进行通信,作用是用户注册和身份认证,并发送推送通知消息。另外一部分是Web服务器,采用一个轻量级的HTTP服务器,负责接收用户的Web请求。服务器的这两方式,意义非凡:当相应的TCP端口被防火墙封闭,可以使用轮询的方式进行访问,因此又有助于通过防火墙。

服务器架构如下:

 

最上层包含四个组成部分,分别是SessionManager,Auth Manager,PresenceManager以及Notification Manager。SessionManager负责管理客户端与服务器之间的会话,Auth Manager负责客户端用户认证管理,Presence Manager负责管理客户端用户的登录状态,NotificationManager负责实现服务器向客户端推送消息功能。

这个解决方案的最大优势就是简单,我们不需要象C2DM那样依赖操作系统版本,也不会担心某一天Google服务器不可用。利用XMPP协议我们还可以进一步的对协议进行扩展,实现更为完善的功能。采用这个方案,我们目前只能发送文字消息,不过对于推送来说一般足够了,因为我们不能指望通过推送得到所有的数据,一般情况下,利用推送只是告诉手机端服务器发生了某些改变,当客户端收到通知以后,应该主动到服务器获取最新的数据,这样才是推送服务的完整实现。 XMPP协议书相对来说还是比较简单的,值得我们进一步研究。

4)androidpn不足:

androidpn是一个基于XMPP协议的java开源Android push notification实现。它包含了完整的客户端和服务器端。但也存在一些不足之处:

①、比如时间过长时,就再也收不到推送的信息了。

②、性能上也不够稳定。

③、如果将消息从服务器上推送出去,就不再管理了,不管消息是否成功到达客户端手机上。

如果我们要使用androidpn,则还需要做大量的工作,需要理解XMPP协议、理解Androidpn的实现机制,需要调试内部存在的BUG。

 

5、使用第三方平台

  目前国内、国外有一些推送平台可供使用,但是涉及到收费问题、保密问题、服务质量问题、扩展问题等等,又不得不是我们望而却步。

 

6、自己搭建一个推送平台。

  这不是一件轻松的工作,当然可以根据各自的需要采取合适的方案。

 

========================================================================================================================

四、Android Push Notification实现信息推送使用

 AndroidPn项目就是使用XMPP协议实现信息推送的一个开源项目。在这里介绍其使用过程。

 1、Apndroid Push Notification的特点: 

1)快速集成:提供一种比C2DM更加快捷的使用方式,避免各种限制.  

2)无需架设服务器:通过使用"云服务",减少额外服务器负担.

3)可以同时推送消息到网站页面,android 手机

4)耗电少,占用流量少.

2、具体配置过程: 

1)下载并解压androidpn的压缩包

①、首先, 我们需要下载androidpn-client-0.5.0.zip和androidpn-server-0.5.0-bin.zip。

  下载地址:http://sourceforge.net/projects/androidpn/ 

 ②、解压两个包,Eclipse导入client,配置好目标平台,打开raw/androidpn.properties文件,配置客户端程序。

2)配置:

A、如果是模拟器来运行客户端程序,把xmppHost配置成10.0.2.2[模拟器把10.0.2.2认为是所在主机的地址,127.0.0.1是模拟器本身的回环地址,10.0.2.1表示网关地址,10.0.2.3表示DNS地址,10.0.2.15表示目标设备的网络地址],关于模拟器的详细信息,大家可参阅相关资料。

  xmppPort=5222 是服务器的xmpp服务监听端口

   运行androidpn-server-0.5.0\bin\run.bat启动服务器,从浏览器访问http://127.0.0.1:7070/index.do (androidPN Server有个轻量级的web服务器,在7070端口监听请求,接受用户输入的文本消息)

  运行客户端,客户端会向服务器发起连接请求,注册成功后,服务器能识别客户端,并维护和客户端的IP长连接。

 B、如果是在同一个局域网内的其他机器的模拟器测试(或者使用同一无线路由器wifi上网的真机) ,则需要把这个值设置为服务器机器的局域网ip

  例如:你的电脑和android手机都通过同一个无线路由器wifi上网,电脑的ip地址为 192.168.1.2 而手机的ip地址为192.168.1.3,这个时候需要把这个值修改为xmppHost=192.168.1.1 或是电脑的IP地址,就可以在手机上使用了. 

 C、如果是不在同一个局域网的真机测试,我们需要将这个值设置为服务器的IP地址。 

 

具体配置如下图所示:

 

     

 3)示例:

  我的电脑IP是:192.168.8.107 

A、服务器运行主界面:

    

B、推送信息如下界面所示: 

    

C、测试结果如下图所示: 

    

 

 

  • 熟悉掌握常见的设计模式:单例模式、工厂模式、策略模式、代理模式、装饰模式、模板模式

 

  • 熟悉UML设计,可以设计程序的用例图、类图、活动图等

 

 

  • 对Cocos2d游戏引擎有一定的了解和实践,并接触过处理3D图形和模型库的OpenGL

在进行游戏界面的绘制工作中,需要处理大量的工作,这些工作有很多共性的操作;并且对于游戏界面的切换,元素动作的处理,都已经有人做好了这些工作,并将其封装到框架中,其中Cocos2d-android就是这样一个框架。

Cocos2d实现游戏的绘制:

1、实现步骤:

首先来说,要想绘制出游戏界面,按照谷歌文档中的说明,需要实现两步操作:

①、所有的SurfaceView和SurfaceHolder.Callback,被UI Thread调用

也就是说需要接收用户的操作

②、确保所绘制的进程是有效的:

就要调用SurfaceHolder.Callback中的创建方法creat被调用和销毁方法destroy被调用

2、具体的实现:

1)、Cocos2d中有CCGLSurfaceView这个类,是继承于SurfaceView的,并实现了SurfaceHolder.Callback的接口。创建出这个对象,就有了绘制游戏界面的容器。

2)、绘制容器中的画面和元素,还要接受用户的操作;就需要将绘制的操作放在一个子线程中执行,UI Thread这个线程接收用户的操作;通过GLThread这个类实现不断的绘制界面的操作。

GLThread绘制线程的实现:

①、复写了run方法,在run方法中调用了GLThread自己的run方法:guardedRun

       此方法中,通过while(true)不停的绘制,其中有相应的标记进行控制

       绘制的方法:mRenderer.onDrawFrame(gl);【绘制一帧】

              【void org.cocos2d.opengl.GLSurfaceView.Renderer.onDrawFrame(GL10 gl)】

②、Canvas和GL10这个接口如何进行处理绘制的:

       在Canvas中,Bitmap和GL是互斥的,一个为null,另一个必须不为null

       Cocos2d底层用到的是OpenGL的信息,所以方法中传递的是gl的接口

③、GLThread的开启:

       @、在GLSurfaceView中的setRenderer方法中开启的:

mGLThread = new GLThread(renderer);

        mGLThread.start();

       @、在CCDirector(继承了GLSurfaceView.Renderer)的initOpenGLViewWithView方法中调用了setRenderer

       @、的调用是由attachInView(View view)方法返回的

       最终是由导演CCDirector进行调用,这是导演的第一个工作,

       attachInView(View view)的作用是将导演和SurfaceView进行绑定,绑定时,将绘制线程开启起来

3)由此,大致过程如下:

①、创建出CCGLSurfaceView(即对应的SurfaceView),设置显示setContentView(surfaceView)

②、紧随其后,创建出导演CCDirector【通过单例获取:director=CCDirector.sharedDirector();】

③、通过调用导演中的attachInView(surfaceView),传入surfaceView:

       这样就建立了CCDirector和SurfaceView之间的关系

       并且还开启了绘制线程,进行绘制:

              attachInView(View view)方法调用了initOpenGLViewWithView方法【都是导演中的方法】

              initOpenGLViewWithView方法调用了setRenderer【开启绘制线程用的】

              在setRenderer中创建了绘制线程,并开启起来

mGLThread = new GLThread(renderer);

               mGLThread.start();

3、界面元素的展示:

上面的操作只是创建出界面,可以不断绘制界面中的内容,要想丰富界面,就需要添加元素到界面中。

Cocos2的架构:

①、Cocos2D Graphic图形引擎②、CocosDenshion Audio声音引擎③、物理引擎④、Lua脚本库

其中对于图形引擎,在Cocos2d中,绘制游戏就相当于在拍电影
由导演类CCDirector控制这个游戏元素的展现和消失;其中还包括场景类CCScene和精灵类CCSprite

说明:

1)CCDirector(导演):

引擎的控制者,控制场景的切换,游戏引擎属性的设置 【管理整棵大树】

2)CCScene (场景):场景类

例如游戏的闪屏,主菜单,游戏主界面等。 【类似于树根,树干】

3)CCLayer(布景):图层类

每个图层都有自己的触发事件,该事件只能对其拥有的元素有效,而图层之上的元素所包含的元素,是不受其事件管理的【类似于树枝】

4)CCSprite(人物):精灵类,

界面上显示的最小单元【类似于树叶】

5)CCNode:

引擎中最重要的元素,所有可以被绘制的东西都是派生于此。它可以包含其它CCNode,可以执行定时器操作,可以执行CCAction。

                     CCScene,CCLayer,CCSprite的父类

6)CCAction(动作):动作类

如平移、缩放、旋转等动作

示例代码:

public class MainActivity extends Activity {

       private CCDirector director;

       @Override

       protected void onCreate(Bundle savedInstanceState) {

              super.onCreate(savedInstanceState);

              //创建surfaceView

              CCGLSurfaceView surfaceView = new CCGLSurfaceView(this);

              setContentView(surfaceView);

              //创建导演

              director = CCDirector.sharedDirector();

              /*    设置相关参数

               */

              //横屏显示

              director.setDeviceOrientation(CCDirector.kCCDeviceOrientationLandscapeLeft);

              //设置屏幕大小

              director.setScreenSize(480, 320);

              //显示帧率

              director.setDisplayFPS(true);

             

              //①建立CCDirector和SurfaceView之间的关系;开启绘制线程

              director.attachInView(surfaceView);

              /*

               * 管理显示内容

               */

              //创建场景

              CCScene scene = CCScene.node();

//            FirstLayer layer = new FirstLayer();

//            ActionLayer layer = new ActionLayer();

              DemoLayer layer = new DemoLayer();

              //添加场景中的layer

              scene.addChild(layer);

              director.runWithScene(scene);

       }

      

       @Override

       protected void onResume() {

              director.onResume();

              super.onResume();

       }

      

       @Override

       protected void onPause() {

              director.onPause();

              super.onPause();

       }

      

       @Override

       protected void onDestroy() {

              director.end();

              super.onDestroy();

       }

}

public class FirstLayer extends CCLayer {

      

       private static final String TAG = "FristLayer";

       private int count;

       public FirstLayer(){

              // 一个场景里面只能有一个layer可以处理用户的Touch

              this.setIsTouchEnabled(true);

              count = 0;

              init();

             

       }

 

       /*

        * 初始化

        * 展示精灵并实现动画

        */

       private void init() {

              //创建精灵

              CCSprite sprite = CCSprite.sprite("z_1_01.png");

              this.addChild(sprite);

              sprite.setAnchorPoint(0, 0);

             

              CCSprite spritex = CCSprite.sprite("z_1_01.png");

              spritex.setFlipX(true);

              spritex.setAnchorPoint(0, 0);

              spritex.setPosition(100, 0);

              this.addChild(spritex, 0, 10);

             

              CCSprite spritey = CCSprite.sprite("z_1_01.png");

              spritey.setFlipY(true);

              spritey.setAnchorPoint(0, 0);

              spritey.setPosition(0, 100);

              this.addChild(spritey);

             

       }

      

       @Override

       public boolean ccTouchesBegan(MotionEvent event) {

              // 坐标转换:将MotionEvent封装的手机屏幕坐标系的坐标信息转换成Cocos2D的坐标系

              CGPoint touchPos= this.convertTouchToNodeSpace(event);

              CCSprite sprite = (CCSprite) this.getChildByTag(10);

              boolean containsPoint = CGRect.containsPoint(sprite.getBoundingBox(), touchPos);

              if(containsPoint){

//                   sprite.setOpacity(new Random().nextInt(255));

//                   count++;

//                   sprite.setVertexZ(1.0f+count);//最大132

//                   Log.i(TAG, "count=="+count);

                     //移除精灵

//                   sprite.removeSelf();

                     //隐藏精灵

                     sprite.setVisible(false);

                     /*

                      * Tips:此处不能使用sprite.removeSelf();

                      * 否则在第二次点击的时候,就会挂掉,因为再次点击的时候,精灵已经从layer中移除出去了

                      */

              }

             

              return super.ccTouchesBegan(event);

       }

}

 

  • 有一定的屏幕适配经验

手机自适应主要分为两种情况:

横屏和竖屏的切换,以及分辨率大小不同。

一、横竖屏切换:

1、Android应用程序支持横竖屏幕的切换,android中每次屏幕的切换动会重启Activity,所以应该在Activity销毁(执行onPause()方法和onDestroy()方法)前保存当前活动的状态;在Activity再次创建的时候载入配置,那样,进行中的游戏就不会自动重启了!有的程序适合从竖屏切换到横屏,或者反过来,这个时候怎么办呢?可以在配置Activity的地方进行如下的配置android:screenOrientation="portrait"(landscape是横向,portrait是纵向)。这样就可以保证是竖屏总是竖屏了。

2、而有的程序是适合横竖屏切换的。如何处理呢?首先要在配置Activity的时候进行如下的配置:

android:configChanges="keyboardHidden|orientation",

另外需要重写Activity的onConfigurationChanged方法。

实现方式如下:

@Override

publicvoidonConfigurationChanged(ConfigurationnewConfig){

super.onConfigurationChanged(newConfig);

if(this.getResources().getConfiguration().orientation==Configuration.ORIENTATION_LANDSCAPE){

//landdonothingisok

}elseif(this.getResources().getConfiguration().orientation==Configuration.ORIENTATION_PORTRAIT){

//portdonothingisok

}

}

 

二、分辨率问题:

对于分辨率问题,官方给的解决办法是创建不同的layout文件夹,这就需要对每种分辨率的手机都要写一个布局文件,虽然看似解决了分辨率的问题,但是如果其中一处或多处有修改了,就要每个布局文件都要做出修改,这样就造成很大的麻烦。那么可以通过以下几种方式解决:

一)使用layout_weight

目前最为推荐的Android多屏幕自适应解决方案。

    该属性的作用是决定控件在其父布局中的显示权重,一般用于线性布局中。其值越小,则对应的layout_width或layout_height的优先级就越高(一般到100作用就不太明显了);一般横向布局中,决定的是layout_width的优先级;纵向布局中,决定的是layout_height的优先级。

    传统的layout_weight使用方法是将当前控件的layout_width和layout_height都设置成fill_parent,这样就可以把控件的显示比例完全交给layout_weight;这样使用的话,就出现了layout_weight越小,显示比例越大的情况(即权重越大,显示所占的效果越小)。不过对于2个控件还好,如果控件过多,且显示比例也不相同的时候,控制起来就比较麻烦了,毕竟反比不是那么好确定的。于是就有了现在最为流行的0px设值法。看似让人难以理解的layout_height=0px的写法,结合layout_weight,却可以使控件成正比例显示,轻松解决了当前Android开发最为头疼的碎片化问题之一。

先看下面的styles(style_layout.xml)

<?xml version="1.0" encoding="utf-8"?>

<resources> 

 

<!-- 全屏幕拉伸-->

  <style name="layout_full"> 

    <item name="android:layout_width">fill_parent</item> 

    <item name="android:layout_height">fill_parent</item> 

  </style>

  

<!-- 固定自身大小-->

  <style name="layout_wrap"> 

    <item name="android:layout_width">wrap_content</item> 

    <item name="android:layout_height">wrap_content</item> 

  </style>

 

<!-- 横向分布-->

  <style name="layout_horizontal" parent="layout_full"> 

    <item name="android:layout_width">0px</item> 

  </style>

   

<!-- 纵向分布-->

  <style name="layout_vertical" parent="layout_full"> 

    <item name="android:layout_height">0px</item> 

  </style>

        

</resources>

可以看到,layout_width和layout_height两个属性被我封装成了4个style, 根据实际布局情况,选用当中的一种,不需要自己设置

二)清单文件配置:【不建议使用这种方式,需要对不同的界面写不同的布局】

需要在AndroidManifest.xml文件的<manifest>元素如下添加子元素

<supports-screensandroid:largeScreens="true"

android:normalScreens="true"

android:anyDensity="true"

android:smallScreens="true"

android:xlargeScreens="true">

</supports-screens>

以上是为我们的屏幕设置多分辨率支持(更准确的说是适配大、中、小三种密度)。

Android:anyDensity="true",这一句对整个的屏幕都起着十分重要的作用,值为true,我们的应用程序当安装在不同密度的手机上时,程序会分别加载hdpi,mdpi,ldpi文件夹中的资源。相反,如果值设置为false,即使我们在hdpi,mdpi,ldpi,xdpi文件夹下拥有同一种资源,那么应用也不会自动地去相应文件夹下寻找资源。而是会在大密度和小密度手机上加载中密度mdpi文件中的资源。

有时候会根据需要在代码中动态地设置某个值,可以在代码中为这几种密度分别设置偏移量,但是这种方法最好不要使用,最好的方式是在xml文件中不同密度的手机进行分别设置。这里地图的偏移量可以在values-xpdi,values-hpdi,values-mdpi,values-ldpi四种文件夹中的dimens.xml文件进行设置。

 

三)、其他:

说明:

       在不同分辨率的手机模拟器下,控件显示的位置会稍有不同

       通过在layout中定义的布局设置的参数,使用dp(dip),会根据不同的屏幕分辨率进行适配

       但是在代码中的各个参数值,都是使用的像素(px)为单位的

技巧:

1、尽量使用线性布局,相对布局,如果屏幕放不下了,可以使用ScrollView(可以上下拖动)

ScrowView使用的注意:

在不同的屏幕上显示内容不同的情况,其实这个问题我们往往是用滚动视图来解决的,也就是ScrowView;需要注意的是ScrowView中使用layout_weight是无效的,既然使用ScrowView了,就把它里面的控件的大小都设成固定的吧。

2、指定宽高的时候,采用dip的单位,dp单位动态匹配

3、由于android代码中写的单位都是像素,所有需要通过工具类进行转化

4、尽量使用9-patch图,可以自动的依据图片上面显示的内容被拉伸和收缩。其中在编辑的时候,灰色区域是被拉伸的,上下两个点控制水平方向的拉伸,左右两点控制垂直方向的拉伸

       工具在adt-bundle-windows-x86-20130522\sdk\tools目录下的draw9patch.bat

 

 

 

三、手机分辨率px和dp的关系:

dp:是dip的简写,指密度无关的像素。

       指一个抽象意义上的像素,程序用它来定义界面元素。一个与密度无关的,在逻辑尺寸上,与一个位于像素密度为160DPI的屏幕上的像素是一致的。要把密度无关像素转换为屏幕像素,可以用这样一个简单的公式:pixels=dips*(density/160)。举个例子,在DPI为240的屏幕上,1个DIP等于1.5个物理像素。

强烈推荐你用DIP来定义你程序的界面布局,因为这样可以保证你的UI在各种分辨率的屏幕上都可以正常显示。

/**

     * 根据手机的分辨率从 px(像素) 的单位 转成为 dp

     */ 

    public static int px2dip(Context context, float pxValue) { 

        final float scale = context.getResources().getDisplayMetrics().density; 

        return (int) (pxValue / scale + 0.5f); 

    } 

 

/**

     * 根据手机的分辨率从 dip 的单位 转成为 px(像素)

     */ 

    public static int dip2px(Context context, float dpValue) { 

        final float scale = context.getResources().getDisplayMetrics().density; 

        return (int) (dpValue * scale + 0.5f); 

    } 

 

  • 对OAuth2认证有一定的了解

转到分享界面后,进行OAuth2认证:

以新浪为例:

第一步、WebView加载界面,传递参数

使用WebView加载登陆网页,通过Get方法传递三个参数:应用的appkey、回调地址和展示方式display(如手机设备为mobile);

如:https://auth.sina.com.cn/oauth2/authorize?client_id=1750636396&redirect_uri=http://vdisk.weibo.com/&display=mobile

第二步、回调地址获取code

       当点击登陆(或授权)的时候,会将自定义的回调地址发送到相应的服务器端,这个回调地址只是为了从相应的服务器端(如新浪)获取到一个code;可以在WebViewClient的shouldOverrideUrlLoading方法中捕获到,然后获取到这个跳转的URL后,截取其中的code,如:http://vdisk.weibo.com/?code=3ea97ac6d5c1016a70d1c16e98b6f9ca

第三步、获取token

       通过这个code到相应的服务器获取到token【当然不仅仅是获取到token这个认证令牌,还有令牌有效期、uid,如果有权限的话,有的还会返回刷新令牌的token】,这些数据需要加密后保存在本地。

然后下次再登陆的时候,就可以直接登陆,然后通过发送给服务器端token等数据,获取到相应的数据

  

 

  • 对Android底层有一定的认识,研究过相关的Android源码

我将从以下几方面简单说明:

一、系统架构:

一)、系统分层:(由下向上)【如图】

1、安卓系统分为四层,分别是Linux内核层、Libraries层、FrameWork层,以及Applications层;

其中Linux内核层包含了Linux内核和各种驱动;

Libraries层包含各种类库(动态库(也叫共享库)、android运行时库、Dalvik虚拟机),编程语言主要为CC++

FrameWork层大部分使用java语言编写,是android平台上Java世界的基石

Applications层是应用层,我们在这一层进行开发,使用java语音编写

2Dalvik VM和传统JVM的区别:

传统的JVM:编写.java文件 à 编译为.class文件 à 打包成.jar文件

Dalvik VM  编写.java文件 à 编译为.class文件 à 打包成.dex文件 à 打包成.apk文件(通过dx工具)

       将所有的类整合到一个文件中,提高了效率。更适合在手机上运行

 

1、Linux内核层[LINUX KERNEL]:

包含Linux内核和驱动模块(比如USB、Camera、蓝牙等)。

Android2.2(代号Froyo)基于Linux内核2.6版本。

2、Libraries层[LIBRARIES]:

这一层提供动态库(也叫共享库)、android运行时库、Dalvik虚拟机等。

编程语言主要为C或C++,所以可以简单的看成Native层。

3、FrameWork层[APPLICATION FRAMEWORK]:

这一层大部分用java语言编写,它是android平台上Java世界的基石。

4、Applications层[APPLICATION]:应用层

 

如图所示:

系统分层的图整体简化为下面的一张图,对应如下:

FrameWork层       --------à        Java世界

Libraries层           --------à        Native世界

Linux内核层         --------à        Linux OS

 

Java世界和Native世界间的通信是通过JNI层

JNI层和Native世界都可以直接调用系统底层

 

二)、系统编译:

1、主要步骤:系统环境的准备,下载源码、编译源码、输出结果:

目前系统的编译环境只支持Ubuntu 以及 Mac OS 两种操作系统,磁盘的控件要足够大

在下载源码的时候,由于Android源码使用Git进行管理,需要下载一些工具,如apt-get install git-core curl

源码下载好后,进行编译:首先搭建环境,部署JDK(不同的源码编译时需要的JDK版本不同,如2.2需要JDK5,2.3需要1.6),

                                          然后设置编译环境:使用. build/envsetup.sh脚本;选择编译目标(可以根据自己需要的版本进行不同的搭配)

                                          最后通过make –j4的命令进行编译。(make是编译的函数即命令,j4指的是cpu处理器的核数:单核的是j4 x i;双核的是j8)

最后将编译好的结果进行输出:所有的编译产物都位于 /out 目录下

2、编译流程图

 

 

 

二、系统的启动:

通过Linux内核将Linux系统中用户空间的第一个进程init启动起来,这是安卓世界第一个被启动的进程;

然后在init中会加载init.rc的配置文件,并开启系统的守护进程(守护media(多媒体的装载)和孵化器zygoteJava世界的开启)),其实此时调试桥的守护进程也被开启起来了;

然后会处理一些动作执行,在app_main.cpp中会将Zygote孵化器(Zygote是整个java世界的基础,整个安卓世界中(包括frameworkappapk)都是由孵化器启动的)启动起来:

app_main中,会调用AppRuntimestart方法开启AppRuntime,其实开启的是其父类AndroidRuntimestart方法被调用,zygote由此就被调用了,此时Native层的右上角有一块区域即ANDROID RUNTIME就启动起来了;

与此同时,AppRuntime会调用ZygoteInitmain方法启动ZygoteInit(整个的APPLICATION FRAMEWORK都会由ZygoteInit带起来的,JNI也被启动起来):

ZygoteInit中会调用SystemServer这个类,在SystemServermain方法中启动init1()方法,将system_init.cpp开启起来,在init1()方法中,将整个Native世界(即LIBRARIES层)开启起来了

然后在system_init.cpp会调用SystemServerinit2()方法开启ServerThread,通过ServerThreadframework层开启起来(所有的就全部开启起来了),即java世界(APPLICATION FRAMEWORK)就被启动了;此时ActivityManagerWindowManagerPackageManager(最主要,所有的清单文件及apk都有它管理)等等framework层全部开启起来

 

一)安卓系统的总体启动顺序:

1、通过LINUX内核,将init进程启动起来(是Linux系统中用户空间的第一个进程)

2、将ANDROID RUNTIME这一块的内容启动完毕

3、分为两步分别启动LIBRARIES(即Native世界)和APPLICATION FRAMEWORK(即java世界)

1)先启动LIBRARIES(即Native世界)

2)后启动APPLICATION FRAMEWORK(即java世界)【ActivityManager,WindowManager,电源管理等等】

二)具体启动流程

一)、启动流程:

1、init进程:——安卓世界第一个被启动的进程

       加载一堆配置文件,核心加载的init.rc配置文件,其中包含了孵化器和守护进程都被开启了

1)、启动服务:开启ServerManager

守护进程启动(Daemon Process):/system/bin/servicemanager

守护的是:

①、Java世界的开启:onrestart  restart  zygote

②、多媒体的装载:onrestart  restart  media

@、adbd的守护也被开启起来了,即调试桥的守护进程也被开启起来了

2)、启动孵化器Zygote

       在app_main中启动孵化器Zygote,整个安卓世界中(包括framework和app等apk)都是由孵化器启动的

       【此时虚拟机还没开启起来,只是配置了一些vm的参数】

3、app_main:——开启孵化器

       app_main中,调用AppRuntime的start方法,将Native层的右上角有一块区域,即ANDROID RUNTIME启动起来

其中的start方法实际是其父类AndroidRuntime的start方法

【此时VM虚拟机被开启起来了,通过start方法开启,在AndroidRuntime中并设置了默认的内存大小16M】

【注册JNI,并启动孵化器Zygote】

4、ZygoteInit开启

       AppRuntime被启动后,会调用ZygoteInit的main方法,启动ZygoteInit;

       然后,整个的APPLICATION 和FRAMEWORK都会由ZygoteInit带起来的

5、SystemServer启动:

       ZygoteInit调用SystemServer这个类,在SystemServer的main方法中启动init1()方法,将system_init.cpp开启起来

       在init1()方法中,将整个Native世界开启起来了

6、ServerThread启动(开启framework层)

调用SystemServer的init2()方法开启ServerThread,通过ServerThread将framework层开启起来(所有的就全部开启起来了)

       此时ActivityManager,WindowManager,PackageManager(最主要,所有的清单文件及apk都有它管理)等等framework层全部开启起来

 

二)、具体介绍:

1、启动入口:init进程

@、源码位置:/system/core/init/init.c

@、进程入口:main方法

1)创建文件夹,挂载设备【通过mkdir的命令创建,挂载一些系统设备后】

2)重定向输入输出,如错误信息输出【设置了一些输入输出的处理】

3)设置日志输出【一些系统的日志】

4)init.rc系统启动的配置文件【加载了相关的信息,不同版本的手机所特有的配置信息】

①、文件位置:/system/core/rootdir

②、守护进程启动(Daemon Process):/system/bin/servicemanager

守护的是

Java世界的开启:onrestart  restart  zygote

       多媒体的装载:onrestart  restart  media

       adbd守护也被开启起来了,即调试桥(adb[Android Debug Bridge])的守护进程(adbd[Android Debug Bridge Daemon])也被开启起来了

③、启动Zygote——app_main.cpp【Zygote是整个java世界的基础】

              当编译之后,在system/bin/app_process下会有孵化器的启动Xzygote

              守护进程被开启之后,紧接着Zygote也被启动起来了

5)解析和当前设备相关的配置信息(/init.%s.rc)

Tips:

当解析完init.rc和设备配置信息后会获取到一系列Action

Init将动作的执行划分为四个阶段(优先级由大到小):

early-init        :初期

Init                :初始化阶段

early-boot       :系统启动的初期

boot               :系统启动

6)处理动作执行:这个阶段Zygote将被启动

7)无限循环阶段,等待一些事情发生

 

2、Zygote简介:

@、Zygote启动:app_main.cpp

1)Zygote简介:

①、本身为Native的应用程序

②、由init进程通过init.rc加载

2)功能分析:

①、Main方法中AppRuntime.start(),工作由父类AndroidRuntime来完成

②、在AndroidRuntime中开启了如下内容

@startVM——开启虚拟机(查看堆内存设置):默认16M【】

@注册JNI函数【此时还在Native层,需要将连接java和c的桥(即JNI)搭建好】

@启动“com.android.internal.os.ZygoteInit”的main方法

【系统级别的包(由runtime的start方法开启的这个包)】

start方法实际是其父类AndroidRuntime的

@进入java世界的入口

 

3、ServiceThread的简介:(java世界所做的事情)

1)preloadClasses();:预加载class

       读取一个preloaded-classes的配置文件

       此文件的内容非常多,这就是安卓系统启动慢的原因之一

       此时会有一个垃圾回收的操作gc(),将无用的回收掉

2)ZygoteInit在main方法中利用JNI开启com.android.server.SystemServer

3)启动system_init.cpp处理Native层的服务

4)然后调用SystemServer的init2()

5)启动ServiceThread,启动android服务

6)Launcher启动

 

三、开机时的时间消耗:

1、ZygoteInit.main()中会预加载类

目录:framework/base/preload-class

ZygoteInit.main()会加载很多的类,将近1800多个(安卓2.3的)

2、开机时会对系统所有的apk进行扫描

       需要将所有的应用展现给用户,就需要对apk进行扫描,扫描所有的包

       data目录下有个apk的包

       system目录下有个apk的包

       framework目录下也有相关的包

3、SystemServer创建的那些Service

 

 

 

四、安卓工程的启动过程

1、Eclipse将.java源文件编译成.class

2、使用dx工具将所有.class文件转换为.dex文件

3、再将.dex文件和所有资源打包成.apk文件

4、将.apk文件安装到虚拟机完成程序安装

5、启动程序 – 开启进程 – 开启主线程

6、创建Activity对象 – 执行OnCreate()方法

7、按照main.xml文件初始化界面

 

 

=================================

应用程序启动:

一、解析清单文件并加载

应用程序的启动需要从PackageManagerService说起,由于应用程序是有PackageManager管理的,可以简单认为PackageManagerService是为应用程序启动的做了一些准备工作,才能将应用程序开启起来。

1、PackageManagerService(资料)读取所有应用程序的Mainfest信息,并且建立信息库存储在系统级共享内存中

1)解析:

PackageManagerService在启动后,会进行解析的工作,它会重点监控一些文件:system/framework、system/app、data/app、data/app_private;一旦将数据存入到这些文件中,就会去解析

2)权限分配:

PackageManagerService会建立底层userids和groupids同上层permissions之间的映射,就会给一些底层用户分配权限,

进行权限的映射,UID和GroupID,都会分配相应的权限

3)保存数据:

PackageManagerService还有重要的一个操作就是将解析的每个apk的信息保存到packages.xml和packages.list文件里,

在packages.list记录了如下数据:pkgName,userId,debugFlag,dataPath(包的数据路径)

【下次再开机的时候,不会再扫描每个apk了,只需要读取packages.xml和packages.list文件即可】

除了这两个主要的工作外,还会进行一些其他的操作,如检测文件等

 

2、Launcher就将PackageManagerService已经解析并处理好的数据都加载到内存中,从内存中就能获取到相应的数据,

并展示到手机上【之所以可以展示在手机桌面上,就是因为在清单文件中配置了如下的内容:】

              <action android:name="android.intent.action.MAIN" />:应用程序的入口

<category android:name="android.intent.category.LAUNCHER" />:配置了这个属性就可以显示在列表中

点击图标,应用就被开启起来了:

二、Activity的启动与生命周期的监控

应用程序被开启后,是需要开启并创建Activity,加载相应的view,从而展示出应用程序

1、Activity是通过startActivity开启起来的,startActivity是由有Context调用的,其具体的实现类是ContextImpl

在ContextImpl中的startActivity方法中,会调用ActivityThread的相关方法【mMainThread.getInstrumentation().execStartActivity()】;可以追溯到Instrumentation这个类,其中的execStartActivity()的方法中实现了startActivity的调用:ActivityManagerNative.getDefault().startActivity,由此可以看出是底层进行处理。

2、ActivityMonitor监控Activity

当Activity实例创建的时候,就会给Activity配置一个监视器ActivityMonitor,监控Activity的声明周期:

在Instrumentation的execStartActivity()的方法中,上来先判断ActivityMonitor是否为null:在第一次开启Activity的时候,ActivityMonitor还是null的,就会调用ActivityManagerNative.getDefault().startActivity(…..),是在操作native底层的信息,从而执行startActivity,再去开启一个Activity。

简单来说,就是通过调用JNI,调用startActivity方法,开启Activity;创建好了之后,随即也创建好了Activity的监视器ActivityMonitor

3、在ActivityMonitor中就有Activity各种生命周期的监控

①、在newActivity方法中:

可以通过拿到Activity的字节码,创建一个Activity,并将这个Activity返回

       还会调用attach方法,传入ActivityThread的线程

②、在各种生命周期的方法中,调用activity的各自的生命周期的方法

 

总结:

       1、通过PackageManagerService将所有用到的资源加载进内存中

       2、在Launcher中,将view等控件加载到ViewGroup中,点击每个item会有相应的操作

       3、在公开的文档中是找不到具体调用startActivity的类的,而是由系统完成调用的,实现了Activity的启动

实际就是通过Context的实现类ContextImpl进行调用的,一步步转到底层(ActivityManagerNative)实现调用

       4、另一个重要的类就是ActivityMonitor,监控Activity生命周期的;在其newActivity方法中创建了Activity,并调用了attach方法;

也就是说当一个Activity被创建的时候,就会绑定一个ActivityMonitor,用来监控Activity的生命周期

 

三、应用程序启动的时序图:

 

 

 

  • 对Activity、Window和View三者间的关系有一定的见解

一、简述如何将Activity展现在手机上

Tips:

Activity本身是没办法处理显示什么控件(view)的,是通过PhoneWindow进行显示的

换句话说:activity就是在造PhoneWindow,显示的那些view都交给了PhoneWindow处理显示

1、在Activity创建时调用attach方法:

2、attach方法中会调用PolicyManager.makeNewWindow()

实际工作的是IPolicy接口的makeNewWindow方法

①、其中创建了一个window(可以比喻为一个房子上造了一个窗户):mWindow = PolicyManager.makeNewWindow(this);

②、在window这个类中,才调用了setContentView(),这是最终的调用

       在Activity的setContentView方法中,实际上是调用:getWindow().setContentView(view, params);

       这里的getWindow()就是获取到一个Window对象

Tips:

       为啥attch优先于onCreate调用,就是由于在attch方法中,会创建window,有了window才能调用setContentView

3、在IPolicy的实现类中创建了PhoneWindow:

①、由mWindow = PolicyManager.makeNewWindow(this);,

②、这里的makeNewWindow(this);方法中,返回的是:return sPolicy.makeNewWindow(context);

③、这个sPolicy实际是一个接口,其实现类是Policy,其中只是创建了一个PhoneWindow

4、在PhoneWindow的setContentView中向ViewGroup(root)中添加了需要显示的内容

①、PhoneWindow是继承Window的

②、setContentView这个方法中,需要先判断一个mContentParent是否为空,因为在默认进来的时候,什么都没创建呢

       此时需要创建:installDecor(),DecorView是最根上的显示的

       可以通过adt中的的tools中有个hierarchyviewer.bat的工具,可以查看手机的结构

③、DecorView:是继承与FrameLayout的,作为parent存在,最初显示的

④、下次再加载的时候,mContentParent就不为空了,会将其中的所有的view移除掉,然后在通过布局填充器加载布局

 

 

二、三者关系:

1、在Activity中调用attach,创建了一个Window

2、创建的window是其子类PhoneWindow,在attach中创建PhoneWindow

3、在Activity中调用setContentView(R.layout.xxx)

4、其中实际上是调用的getWindow().setContentView()

5、调用PhoneWindow中的setContentView方法

6、创建ParentView:

       作为ViewGroup的子类,实际是创建的DecorView(作为FramLayout的子类)

7、将指定的R.layout.xxx进行填充

通过布局填充器进行填充【其中的parent指的就是DecorView】

8、调用到ViewGroup

9、调用ViewGroup的removeAllView(),先将所有的view移除掉

10、添加新的view:addView()

 

Tips:

       ①、Activity就是在造“窗户”,即创建PhoneWindow

       ②、PhoneWindow才是进行显示view的操作,主要就是setContentView()

 

 

 

  • 有良好的编码能力和代码规范,可以快速阅读英文技术文档

 

  • 备用:熟悉HTTP,TCP/IP等常见网络协议,熟悉XML,JSON解析

 

  • 4
    点赞
  • 1
    评论
  • 13
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值