BAT、网易、蘑菇街面试题整理-8

Android

1. ActivityFragment的生命周期。

http://blog.csdn.net/zjclugger/article/details/10442335

一、Activity 生命周期


二、Fragment 生命周期


三、对比图



四、测试代码

[java]  view plain  copy
  1. package com.goso.testapp;  
  2.   
  3. import android.app.Activity;  
  4. import android.app.ListFragment;  
  5. import android.os.Bundle;  
  6. import android.util.Log;  
  7. import android.view.LayoutInflater;  
  8. import android.view.View;  
  9. import android.view.ViewGroup;  
  10. import android.widget.ArrayAdapter;  
  11. import android.widget.ListView;  
  12.   
  13. /** 
  14.  * Demonstration of using ListFragment to show a list of items 
  15.  * from a canned array. 
  16.  */  
  17. public class FragmentListArray extends Activity {  
  18.   
  19.     @Override  
  20.     protected void onCreate(Bundle savedInstanceState) {  
  21.         super.onCreate(savedInstanceState);  
  22.         Log.e("HJJ""Activity &&&& onCreate...");  
  23.         // Create the list fragment and add it as our sole content.  
  24.         if (getFragmentManager().findFragmentById(android.R.id.content) == null) {  
  25.             ArrayListFragment list = new ArrayListFragment();  
  26.             getFragmentManager().beginTransaction().add(android.R.id.content, list).commit();  
  27.         }  
  28.     }  
  29.   
  30.     @Override  
  31.     protected void onStart() {  
  32.         // TODO Auto-generated method stub  
  33.         super.onStart();  
  34.         Log.e("HJJ""Activity &&&& onStart...");  
  35.     }  
  36.       
  37.     @Override  
  38.     protected void onResume() {  
  39.         // TODO Auto-generated method stub  
  40.         super.onResume();  
  41.         Log.e("HJJ""Activity &&&& onResume...");  
  42.     }  
  43.       
  44.     @Override  
  45.     protected void onStop() {  
  46.         // TODO Auto-generated method stub  
  47.         super.onStop();  
  48.         Log.e("HJJ""Activity &&&& onStop...");  
  49.     }  
  50.       
  51.     @Override  
  52.     protected void onPause() {  
  53.         // TODO Auto-generated method stub  
  54.         super.onPause();  
  55.         Log.e("HJJ""Activity &&&& onPause...");  
  56.     }  
  57.       
  58.     @Override  
  59.     protected void onDestroy() {  
  60.         // TODO Auto-generated method stub  
  61.         super.onDestroy();  
  62.         Log.e("HJJ""Activity &&&& onDestroy...");  
  63.     }  
  64.       
  65.     public static class ArrayListFragment extends ListFragment {  
  66.   
  67.         @Override  
  68.         public void onAttach(Activity activity) {  
  69.             // TODO Auto-generated method stub  
  70.             Log.e("HJJ""ArrayListFragment **** onAttach...");  
  71.             super.onAttach(activity);  
  72.         }  
  73.           
  74.         @Override  
  75.         public void onCreate(Bundle savedInstanceState) {  
  76.             // TODO Auto-generated method stub  
  77.             Log.e("HJJ""ArrayListFragment **** onCreate...");  
  78.             super.onCreate(savedInstanceState);  
  79.         }  
  80.           
  81.         @Override  
  82.         public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  83.                 Bundle savedInstanceState) {  
  84.             // TODO Auto-generated method stub  
  85.             Log.e("HJJ""ArrayListFragment **** onCreateView...");  
  86.             return super.onCreateView(inflater, container, savedInstanceState);  
  87.         }  
  88.           
  89.         @Override  
  90.         public void onActivityCreated(Bundle savedInstanceState) {  
  91.             super.onActivityCreated(savedInstanceState);  
  92.             Log.e("HJJ""ArrayListFragment **** onActivityCreated...");  
  93.             String[] array = new String[]{"C++""JAVA""PYTHON"};  
  94.             setListAdapter(new ArrayAdapter<String>(getActivity(),  
  95.                     android.R.layout.simple_list_item_1, array));  
  96.         }  
  97.   
  98.         @Override  
  99.         public void onStart() {  
  100.             // TODO Auto-generated method stub  
  101.             Log.e("HJJ""ArrayListFragment **** onStart...");  
  102.             super.onStart();  
  103.         }  
  104.           
  105.         @Override  
  106.         public void onResume() {  
  107.             Log.e("HJJ""ArrayListFragment **** onResume...");  
  108.             // TODO Auto-generated method stub  
  109.             super.onResume();  
  110.         }  
  111.           
  112.         @Override  
  113.         public void onPause() {  
  114.             Log.e("HJJ""ArrayListFragment **** onPause...");  
  115.             // TODO Auto-generated method stub  
  116.             super.onPause();  
  117.         }  
  118.           
  119.         @Override  
  120.         public void onStop() {  
  121.             Log.e("HJJ""ArrayListFragment **** onStop...");  
  122.             // TODO Auto-generated method stub  
  123.             super.onStop();  
  124.         }  
  125.           
  126.         @Override  
  127.         public void onDestroyView() {  
  128.             Log.e("HJJ""ArrayListFragment **** onDestroyView...");  
  129.             // TODO Auto-generated method stub  
  130.             super.onDestroyView();  
  131.         }  
  132.           
  133.         @Override  
  134.         public void onDestroy() {  
  135.             // TODO Auto-generated method stub  
  136.             Log.e("HJJ""ArrayListFragment **** onDestroy...");  
  137.             super.onDestroy();  
  138.         }  
  139.           
  140.         @Override  
  141.         public void onDetach() {  
  142.             Log.e("HJJ""ArrayListFragment **** onDetach...");  
  143.             // TODO Auto-generated method stub  
  144.             super.onDetach();  
  145.         }  
  146.           
  147.         @Override  
  148.         public void onListItemClick(ListView l, View v, int position, long id) {  
  149.             Log.i("FragmentList""Item clicked: " + id);  
  150.         }  
  151.     }  
  152. }  

五、测试结果

[java]  view plain  copy
  1. onCreate过程  
  2. 01-22 15:30:28.091: E/HJJ(10315): Activity &&&& onCreate...  
  3. 01-22 15:30:28.091: E/HJJ(10315): ArrayListFragment **** onAttach...  
  4. 01-22 15:30:28.091: E/HJJ(10315): ArrayListFragment **** onCreate...  
  5. 01-22 15:30:28.115: E/HJJ(10315): ArrayListFragment **** onCreateView...  
  6. 01-22 15:30:28.123: E/HJJ(10315): ArrayListFragment **** onActivityCreated...  
  7.   
  8. onStart过程  
  9. 01-22 15:30:28.123: E/HJJ(10315): Activity &&&& onStart...  
  10. 01-22 15:30:28.123: E/HJJ(10315): ArrayListFragment **** onStart...  
  11.   
  12. onResume过程  
  13. 01-22 15:30:28.123: E/HJJ(10315): Activity &&&& onResume...  
  14. 01-22 15:30:28.123: E/HJJ(10315): ArrayListFragment **** onResume...  
  15.   
  16. onPause过程  
  17. 01-22 15:31:26.748: E/HJJ(10315): ArrayListFragment **** onPause...  
  18. 01-22 15:31:26.748: E/HJJ(10315): Activity &&&& onPause...  
  19.   
  20. onStop过程  
  21. 01-22 15:31:27.638: E/HJJ(10315): ArrayListFragment **** onStop...  
  22. 01-22 15:31:27.638: E/HJJ(10315): Activity &&&& onStop...  
  23.   
  24. onStart过程  
  25. 01-22 15:31:57.537: E/HJJ(10315): Activity &&&& onStart...  
  26. 01-22 15:31:57.537: E/HJJ(10315): ArrayListFragment **** onStart...  
  27.   
  28. onResume过程  
  29. 01-22 15:31:57.537: E/HJJ(10315): Activity &&&& onResume...  
  30. 01-22 15:31:57.537: E/HJJ(10315): ArrayListFragment **** onResume...  
  31.   
  32. onPause过程  
  33. 01-22 15:32:47.412: E/HJJ(10315): ArrayListFragment **** onPause...  
  34. 01-22 15:32:47.412: E/HJJ(10315): Activity &&&& onPause...  
  35.   
  36. onStop过程  
  37. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onStop...  
  38. 01-22 15:32:47.865: E/HJJ(10315): Activity &&&& onStop...  
  39.   
  40. onDestroy过程  
  41. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onDestroyView...  
  42. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onDestroy...  
  43. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onDetach...  
  44. 01-22 15:32:47.865: E/HJJ(10315): Activity &&&& onDestroy...  

2. Acitivty的四中启动模式与特点。

一:Standard的启动模式

Standard是默认的模式每开始一个activity,就会在栈中加一个activity,相同的也会加,

所以加多少个,就要按多少次返回键才能回到最初的界面

二:singleTop的启动模式

1.清单配置:

<activity

android:name="com.itcode.taskstack.SecondActivity"

android:label="@string/_second"

android:launchMode="singleTop">

</activity>

Singletop:如果任务栈的栈顶已经存在这个activity的实例,

不会创建新的activity,而是利用旧的activity实例

调用 旧的activity的onNewIntent()方法

2.作用:

避免一个糟糕的用户体验,如果这个界面已经被打开且在任务栈的栈顶,就不会重复开启了

 

三:Singletask的启动模式:

1.Androidfest配置:

<activity

android:name="com.itcode.taskstack.SecondActivity"

android:label="@string/_second"

android:launchMode="singleTask">

</activity>

2.作用:

singletask的启动模式:在任务栈里面只允许一个实例存在,假如02是singletask,

栈里是:01 02 01 03 若此时开启02,则会复用这个已经存在的activity,并且把当前activity上面其他的activity从任务栈里清空!

相当于条用 onNewIntent+ClearTop

1. 设置了"singleTask"启动模式的Activity,它在启动的时候,会先在系统中查找属性值affinity等于它的属性值taskAffinity的任务存在;如果存在这样的任务,它就会在这个任务中启动,否则就会在新任务中启动。因此,如果我们想要设置了"singleTask"启动模式的Activity在新的任务中启动,就要为它设置一个独立的taskAffinity属性值。

        2. 如果设置了"singleTask"启动模式的Activity不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的Activity实例,如果存在,就会把位于这个Activity实例上面的Activity全部结束掉,即最终这个Activity实例会位于任务的堆栈顶端中。


3.应用场景:

浏览器:底层使用的是webkit c 内核,初始化一次需要申请很多的内存资源,占用cpu时间

所以使用singletask,保证在任务栈里只会有一个实例存在

四:singleInstance的启动模式(相当于实例):

 1.Androidfest的配置:

<activity

android:name="com.itcode.taskstack.SecondActivity"

android:label="@string/_second"

android:launchMode="singleInstance">

</activity>

2.特点:

singleInstance的启动模式更加极端,

开启新的activity,会给自己创建一个单独的任务栈

不管是从应用内部打开还是通过其他应用调用

TaskId是单独的,已存在的则只需调用onNewIntent

 3.应用场景:

在整个手机操作系统里面只会有一个该activity的实例存在,

有道词典,金山词典

所以多个应用程序共享这个activity的实例,有线程安全问题!

例如闹铃提醒,将闹铃提醒与闹铃设置分离





Affinity(吸引力)和新任务

默认情况下,一个应用程序中的activity相互之间会有一种Affinity──也就是说,它们首选都归属于一个任务。然而,可以在<activity>元素中把每个activitytaskAffinity属性设置为一个独立的affinity。于是在不同的应用程序中定义的activity可以享有同一个affinity,或者在同一个应用程序中定义的activity有着不同的affinityaffinity在两种情况下生效:当加载activityIntent对象包含了FLAG_ACTIVITY_NEW_TASK 标记,或者当activityallowTaskReparenting属性设置为“true”

FLAG_ACTIVITY_NEW_TASK标记

如前所述,在默认情况下,一个新activity被另外一个调用了startActivity()方法的activity载入了任务之中。并压入了调用者所在的堆栈。然而,如果传递给startActivity()Intent对象包含了FLAG_ACTIVITY_NEW_TASK标记,系统会为新activity安排另外一个任务。一般情况下,如同标记所暗示的那样,这会是一个新任务。然而,这并不是必然的。如果已经存在了一个与新activity有着同样affinity的任务,则activity会载入那个任务之中。如果没有,则启用新任务。

allowTaskReparenting 属性

如果一个activityallowTaskReparenting属性设置为“true”。它就可以从初始的任务中转移到与其拥有同一个affinity并转向前台的任务之中。比如说,一个旅行应用程序中包含的预报所选城市的天气情况的activity。它与这个应用程序中其它的activity拥有同样的affinity(默认的affinity)而且允许重定父级。你的另一个activity启动了天气预报,于是它就会与这个activity共处与同一任务之中。然而,当那个旅行应用程序再次回到前台的时候,这个天气预报activity就会被再次安排到原先的任务之中并显示出来。

如果在用户的角度看来,一个.apk文件中包含了多于一个的应用程序,你可能会想要为它们所辖的activity安排不一样的affinity

加载模式

<activity>元素的launchMode属性可以设置四种不同的加载模式:

"standard(默认值)
"singleTop
"singleTask
"singleInstance"

这些模式之间的差异主要体现在四个方面:

· 哪个任务会把持对intent做出响应的activitystandardsingleTop模式而言,是产生intent(并调用 startActivity())的任务──除非Intent对象包含FLAG_ACTIVITY_NEW_TASK标记。而在这种情况下,如同上面Affinitie和新任务一节所述,会是另外一个任务。 

相反,对singleTasksingleInstance模式而言,activity总是位于任务的根部。正是它们定义了一个任务,所以它们绝不会被载入到其它任务之中。

· activity是否可以存在多个实例。一个standardsingleTopactivity可以被多次初始化。它们可以归属于多个任务,而一个任务也可以拥有同一activity的多个实例。

相反,对singleTasksingleInstanceactivity被限定于只能有一个实例。因为这些activity都是任务的起源,这种限制意味着在一个设备中同一时间只允许存在一个任务的实例。

· 在实例所在的任务中是否会有别的activity一个singleInstance模式的activity将会是它所在的任务中唯一的activity。如果它启动了别的activity,那个activity将会依据它自己的加载模式加载到其它的任务中去──如同在intent中设置了FLAG_ACTIVITY_NEW_TASK 标记一样的效果。在其它方面,singleInstance模式的效果与singleTask是一样的。

剩下的三种模式允许一个任务中出现多个activitysingleTask模式的activity将是任务的根activity,但它可以启动别的activity并将它们置入所在的任务中。standardsingleTop”activity则可以在堆栈的任意位置出现。

· 是否要载入新的类实例以处理新的intent对默认的"standard"模式来说,对于每个新intent都会创建一个新的实例以进行响应,每个实例仅处理一个intentsingleTop模式下,如果activity位于目的任务堆栈的最上面,则重用目前现存的activity来处理新的intent。如果它不是在堆栈顶部,则不会发生重用。而是创建一个新实例来处理新的intent并将其推入堆栈。 

举例来说,假设一个任务的堆栈由根activityAactivity BC和位于堆栈顶部的D组成,即堆栈A-B-C-D。一个针对D类型的activityintent抵达的时候,如果D是默认的standard加载模式,则创建并加载一个新的类实例,于是堆栈变为A-B-C-D-D。 然而,如果D的载入模式为singleTop,则现有的实例会对新intent进行处理(因为它位于堆栈顶部)而堆栈保持A-B-C-D的形态。

换言之,如果新抵达的intent是针对B类型的activity,则无论B的模式是standard还是singleTop” ,都会加载一个新的B的实例(因为B不位于堆栈的顶部),而堆栈的顺序变为A-B-C-D-B

如前所述,singleTasksingleInstance模式的activity永远不会存在多于一个实例。所以实例将处理所有新的intent。一个singleInstance模式的activity永远保持在堆栈的顶部(因为它是那个堆栈中唯一的一个activity),所以它一直坚守在处理intent的岗位上。然而,对一个singleTask模式的activity来说,它上面可能有,也可能没有别的activity和它处于同一堆栈。在有的情况下,它就不在能够处理intent的位置上,则那个intent将被舍弃。(即便在intent被舍弃的情况下,它的抵达仍将使这个任务切换至前台,并一直保留)


3. Activity缓存方法。

Activity的onSaveInstanceState()和 onRestoreInstanceState()方法  

2013-01-28 22:08:13|  分类: Android |  标签:activity组件  |举报|字号 订阅

     Activity的 onSaveInstanceState() 和 onRestoreInstanceState()并不是生命周期方法,它们不同于 onCreate()onPause()等生命周期方法,它们并不一定会被触发。

     当应用遇到意外情况(如:内存不足)由系统销毁一个Activity时,onSaveInstanceState() 会被调用。但是当用户主动去销毁一个Activity时,例如在应用中按返回键,onSaveInstanceState()就不会被调用。因为在这种情况下,用户的行为决定了不需要保存Activity的状态。通常onSaveInstanceState()只适合用于保存一些临时性的状态onPause()适合用于数据的持久化保存

    另外,当屏幕的方向发生了改变 Activity会被摧毁并且被重新创建,如果你想在Activity摧毁前缓存一些数据,并且在Activity被重新创建后恢复缓存的数据。可以重写Activity的 onSaveInstanceState() onRestoreInstanceState()方法.

    注意:在Activity的onCreate(Bundle savedInstanceState)方法里面,该方法的参数与onRestoreInstanceState()方法中的参数一致,因此在onCreate()方法中也能恢复缓存的数据。


    如下:

public class PreferencesActivity extends Activity {
private String name;
protected void onRestoreInstanceState(Bundle savedInstanceState) {
name = savedInstanceState.getString("name"); //被重新创建后恢复缓存的数据
super.onRestoreInstanceState(savedInstanceState);
}
protected void onSaveInstanceState(Bundle outState) {
outState.putString("name", "liming");//被摧毁前缓存一些数据
super.onSaveInstanceState(outState);
}
}


4. Service的生命周期,两种启动方法,有什么区别。

1、Activity的生命周期

情形一、一个单独的Activity的正常的生命过程是这样的:onCreate->onStart->onPause->onStop->onDestroy。例如:运行一个Activity,进行了一些简单操作(不涉及页面的跳转等),然后按返回键结束。

 

情形二、有两个Activity(a和b),一开始显示a,然后由a启动b,然后在由b回到a,这时候a的生命过程应该是怎么样的呢(a被b完全遮盖)?

a经历的过程为onCreate->onStart->onResume->onPause->onStop->onRestart->onStart->onResume。这个过程说明了图中,如果Activity完全被其他界面遮挡时,进入后台,并没有完全销毁,而是停留在onStop状态,当再次进入a时,onRestart->onStart->onResume,又重新恢复。

 

情形三、基本情形同二一样,不过此时a被b部分遮盖(比如给b添加个对话框主题 android:theme="@android:style/Theme.Dialog"

a经历的过程是:onCreate->onStart->onResume->onPause->onResume

所以当Activity被部分遮挡时,Activity进入onPause,并没有进入onStop,从Activity2返回后,执行了onResume

 

情形四、 打开程序,启动a,点击a,启动AlertDialog,按返回键从AlertDialog返回。

a经历的过程是:onCreate->onStart->onResume

当启动和退出Dialog时,Activity的状态始终未变,可见,Dialog实际上属于Acitivity内部的界面,不会影响Acitivty的生命周期。

 

2、Service的生命周期

使用context.startService() 启动Service

其生命周期为context.startService() ->onCreate()- >onStart()->Service running-->(如果调用context.stopService() )->onDestroy() ->Service shut down

如果Service还没有运行,则android先调用onCreate()然后调用onStart();
如果Service已经运行,则只调用onStart(),所以一个Service的onStart方法可能会重复调用多次。 

调用stopService的时候直接onDestroy,
如果是调用者自己直接退出而没有调用stopService的话,Service会一直在后台运行。
该Service的调用者再启动起来后可以通过stopService关闭Service。

所以调用startService的生命周期为:onCreate --> onStart(可多次调用) --> onDestroy

对于bindService()启动Service会经历:
context.bindService()->onCreate()->onBind()->Service running-->onUnbind() -> onDestroy() ->Service stop

onBind将返回给客户端一个IBind接口实例,IBind允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。
这个时候把调用者(Context,例如Activity)会和Service绑定在一起,Context退出了,
Srevice就会调用onUnbind->onDestroy相应退出。 

所以调用bindService的生命周期为:onCreate --> onBind(只一次,不可多次绑定) --> onUnbind --> onDestory。
一但销毁activity它就结束,如果按home把它放到后台,那他就不退出。

PS:
在Service每一次的开启关闭过程中,只有onStart可被多次调用(通过多次startService调用),
其他onCreate,onBind,onUnbind,onDestory在一个生命周期中只能被调用一次。


5. 怎么保证service不被杀死。

android应用多线程守护让你很难杀死它

public class TestserviceActivity extends Activity {

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

System.out.println("activity运行的线程id:"+ Thread.currentThread().getName() +"--"+

Thread.currentThread().getId());

System.out.println("activity的进程id:"+android.os.Process.myPid());

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

startService(intent);

}

}

public class Service1 extends Service {

@Override

public IBinder onBind(Intent intent) {

// TODO Auto-generated method stub

return null;

}

@Override

public void onCreate() {

// TODO Auto-generated method stub

System.out.println("服务1 被开启");

System.out.println("服务运行的线程id:"+ Thread.currentThread().getName() +"--"+

Thread.currentThread().getId());

System.out.println("服务的进程id:"+android.os.Process.myPid());

super.onCreate();

}

@Override

public void onDestroy() {

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

startService(intent);

super.onDestroy();

}

}

public class Service2 extends Service {

@Override

public IBinder onBind(Intent intent) {

// TODO Auto-generated method stub

return null;

}

@Override

public void onCreate() {

System.out.println("服务2 被开启");

super.onCreate();

}

@Override

public void onDestroy() {

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

startService(intent);

super.onDestroy();

}

}

4、清单文件中

<service android:name=".Service1"

android:process="cn.it.yqq"

></service>

<service android:name=".Service2"></service>


http://blog.csdn.net/mad1989/article/details/22492519

6. 广播的两种注册方法,有什么区别。

 之前一直碰到这个问题,都没有证明回答,现在总结如下:

    通过在配置文件里面注册广播属于常驻型广播,意思就是即便你应用程序结束,一旦有了对应的广播过来,其还是会被激活;而在代码里注册的广播则是非常驻型广播,比如在oncreate方法里面注册一个广播,那么在ondestroy里就可以BroadcastReceiver.abortBroadcast(),比如注册一个监听SD卡的广播。

 

之前有一个概念经常混淆,就是发送广播sendBroadCast或sendOrderBroadCast是要发送自己定义的广播时,才用到的,如果是系统的广播,根本不需要用到它们,系统的广播,我们只需要注册广播监听,如定义intent_filter监听就可以了,所以abortBroadcast终止广播就更用不到了。

 

     1.常驻型广播
  常驻型广播,当你的应用程序关闭了,如果有广播信息来,你写的广播接收器同样的能接受到,
  他的注册方式就是在你的应用程序中的AndroidManifast.xml进行注册。通常说这种方式是静态注册
  下面是配置例子

Java代码  复制代码  收藏代码
  1.  <!-- 桌面 -->   
  2. <receiver android:name=".widget.DeskWidgeWeather">   
  3. <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_weather_provider" />   
  4. <intent-filter>   
  5.  <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>   
  6.  <action android:name="action_weather"/>   
  7. </intent-filter>   
  8. lt;/receiver>  

 

  2.非常驻型广播
   当应用程序结束了,广播自然就没有了,比如你在activity中的onCreate或者onResume中注册广播接收器
   在onDestory中卸载广播接收器。这样你的广播接收器就一个非常驻型的了。这种也叫动态注册。
   比如写一个监听SDcard状态的广播接收器

Java代码  复制代码  收藏代码
  1.   SdcardStateChanageReceiver sdcardStateReceiver;   
  2. @Override  
  3. protected void onCreate(Bundle savedInstanceState)   
  4. {   
  5.  super.onCreate(savedInstanceState);   
  6.  IntentFilter filter new IntentFilter();   
  7.  filter.addAction(Intent.ACTION_MEDIA_REMOVED);   
  8.  filter.addAction(Intent.ACTION_MEDIA_EJECT);   
  9.  filter.addAction(Intent.ACTION_MEDIA_MOUNTED);   
  10.  filter.addDataScheme("file");   
  11.  sdcardStateReceiver new SdcardStateChanageReceiver();    
  12.  registerReceiver(sdcardStateReceiver,filter);   
  13. }   
  14. @Override  
  15. protected void onDestroy(){   
  16.  unregisterReceiver(sdcardStateReceiver);   
  17. }   
  18. class SdcardStateChanageReceiver  extends BroadcastReceiver{   
  19.   
  20.  @Override  
  21.  public void onReceive(Context context, Intent intent)   
  22.  {   
  23.   String state=android.os.Environment.getExternalStorageState();   
  24.   System.out.println("SDCard 发生改变! 状态:"+state);   
  25.   //checkSDCard();   
  26.  }   
  27.  public void checkSDCard(){   
  28.   String state=android.os.Environment.getExternalStorageState();   
  29.   System.out.println(state);   
  30.   if(state.equals(android.os.Environment.MEDIA_REMOVED || state .equals(android.os.Environment.MEDIA_UNMOUNTED)){   
  31.    System.out.println("SDCard 已卸载!");   
  32.   }   
  33.  }   
  34. }   

BroadcastReceiver用于监听被广播的事件

必须被注册,有两种方法:

1、在应用程序的代码中注册

注册BroadcastReceiver:

registerReceiver(receiver,filter);

取消注册BroadcastReceiver:

unregisterReceiver(receiver);

当BroadcastReceiver更新UI,通常会使用这样的方法注册。启动Activity时候注册BroadcastReceiver,Activity不可见时候,取消注册。

2、在androidmanifest.xml当中注册

<receiver>

    <intent-filter>

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

    </intent-filter>

</receiver>

1)第一种不是常驻型广播,也就是说广播跟随程序的生命周期。

2)第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。


使用这样的方法注册弊端:它会始终处于活动状态,毕竟是手机开发,cpu和电源资源比较少,一直处于活动耗费大,不利。



7. Intent的使用方法,可以传递哪些数据类型。


1.Intent的实现过程

  在Android中,Intent不仅可用于应用程序之间的交互,也可用于应用程序内部的Activity/Service之间的交互。

  Intent负责对应用中一次操作进行描述,描述内容包括动作以及动作所涉及的数据,Android中的Intent机制则根据此描述,找到对应的组件,将Intent传递给该被调用组件,完成对组件的一次调用。

  这便是Intent的实现过程,可见,在Intent中提供了组件互相调用的相关信息,实现了调用者与被调用者之间的解耦。

2.Intent的应用场合

  归纳起来,Intent的应用场合主要有以下三种:

2.1启动一个Activity

  (1)Activity.startActivity(Intent intent);  //启动一个Activity

  (2)Activity.startActivityForResult(Intent intent, int requestCode);  //启动一个带请求码的Activity,当该Activity结束时将回调原Activity的onActivityResult()方法,并返回一个结果码。

2.2启动一个Service

  (1)Context.startService(Intent service); 

  (2)Context.bindService(Intent service, ServiceConnection conn, int flags); 
2.3启动一个Broadcast

  (1)sendBroadcast(Intent intent); 
    sendBroadcastAsUser(Intent intent, UserHandle user); 

  (2)sendStickyBroadcast(Intent intent); 
         sendStickyBroadcastAsUser(Intent intent, UserHandle user); 

  (3)sendOrderedBroadcast(Intent intent, String receiverPermission); 
      sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
BroadcastReceiver resultReceiver,Handler scheduler, int initialCode, String initialData, Bundle initialExtras); 

3.Intent属性设置

  Intent的属性设置包括:Action(要执行的动作)、Data(执行动作所操作的数据)、Type(显式的指定Intent的数据类型)、Category(执行动作的附加信息)、Component(指定Intent目标组件的类名称)、Extras(其它所有附加信息的集合)。    

3.1 Action(要执行的动作)

  在SDK中定义了一系列标准动作,其中的一部分如图1所示。



图1 部分标准动作


  其中,ACTION_CALL表示调用拨打电话的应用;ACTION_EDIT表示调用编辑器;ACTION_SYNC表示同步数据。

3.2 Data(执行动作所操作的数据)

  在Intent中,Data使用指向数据的URI来表示。比如,在联系人应用中,指向联系人列表的URI是content://contacts/people/。

3.3 Type(显式的指定Intent的数据类型)

  对于不同的动作,其URI数据的类型是不同的。

  通常,Intent的数据类型能够根据其数据本身进行判定,但是通过设置这个属性,可以强制采用显式指定的类型。

3.4 Category(执行动作的附加信息)

  Category表示执行动作的附加信息。比如,当我们想要让所执行的动作被接收后,作为顶级应用而位于其他所有应用的最上层,并可以使用附加信息LAUNCHER_CATEGORY来实现。 

3.5 Component(指定Intent目标组件的类名称)

  Component用于指定Intent目标组件的类名称。通常,Android会根据Intent 中所包含的其它属性信息(比如Action、Data/Type、Category)进行查找,并找到一个与之匹配的目标组件。但是,如果我们设置了Component属性,明确的指定了Intent目标组件的类名称,那么上述查找过程将不需要执行。 

3.6 Extras(其它所有附加信息的集合)

  使用extras可以为组件提供扩展信息。

4.Intent解析过程

  在使用Intent时,根据是否明确的指定Intent对象的接收者,而分为两种情况。一种是显式的Intent,即在构造Intent对象时就指定其接收者;另一种是隐式的Intent,即在构造Intent对象时,并不指定其接收者。

  对于显式的Intent来说,Android不需要解析Intent,因为目标组件已经很明确。对于隐式的Intent来说,Android需要对其进行解析,通过解析,将Intent映射给可以处理该Intent的Activity、Service或Broadcast。        

  Intent解析机制是通过查找注册在AndroidManifest.xml文件中的所有IntentFilter,以及IntentFilter所定义的Intent,找到最匹配的Intent。

  在解析过程中,Android通过判断Intent的Action、Type、Category这三个属性,从而找出最匹配的Intent,具体的判断方法如下:

  (1)如果Intent指明了Action,则目标组件IntentFilter的Action列表中就必须包含有这个Action,否则不能匹配;

  (2)如果Intent没有提供Type,系统将从Data中得到数据类型。目标组件的数据类型列表中必须包含Intent的数据类型,否则不能匹配。

  (3)如果Intent中的数据不是content: URI,而且Intent也没有明确指定它的Type,将根据Intent中数据的scheme (比如 http: 或者mailto:) 进行匹配。同理,Intent 的scheme必须出现在目标组件的scheme列表中,否则不能匹配。

  (4)如果Intent指定了一个或多个Category,这些类别必须全部出现在目标组件的类别列表中,否则不能匹配。

5.Intent使用实例

  下面介绍几个使用Intent的实例。

5.1调用其他的应用

  通过Intent可以调用并启动别的应用程序,比如调用拨打电话的程序,便可以使用如下的方法来完成:

  1. /*
  2.   * Function :  调用拨打电话的程序
  3.   * Author   :  博客园-依旧淡然
  4.   */
  5.   public void intentDemo_Call() {
  6.       Intent intent_call = new Intent();                //创建一个意图
  7.       intent_call.setAction(Intent.ACTION_CALL);        //设置意图为打电话
  8.       intent_call.setData(Uri.parse("tel:110"));        //设置电话号码
  9.       startActivity(intent_call);                       //启动意图
  10.   }
复制代码


当然了,因为这里使用到了打电话的功能,我们还需要在AndroidManifest.xml文件中,添加申请打电话的资源权限,具体实现方法如下:

  1.  <!-- 打电话的权限 -->
  2. 2     <uses-permission 
  3.       android:name="android.permission.CALL_PHONE"    />
复制代码

有关Android中的权限请求可以参阅《 Android数据手册02:android.permission权限请求汇总 》。
5.2跳转到另一个Activity

  通过使用Intent不仅可以调用别的应用程序,还可以实现应用程序内部之间Activity的跳转。比如如下的代码便实现了从MainActivity跳转到SecondaryActivity,并向SecondaryActivity中传递了两个数据name和age。

  1. /*
  2.       * Function  :  跳转到SecondaryActivity 
  3.       * Author    :  博客园-依旧淡然
  4.       */
  5.      public void intentDemo_GoToSecondaryActivity() {
  6.          Intent intent_toSecondary = new Intent();                      //创建一个意图
  7.          intent_toSecondary.setClass(this, SecondaryActivity.class);    //指定跳转到SecondaryActivity
  8.          intent_toSecondary.putExtra("name", "jack");                   //设置传递内容name
  9.          intent_toSecondary.putExtra("age", 23);                        //设置传递内容age
  10.          startActivity(intent_toSecondary);                             //启动意图
  11.      }
复制代码

 那么,如何在SecondaryActivity中接收从MainActivity中传过来的内容(name、age)呢?下面的代码给出了一种实现方案。

  1. /*
  2.       * Function  :  接收mainActivity中的intent_toSecondary
  3.       * Author    :  博客园-依旧淡然
  4.       */
  5.      public void acceptIntent() {
  6.          Intent intent_accept = getIntent();           //创建一个接收意图
  7.          Bundle bundle = intent_accept.getExtras();    //创建Bundle对象,用于接收Intent数据
  8.          String name = bundle.getString("name");       //获取Intent的内容name
  9.          int age = bundle.getInt("age");               //获取Intent的内容age
  10.      }
复制代码

5.3发送一个带回调方法的意图

  有时,我们可能需要通过定义在MainActivity中的某一控件来启动SecondaryActivity,并且当SecondaryActivity结束时,返给MainActivity一个执行结果。

  要实现上述的功能,只需要完成以下三步骤即可。


  (1)在MainActivity中实现向SecondaryActivity发送带请求码的意图,具体实现方法如下:


  1. /*
  2.       * Function  :  向SecondaryActivity发送带请求码的意图
  3.       * Author    :  博客园-依旧淡然
  4.       */
  5.      public void intentDemo_request() {
  6.          Intent intent_request = new Intent();                      //创建一个意图
  7.          intent_request.setClass(this, SecondaryActivity.class);    //指定跳转到SecondaryActivity
  8.          startActivityForResult(intent_request, REQUEST_CODE);      //启动带请求码意图
  9.      }
复制代码


(2)在SecondaryActivity中接收intent_request,并向意图中填充要返给MainActivity的内容,最后还需要设置一个返回码。具体的实现方法如下:


  1. /*
  2.       * Function  :  接收mainActivity中的intent_request并返回一个结果码
  3.       * Author    :  博客园-依旧淡然 
  4.       */
  5.      public void acceptIntentAndReturn() {
  6.          Intent intent = getIntent();                           //创建一个接收意图
  7.          intent.putExtra("back", "data of SecondaryActivity");  //设置意图的内容
  8.          setResult(RESULT_CODE, intent);                        //设置结果码
  9.          finish();                                              //结束SecondaryActivity,并返回MainActivity
  10.      }
复制代码
3)当结束SecondaryActivity时,程序将返回到MainActivity界面。此时,MainActivity中的onActivityResult()方法将被回调,而我们要做的就是实现该方法。在本示例中,该方法的具体实现方法如下:

  1. /*
  2.       * Function  :  onActivityResult回调方法
  3.       * Author    :  博客园-依旧淡然
  4.       */
  5.      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  6.          if(requestCode == REQUEST_CODE && resultCode == SecondaryActivity.RESULT_CODE) {
  7.              Bundle bundle = data.getExtras();
  8.              String str = bundle.getString("back");
  9.              Toast.makeText(this, "从SecondaryActivity的返回值为:" + str, Toast.LENGTH_LONG).show();
  10.          }
  11.      }
复制代码

 以上的代码,我们通过判断requestCode和resultCode便可以唯一的确定MainActivity中的调用者和SecondaryActivity中的被调用者,建立起了一一对应的关系。并且,我们通过Bundle对象获取了从SecondaryActivity中返回给MainActivity的内容,并通过Toast进行了输出,如图2所示。

图2 从SecondaryActivity中返回的内容


  由图2可见,从SecondaryActivity中返回的内容正是我们在SecondaryActivity中定义的字符串“data of SecondaryActivity”。


6.总结

  本篇博文主要介绍了Intent的相关概念,以及Intent在Activity中的三种使用方法。有关Intent在Service和Broadcast中的使用方法,将在后续的Service和Broadcast的学习过程中再做介绍。

原文链接:http://www.cnblogs.com/menlsh/archive/2013/03/30/2991111.html


8. ContentProvider使用方法。

Content Provider为存储数据和获取数据提供了统一的接口,它可以完成在不同应用程序下的数据共享,而在上一篇文章Android开发之SQLite的使用方法讲到的SQLite只能在同一个程序中共享数据。另外android为一些常见的数据,比如说音频,视频,图片,通讯录等提供了Content Provider,这样我们就可以很方便的对这些类型的数据操作了。使用ContentProvider的好处是开发人员不需要考虑数据内部是怎么存储的,比如说如果我们想利用ContenProvider来存数据,只需告诉insert函数该ContentProvider的uri和想存入的数据(包括列名和数值),查询时也是一样,只需输入Uri和查询的表,列名和查询条件,至于ContentProvider里面是怎么进行这些操作的我们不需要知道。

  实验基础

  在了解本实验的内容,需要用到下面这几个跟ContentProvider有关的类。

  UriMatcher:

  要了解UriMatcher,首先需要了解android中的Uri表示方法,众所周知,Uri为通用资源标识符,它代表的是要操作的数据,Android中的每一种资源(比如文本,图像,视频等)都可以用Uri来表示。Android中的Uri由以下三部分组成:”content://”(即authory),数据的路径,资源标识ID(可选),其中如果存在ID,则表示某一个具体的资源,如果不存在ID,则表示路径下的整体。因此addUri()函数的3个参数也是对应上面的那3个。

  UriMatcher的匹配过程分为3步,初始化UriMatcher;注册需要用的Uri;与已经注册的Uri进行匹配。

  ContentResolver :

  当使用ContentProvider在不同的应用程序中共享数据时,其数据的暴露方式是采取类似数据库中表的方法。而ContentResolver 是恰好是采用类似数据库的方法来从ContentProvider中存取数据的,它是通过Uri来查询ContentProvider中提供的数据,查询时,还需知道目的数据库的名称,数据段的数据类型,或者说资源的ID。

  SQLiteQueryBuilder:

  SQLiteQueryBuilder是一个用来生产SQL查询语句的辅助类,可以方便的去访问SQLiteDatabase. 在构造SQL查询语句时,它同样也需要指定表名,指定列名,指定where条件等。

  实验过程

  本文通过一个实例来实现一个ContentProvider,其实一般情况下我们是不需要自己来实现的,而是使用andorid内置的ContentProvider,但是自己实现一个以后,对它的原理能更深刻的认识,以后使用内置的就得心应手了。这是mars老师的话,本人火候不够,暂时还没深刻的体会。Mars老师将实现ContentProvider的步骤总结为如下:

  


 

 

  程序中需要4个java文件,下面就分别来介绍实现这些类需注意的事项:

  FirstProviderMetaData类:

  因为在继承类FirstContentProvider得到的子类中要用到很多常量,所以这里我们新建了一个类专门用来存储这些常量的,该类这里取名为FirstProviderMetaData,里面存着authorty名,数据库名,数据库版本号,表名,字表名,子表Uri,子表ContentProvider数据类型等等。其中字表是继承BaseColumns类的,而BaseColumns类中已经有_ID和_COUNT这2列了。

  DatabaseHelper类:

  与android中使用SQLite类似,这里同样需要一个继承SQLiteOpenHelper的子类,子类名为DatabaseHelper,我们在子类的回调函数onCreate()中建立了一个表,表名和表中的列名都是引用FirstProviderMetaData类中定义好了的常量的。

  FirstContentProvider类:

  新建一个类,名为FirstContentProvider,继承类ContentProvider这个类,且必须重写父类的下面5个方法,否则会报错。这5个方法分别为onCreate(), getType(), insert(), update(), delete().

  onCreate()为回调函数,是指当ContentProvider创建的时候调用,本程序在该函数中使用DatabaseHelper新建了一个SQLite数据库。

  在getType()方法完成的功能是根据传入的Uri,返回该Uri所表示的数据类型。函数内部是使用的UriMatcher来匹配该函数所传进来的参数,来得到其数据类型。

  insert()函数给指定的数据库表中插入一个指定的值,插入完成后必然会生成一个新的记录,然后利用该记录和表的Uri重新生成一个新的Uri,这个Uri就代表了插入成功的那条记录的Uri,该函数返回的也是这个Uri。

  MainActivity类:

  在MainActivity中,主要是有2个按钮,分为为它们绑定了监听器,来完成插入和查询操作。

  当单击插入按钮时,在监听器函数中会首先得到一个ContentResolver,然后当在执行ContentResolver的insert方法时会自动调用ContentProvider的insert方法,因为ContentResolver的insert方法中的第一个参数就为某个ContentProvider的Uri。

  AndroidManifest.xml:

  ContentProvider的使用需要在AndroidManifest.xml中进行注册,在activity标签外加入如下声明即可:

<provider android:name="com.example.cptest.FirstContentProvider" android:authorities="com.example.cptest.FirstContentProvider" />

  实验主要部分代码及注释:

MainActivity.java:

复制代码 代码如下:

package com.example.cptest;

//import com.example.cptest.FirstProviderMetaData;
import com.example.cptest.FirstProviderMetaData.UserTableMetaData;

import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {

    private Button insert = null;
    private Button query = null;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        insert = (Button)findViewById(R.id.insert);
        insert.setOnClickListener(new InsertOnClickListener());
        query = (Button)findViewById(R.id.query);
        query.setOnClickListener(new QueryOnClickListener());
        System.out.println(getContentResolver().getType(FirstProviderMetaData.UserTableMetaData.CONTENT_URI));
    }

    //往子表中插入一条记录
    public class InsertOnClickListener implements OnClickListener{

        public void onClick(View arg0) {
            // TODO Auto-generated method stub   
            ContentValues values = new ContentValues();
            values.put(FirstProviderMetaData.UserTableMetaData.USER_NAME, "tornadomeet");
            //实际上使用的是ContentResolver的insert方法
            //该insert中有2个参数,第一个为代表了ContentProvider的Uri,第二个参数为要插入的值。此处的insert函数
            //一执行,则自动调用ContentProvider的insert方法。
            Uri uri = getContentResolver().insert(FirstProviderMetaData.UserTableMetaData.CONTENT_URI,
                    values);
            System.out.println("uri--->" +uri.toString());
        }       
    }

   
    //查询也是采用的ContentResolver中的query方法。
    public class QueryOnClickListener implements OnClickListener{
        public void onClick(View v) {
            // TODO Auto-generated method stub       
            Cursor c = getContentResolver().query(FirstProviderMetaData.UserTableMetaData.CONTENT_URI,
                    null, null, null, null);
            while(c.moveToNext())
                System.out.println(c.getString(c.getColumnIndex(UserTableMetaData.USER_NAME)));
        }    
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
}


DatabaseHelper:
复制代码 代码如下:

package com.example.cptest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;

public class DatabaseHelper extends SQLiteOpenHelper {

    private static final int  VERSON = 1;//默认的数据库版本

    //继承SQLiteOpenHelper类的类必须有自己的构造函数
    //该构造函数4个参数,直接调用父类的构造函数。其中第一个参数为该类本身;第二个参数为数据库的名字;
    //第3个参数是用来设置游标对象的,这里一般设置为null;参数四是数据库的版本号。
    public DatabaseHelper(Context context, String name, CursorFactory factory, int verson){
        super(context, name, factory, verson);
    }

    //该构造函数有3个参数,因为它把上面函数的第3个参数固定为null了
    public DatabaseHelper(Context context, String name, int verson){
        this(context, name, null, verson);
    }

    //该构造函数只有2个参数,在上面函数 的基础山将版本号固定了
    public DatabaseHelper(Context context, String name){
        this(context, name, VERSON);
    }

    //该函数在数据库第一次被建立时调用
    @Override
    public void onCreate(SQLiteDatabase arg0) {
        // TODO Auto-generated method stub
        System.out.println("create a database");
        //execSQL()为执行参数里面的SQL语句,因此参数中的语句需要符合SQL语法,这里是创建一个表
        //arg0.execSQL("create table user1(id int, name varchar(20))");下面的语句格式是与该句类似
//        arg0.execSQL("create table" + FirstProviderMetaData.USERS_TABLE_NAME
//                + "(" + FirstProviderMetaData.UserTableMetaData._ID
//                + " INTEGER PRIMARY KEY AUTOINCREMENT," +   //ID类型为自增长的整型
//                FirstProviderMetaData.UserTableMetaData.USER_NAME + " varchar(20));"
//                );
    //    arg0.execSQL("create table user1(id int, name varchar(20))");
        arg0.execSQL("create table" + FirstProviderMetaData.USERS_TABLE_NAME + "("
                + FirstProviderMetaData.UserTableMetaData._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                +FirstProviderMetaData.UserTableMetaData.USER_NAME + " varchar(20))");
        System.out.println("create a database ok");
    }

    @Override
    public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
        // TODO Auto-generated method stub
        System.out.println("update a database");
    }

}


FirstProviderMetaData.java:
复制代码 代码如下:

package com.example.cptest;

import android.net.Uri;
import android.provider.BaseColumns;

public class FirstProviderMetaData {

    //这里的AUTHORTY为包的全名+ContentProvider子类的全名
    public static final String AUTHORTY = "com.example.cptest.FirstContentProvider";
    //数据库的名称
    public static final String DATABASE_NAME = "FisrtProvider.db";
    //数据库的版本号
    public static final int DATABASE_VERSION = 1;
    //数据库中的表名
    public static final String USERS_TABLE_NAME = "users";
    //表中的字表
    public static final class UserTableMetaData implements BaseColumns{
        //子表名
        public static final String TABLE_NAME = "users";
        //CONTENT_URI为常量Uri; parse是将文本转换成Uri
        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORTY + "/users");
        //返回ContentProvider中表的数据类型
        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.firstprovider.user";
        //返回ContentProvider表中item的数据类型
        public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.firstprovider.user";
        //子表列名
        public static final String USER_NAME = "name";
        //表中记录的默认排序算法,这里是降序排列
        public static final String DEFAULT_SORT_ORDER = "_id desc";

       
    }

}


FirstContentProvider.java:
复制代码 代码如下:

package com.example.cptest;

import java.util.HashMap;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;

import com.example.cptest.FirstProviderMetaData.UserTableMetaData;

public class FirstContentProvider extends ContentProvider {

    //定义一个UriMatcher类对象,用来匹配Uri的。
    public static final UriMatcher uriMatcher;
    //组时的ID
    public static final int INCOMING_USER_COLLECTION = 1;
    //单个时的ID
    public static final int INCOMING_USER_SIGNAL = 2;
    private DatabaseHelper dh;//定义一个DatabaseHelper对象
    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);//UriMatcher.NO_MATCH表示不匹配任何路径的返回码
        uriMatcher.addURI(FirstProviderMetaData.AUTHORTY, "users", INCOMING_USER_COLLECTION);
        uriMatcher.addURI(FirstProviderMetaData.AUTHORTY, "users/#", INCOMING_USER_SIGNAL);//后面加了#表示为单个
    }

    public static HashMap<String, String> userProjectionMap;//新建一个HashMap,后面执行插入操作时有用
    static
    {
        userProjectionMap = new HashMap<String, String>();
        //这里可以直接调用另外一个类的public变量,这里put里面的2个参数一样,
        //是因为这里是给数据库表中的列取别名,因此取的是一样的名字
        userProjectionMap.put(UserTableMetaData._ID, UserTableMetaData._ID);
        userProjectionMap.put(UserTableMetaData.USER_NAME, UserTableMetaData.USER_NAME);
    }

    //得到ContentProvider的数据类型,返回的参数Uri所代表的数据类型
    @Override
    public String getType(Uri arg0) {
        // TODO Auto-generated method stub
        System.out.println("getType");
        switch(uriMatcher.match(arg0)){
        //matcher满足Uri的前2项(即协议+路径)为第1种情况时,switch语句的值为Uri的第3项,此处为INCOMING_USER_COLLECTION
        case INCOMING_USER_COLLECTION:
            return UserTableMetaData.CONTENT_TYPE;
        case INCOMING_USER_SIGNAL://同上
            return UserTableMetaData.CONTENT_TYPE_ITEM;
        default:
            throw new IllegalArgumentException("Unknown URI" + arg0);//throw是处理异常的,java中的语法
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // TODO Auto-generated method stub
        System.out.println("delete");
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        // TODO Auto-generated method stub
        System.out.println("update");
        return 0;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // TODO Auto-generated method stub
        System.out.println("insert");
//        dh = new DatabaseHelper(getContext(), FirstProviderMetaData.DATABASE_NAME);
        SQLiteDatabase db = dh.getWritableDatabase();
        long rowId = db.insert(UserTableMetaData.TABLE_NAME, null, values);
//        System.out.println("insert  OK");
//        System.out.println("" + rowId);
        if(rowId > 0){           
            //发出通知给监听器,说明数据已经改变
            //ContentUris为工具类
            Uri insertedUserUri = ContentUris.withAppendedId(UserTableMetaData.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(insertedUserUri, null);

            return insertedUserUri;
        }
        throw new SQLException("Failed to insert row into" + uri);
    }

    //回调函数,在ContentProvider创建的时候调用
    @Override
    public boolean onCreate() {
        // TODO Auto-generated method stub
        System.out.println("onCreate");
        dh = new DatabaseHelper(getContext(), FirstProviderMetaData.DATABASE_NAME);//创建1个DatabaseHelper对象
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        // TODO Auto-generated method stub
        System.out.println("query");
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        switch(uriMatcher.match(uri)){
        case INCOMING_USER_COLLECTION:
            qb.setTables(UserTableMetaData.TABLE_NAME);//设置表的名称
            qb.setProjectionMap(userProjectionMap);//其中userProjectionMap为上面建立好了的hashmap
            break;
        case INCOMING_USER_SIGNAL:
            qb.setTables(UserTableMetaData.TABLE_NAME);//设置表的名称
            qb.setProjectionMap(userProjectionMap);//其中userProjectionMap为上面建立好了的hashmap
            //uri.getPathSegments()得到Path部分,即把uri的协议+authory部分去掉,把剩下的部分分段获取,这里取第
            //一部分
            qb.appendWhere(UserTableMetaData._ID + "=" +uri.getPathSegments().get(1));//设置where条件
            break;
        }
        //排序
        String orderBy;
        if(TextUtils.isEmpty(sortOrder)){
            orderBy = UserTableMetaData.DEFAULT_SORT_ORDER;//传入的排序参数为空的时候采用默认的排序
        }
        else{
            orderBy = sortOrder;//不为空时用指定的排序方法进行排序
        }
        SQLiteDatabase db = dh.getWritableDatabase();
        //采用传入的参数进行查询
        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
        //发出通知
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

}


9. ThreadAsycTaskIntentService的使用场景与特点。

http://www.cnblogs.com/shaweng/p/4024766.html

10. 五种布局: FrameLayout  LinearLayout  AbsoluteLayout  RelativeLayout TableLayout 各自特点及绘制效率对比。

11. Android的数据存储形式。

本文介绍Android平台进行数据存储的五大方式,分别如下:   

    1 使用SharedPreferences存储数据

    2 文件存储数据      

    3 SQLite数据库存储数据

    4 使用ContentProvider存储数据

    5 网络存储数据

下面详细讲解这五种方式的特点

第一种: 使用SharedPreferences存储数据

    适用范围保存少量的数据,且这些数据的格式非常简单:字符串型、基本类型的值。比如应用程序的各种配置信息(如是否打开音效、是否使用震动效果、小游戏的玩家积分等),解锁口 令密码等

    核心原理保存基于XML文件存储的key-value键值对数据,通常用来存储一些简单的配置信息。通过DDMS的File Explorer面板,展开文件浏览树,很明显SharedPreferences数据总是存储在/data/data/<package name>/shared_prefs目录下。SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过SharedPreferences.edit()获取的内部接口Editor对象实现。 SharedPreferences本身是一 个接口,程序无法直接创建SharedPreferences实例,只能通过Context提供的getSharedPreferences(String name, int mode)方法来获取SharedPreferences实例,该方法中name表示要操作的xml文件名,第二个参数具体如下:

                 Context.MODE_PRIVATE: 指定该SharedPreferences数据只能被本应用程序读、写。

                 Context.MODE_WORLD_READABLE:  指定该SharedPreferences数据能被其他应用程序读,但不能写。

                 Context.MODE_WORLD_WRITEABLE:  指定该SharedPreferences数据能被其他应用程序读,

Editor有如下主要重要方法:

                 SharedPreferences.Editor clear():清空SharedPreferences里所有数据

                 SharedPreferences.Editor putXxx(String key , xxx value): 向SharedPreferences存入指定key对应的数据,其中xxx 可以是boolean,float,int等各种基本类型据

                 SharedPreferences.Editor remove(): 删除SharedPreferences中指定key对应的数据项

                 boolean commit(): 当Editor编辑完成后,使用该方法提交修改

    实际案例:运行界面如下

                       

这里只提供了两个按钮和一个输入文本框,布局简单,故在此不给出界面布局文件了,程序核心代码如下:         

复制代码
class ViewOcl implements View.OnClickListener{

        @Override
        public void onClick(View v) {

            switch(v.getId()){
            case R.id.btnSet:
                //步骤1:获取输入值
                String code = txtCode.getText().toString().trim();
                //步骤2-1:创建一个SharedPreferences.Editor接口对象,lock表示要写入的XML文件名,MODE_WORLD_WRITEABLE写操作
                SharedPreferences.Editor editor = getSharedPreferences("lock", MODE_WORLD_WRITEABLE).edit();
                //步骤2-2:将获取过来的值放入文件
                editor.putString("code", code);
                //步骤3:提交
                editor.commit();
                Toast.makeText(getApplicationContext(), "口令设置成功", Toast.LENGTH_LONG).show();
                break;
            case R.id.btnGet:
                //步骤1:创建一个SharedPreferences接口对象
                SharedPreferences read = getSharedPreferences("lock", MODE_WORLD_READABLE);
                //步骤2:获取文件中的值
                String value = read.getString("code", "");
                Toast.makeText(getApplicationContext(), "口令为:"+value, Toast.LENGTH_LONG).show();
                
                break;
                
            }
        }
        
    }
复制代码

读写其他应用的SharedPreferences: 步骤如下

                1、在创建SharedPreferences时,指定MODE_WORLD_READABLE模式,表明该SharedPreferences数据可以被其他程序读取

                2、创建其他应用程序对应的Context:

                    Context pvCount = createPackageContext("com.tony.app", Context.CONTEXT_IGNORE_SECURITY);这里的com.tony.app就是其他程序的包名

                3、使用其他程序的Context获取对应的SharedPreferences

                    SharedPreferences read = pvCount.getSharedPreferences("lock", Context.MODE_WORLD_READABLE);

                4、如果是写入数据,使用Editor接口即可,所有其他操作均和前面一致。

SharedPreferences对象与SQLite数据库相比,免去了创建数据库,创建表,写SQL语句等诸多操作,相对而言更加方便,简洁。但是SharedPreferences也有其自身缺陷,比如其职能存储boolean,int,float,long和String五种简单的数据类型,比如其无法进行条件查询等。所以不论SharedPreferences的数据存储操作是如何简单,它也只能是存储方式的一种补充,而无法完全替代如SQLite数据库这样的其他数据存储方式。

 

第二种: 文件存储数据

 核心原理: Context提供了两个方法来打开数据文件里的文件IO流 FileInputStream openFileInput(String name); FileOutputStream(String name , int mode),这两个方法第一个参数 用于指定文件名,第二个参数指定打开文件的模式。具体有以下值可选:

             MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容,如果想把新写入的内容追加到原文件中。可   以使用Context.MODE_APPEND

             MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。

             MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;

             MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。

 除此之外,Context还提供了如下几个重要的方法:

             getDir(String name , int mode):在应用程序的数据文件夹下获取或者创建name对应的子目录

             File getFilesDir():获取该应用程序的数据文件夹得绝对路径

             String[] fileList():返回该应用数据文件夹的全部文件               

实际案例:界面沿用上图

             核心代码如下:

复制代码
public String read() {
        try {
            FileInputStream inStream = this.openFileInput("message.txt");
            byte[] buffer = new byte[1024];
            int hasRead = 0;
            StringBuilder sb = new StringBuilder();
            while ((hasRead = inStream.read(buffer)) != -1) {
                sb.append(new String(buffer, 0, hasRead));
            }

            inStream.close();
            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return null;
    }
    
    public void write(String msg){
        // 步骤1:获取输入值
        if(msg == null) return;
        try {
            // 步骤2:创建一个FileOutputStream对象,MODE_APPEND追加模式
            FileOutputStream fos = openFileOutput("message.txt",
                    MODE_APPEND);
            // 步骤3:将获取过来的值放入文件
            fos.write(msg.getBytes());
            // 步骤4:关闭数据流
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
复制代码

openFileOutput()方法的第一参数用于指定文件名称,不能包含路径分隔符“/” ,如果文件不存在,Android 会自动创建它。创建的文件保存在/data/data/<package name>/files目录,如: /data/data/cn.tony.app/files/message.txt

 下面讲解某些特殊文件读写需要注意的地方:

读写sdcard上的文件

其中读写步骤按如下进行:

1、调用Environment的getExternalStorageState()方法判断手机上是否插了sd卡,且应用程序具有读写SD卡的权限,如下代码将返回true

Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)

2、调用Environment.getExternalStorageDirectory()方法来获取外部存储器,也就是SD卡的目录,或者使用"/mnt/sdcard/"目录

3、使用IO流操作SD卡上的文件 

注意点:手机应该已插入SD卡,对于模拟器而言,可通过mksdcard命令来创建虚拟存储卡

           必须在AndroidManifest.xml上配置读写SD卡的权限

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

案例代码:

复制代码
// 文件写操作函数
    private void write(String content) {
        if (Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED)) { // 如果sdcard存在
            File file = new File(Environment.getExternalStorageDirectory()
                    .toString()
                    + File.separator
                    + DIR
                    + File.separator
                    + FILENAME); // 定义File类对象
            if (!file.getParentFile().exists()) { // 父文件夹不存在
                file.getParentFile().mkdirs(); // 创建文件夹
            }
            PrintStream out = null; // 打印流对象用于输出
            try {
                out = new PrintStream(new FileOutputStream(file, true)); // 追加文件
                out.println(content);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (out != null) {
                    out.close(); // 关闭打印流
                }
            }
        } else { // SDCard不存在,使用Toast提示用户
            Toast.makeText(this, "保存失败,SD卡不存在!", Toast.LENGTH_LONG).show();
        }
    }

    // 文件读操作函数
    private String read() {

        if (Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED)) { // 如果sdcard存在
            File file = new File(Environment.getExternalStorageDirectory()
                    .toString()
                    + File.separator
                    + DIR
                    + File.separator
                    + FILENAME); // 定义File类对象
            if (!file.getParentFile().exists()) { // 父文件夹不存在
                file.getParentFile().mkdirs(); // 创建文件夹
            }
            Scanner scan = null; // 扫描输入
            StringBuilder sb = new StringBuilder();
            try {
                scan = new Scanner(new FileInputStream(file)); // 实例化Scanner
                while (scan.hasNext()) { // 循环读取
                    sb.append(scan.next() + "\n"); // 设置文本
                }
                return sb.toString();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (scan != null) {
                    scan.close(); // 关闭打印流
                }
            }
        } else { // SDCard不存在,使用Toast提示用户
            Toast.makeText(this, "读取失败,SD卡不存在!", Toast.LENGTH_LONG).show();
        }
        return null;
    }
复制代码

 第三种:SQLite存储数据

SQLite是轻量级嵌入式数据库引擎,它支持 SQL 语言,并且只利用很少的内存就有很好的性能。现在的主流移动设备像Android、iPhone等都使用SQLite作为复杂数据的存储引擎,在我们为移动设备开发应用程序时,也许就要使用到SQLite来存储我们大量的数据,所以我们就需要掌握移动设备上的SQLite开发技巧

SQLiteDatabase类为我们提供了很多种方法,上面的代码中基本上囊括了大部分的数据库操作;对于添加、更新和删除来说,我们都可以使用

1 db.executeSQL(String sql);  
2 db.executeSQL(String sql, Object[] bindArgs);//sql语句中使用占位符,然后第二个参数是实际的参数集 

除了统一的形式之外,他们还有各自的操作方法:

1 db.insert(String table, String nullColumnHack, ContentValues values);  
2 db.update(String table, Contentvalues values, String whereClause, String whereArgs);  
3 db.delete(String table, String whereClause, String whereArgs);

以上三个方法的第一个参数都是表示要操作的表名;insert中的第二个参数表示如果插入的数据每一列都为空的话,需要指定此行中某一列的名称,系统将此列设置为NULL,不至于出现错误;insert中的第三个参数是ContentValues类型的变量,是键值对组成的Map,key代表列名,value代表该列要插入的值;update的第二个参数也很类似,只不过它是更新该字段key为最新的value值,第三个参数whereClause表示WHERE表达式,比如“age > ? and age < ?”等,最后的whereArgs参数是占位符的实际参数值;delete方法的参数也是一样

下面给出demo

数据的添加

1.使用insert方法

复制代码
1 ContentValues cv = new ContentValues();//实例化一个ContentValues用来装载待插入的数据
2 cv.put("title","you are beautiful");//添加title
3 cv.put("weather","sun"); //添加weather
4 cv.put("context","xxxx"); //添加context
5 String publish = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
6                         .format(new Date());
7 cv.put("publish ",publish); //添加publish
8 db.insert("diary",null,cv);//执行插入操作
复制代码

2.使用execSQL方式来实现

String sql = "insert into user(username,password) values ('Jack Johnson','iLovePopMuisc');//插入操作的SQL语句
db.execSQL(sql);//执行SQL语句

数据的删除

同样有2种方式可以实现

String whereClause = "username=?";//删除的条件
String[] whereArgs = {"Jack Johnson"};//删除的条件参数
db.delete("user",whereClause,whereArgs);//执行删除

使用execSQL方式的实现

String sql = "delete from user where username='Jack Johnson'";//删除操作的SQL语句
db.execSQL(sql);//执行删除操作

数据修改

同上,仍是2种方式

ContentValues cv = new ContentValues();//实例化ContentValues
cv.put("password","iHatePopMusic");//添加要更改的字段及内容
String whereClause = "username=?";//修改条件
String[] whereArgs = {"Jack Johnson"};//修改条件的参数
db.update("user",cv,whereClause,whereArgs);//执行修改

使用execSQL方式的实现

String sql = "update user set password = 'iHatePopMusic' where username='Jack Johnson'";//修改的SQL语句
db.execSQL(sql);//执行修改

数据查询

下面来说说查询操作。查询操作相对于上面的几种操作要复杂些,因为我们经常要面对着各种各样的查询条件,所以系统也考虑到这种复杂性,为我们提供了较为丰富的查询形式:

1 db.rawQuery(String sql, String[] selectionArgs);  
2 db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy);  
3 db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);  
4 db.query(String distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);  

上面几种都是常用的查询方法,第一种最为简单,将所有的SQL语句都组织到一个字符串中,使用占位符代替实际参数,selectionArgs就是占位符实际参数集;

各参数说明:

  • table:表名称
  • colums:表示要查询的列所有名称集
  • selection:表示WHERE之后的条件语句,可以使用占位符
  • selectionArgs:条件语句的参数数组
  • groupBy:指定分组的列名
  • having:指定分组条件,配合groupBy使用
  • orderBy:y指定排序的列名
  • limit:指定分页参数
  • distinct:指定“true”或“false”表示要不要过滤重复值
  • Cursor:返回值,相当于结果集ResultSet

最后,他们同时返回一个Cursor对象,代表数据集的游标,有点类似于JavaSE中的ResultSet。下面是Cursor对象的常用方法:

复制代码
 1 c.move(int offset); //以当前位置为参考,移动到指定行  
 2 c.moveToFirst();    //移动到第一行  
 3 c.moveToLast();     //移动到最后一行  
 4 c.moveToPosition(int position); //移动到指定行  
 5 c.moveToPrevious(); //移动到前一行  
 6 c.moveToNext();     //移动到下一行  
 7 c.isFirst();        //是否指向第一条  
 8 c.isLast();     //是否指向最后一条  
 9 c.isBeforeFirst();  //是否指向第一条之前  
10 c.isAfterLast();    //是否指向最后一条之后  
11 c.isNull(int columnIndex);  //指定列是否为空(列基数为0)  
12 c.isClosed();       //游标是否已关闭  
13 c.getCount();       //总数据项数  
14 c.getPosition();    //返回当前游标所指向的行数  
15 c.getColumnIndex(String columnName);//返回某列名对应的列索引值  
16 c.getString(int columnIndex);   //返回当前行指定列的值 
复制代码

实现代码

复制代码
String[] params =  {12345,123456};
Cursor cursor = db.query("user",columns,"ID=?",params,null,null,null);//查询并获得游标 if(cursor.moveToFirst()){//判断游标是否为空 for(int i=0;i<cursor.getCount();i++){ cursor.move(i);//移动到指定记录 String username = cursor.getString(cursor.getColumnIndex("username"); String password = cursor.getString(cursor.getColumnIndex("password")); } }
复制代码

通过rawQuery实现的带参数查询

复制代码
Cursor result=db.rawQuery("SELECT ID, name, inventory FROM mytable");
//Cursor c = db.rawQuery("s name, inventory FROM mytable where ID=?",new Stirng[]{"123456"});     
result.moveToFirst(); 
while (!result.isAfterLast()) { 
    int id=result.getInt(0); 
    String name=result.getString(1); 
    int inventory=result.getInt(2); 
    // do something useful with these 
    result.moveToNext(); 
 } 
 result.close();
复制代码

 

在上面的代码示例中,已经用到了这几个常用方法中的一些,关于更多的信息,大家可以参考官方文档中的说明。

最后当我们完成了对数据库的操作后,记得调用SQLiteDatabase的close()方法释放数据库连接,否则容易出现SQLiteException。

上面就是SQLite的基本应用,但在实际开发中,为了能够更好的管理和维护数据库,我们会封装一个继承自SQLiteOpenHelper类的数据库操作类,然后以这个类为基础,再封装我们的业务逻辑方法。

这里直接使用案例讲解:下面是案例demo的界面

SQLiteOpenHelper类介绍

SQLiteOpenHelper是SQLiteDatabase的一个帮助类,用来管理数据库的创建和版本的更新。一般是建立一个类继承它,并实现它的onCreate和onUpgrade方法。

方法名 方法描述
SQLiteOpenHelper(Context context,String name,SQLiteDatabase.CursorFactory factory,int version)

构造方法,其中

context 程序上下文环境 即:XXXActivity.this;

name :数据库名字;

factory:游标工厂,默认为null,即为使用默认工厂;

version 数据库版本号

onCreate(SQLiteDatabase db) 创建数据库时调用
onUpgrade(SQLiteDatabase db,int oldVersion , int newVersion) 版本更新时调用
getReadableDatabase() 创建或打开一个只读数据库
getWritableDatabase() 创建或打开一个读写数据库

首先创建数据库类

复制代码
 1 import android.content.Context;
 2 import android.database.sqlite.SQLiteDatabase;
 3 import android.database.sqlite.SQLiteDatabase.CursorFactory;
 4 import android.database.sqlite.SQLiteOpenHelper;
 5 
 6 public class SqliteDBHelper extends SQLiteOpenHelper {
 7 
 8     // 步骤1:设置常数参量
 9     private static final String DATABASE_NAME = "diary_db";
10     private static final int VERSION = 1;
11     private static final String TABLE_NAME = "diary";
12 
13     // 步骤2:重载构造方法
14     public SqliteDBHelper(Context context) {
15         super(context, DATABASE_NAME, null, VERSION);
16     }
17 
18     /*
19      * 参数介绍:context 程序上下文环境 即:XXXActivity.this 
20      * name 数据库名字 
21      * factory 接收数据,一般情况为null
22      * version 数据库版本号
23      */
24     public SqliteDBHelper(Context context, String name, CursorFactory factory,
25             int version) {
26         super(context, name, factory, version);
27     }
28     //数据库第一次被创建时,onCreate()会被调用
29     @Override
30     public void onCreate(SQLiteDatabase db) {
31         // 步骤3:数据库表的创建
32         String strSQL = "create table "
33                 + TABLE_NAME
34                 + "(tid integer primary key autoincrement,title varchar(20),weather varchar(10),context text,publish date)";
35         //步骤4:使用参数db,创建对象
36         db.execSQL(strSQL);
37     }
38     //数据库版本变化时,会调用onUpgrade()
39     @Override
40     public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
41 
42     }
43 }
复制代码

正如上面所述,数据库第一次创建时onCreate方法会被调用,我们可以执行创建表的语句,当系统发现版本变化之后,会调用onUpgrade方法,我们可以执行修改表结构等语句。

 我们需要一个Dao,来封装我们所有的业务方法,代码如下:

复制代码
 1 import android.content.Context;
 2 import android.database.Cursor;
 3 import android.database.sqlite.SQLiteDatabase;
 4 
 5 import com.chinasoft.dbhelper.SqliteDBHelper;
 6 
 7 public class DiaryDao {
 8 
 9     private SqliteDBHelper sqliteDBHelper;
10     private SQLiteDatabase db;
11 
12     // 重写构造方法
13     public DiaryDao(Context context) {
14         this.sqliteDBHelper = new SqliteDBHelper(context);
15         db = sqliteDBHelper.getWritableDatabase();
16     }
17 
18     // 读操作
19     public String execQuery(final String strSQL) {
20         try {
21             System.out.println("strSQL>" + strSQL);
22             // Cursor相当于JDBC中的ResultSet
23             Cursor cursor = db.rawQuery(strSQL, null);
24             // 始终让cursor指向数据库表的第1行记录
25             cursor.moveToFirst();
26             // 定义一个StringBuffer的对象,用于动态拼接字符串
27             StringBuffer sb = new StringBuffer();
28             // 循环游标,如果不是最后一项记录
29             while (!cursor.isAfterLast()) {
30                 sb.append(cursor.getInt(0) + "/" + cursor.getString(1) + "/"
31                         + cursor.getString(2) + "/" + cursor.getString(3) + "/"
32                         + cursor.getString(4)+"#");
33                 //cursor游标移动
34                 cursor.moveToNext();
35             }
36             db.close();
37             return sb.deleteCharAt(sb.length()-1).toString();
38         } catch (RuntimeException e) {
39             e.printStackTrace();
40             return null;
41         }
42 
43     }
44 
45     // 写操作
46     public boolean execOther(final String strSQL) {
47         db.beginTransaction();  //开始事务
48         try {
49             System.out.println("strSQL" + strSQL);
50             db.execSQL(strSQL);
51             db.setTransactionSuccessful();  //设置事务成功完成 
52             db.close();
53             return true;
54         } catch (RuntimeException e) {
55             e.printStackTrace();
56             return false;
57         }finally {  
58             db.endTransaction();    //结束事务  
59         }  
60 
61     }
62 }
复制代码

我们在Dao构造方法中实例化sqliteDBHelper并获取一个SQLiteDatabase对象,作为整个应用的数据库实例;在增删改信息时,我们采用了事务处理,确保数据完整性;最后要注意释放数据库资源db.close(),这一个步骤在我们整个应用关闭时执行,这个环节容易被忘记,所以朋友们要注意。

我们获取数据库实例时使用了getWritableDatabase()方法,也许朋友们会有疑问,在getWritableDatabase()和getReadableDatabase()中,你为什么选择前者作为整个应用的数据库实例呢?在这里我想和大家着重分析一下这一点。

我们来看一下SQLiteOpenHelper中的getReadableDatabase()方法:

复制代码
 1 public synchronized SQLiteDatabase getReadableDatabase() {  
 2     if (mDatabase != null && mDatabase.isOpen()) {  
 3         // 如果发现mDatabase不为空并且已经打开则直接返回  
 4         return mDatabase;  
 5     }  
 6   
 7     if (mIsInitializing) {  
 8         // 如果正在初始化则抛出异常  
 9         throw new IllegalStateException("getReadableDatabase called recursively");  
10     }  
11   
12     // 开始实例化数据库mDatabase  
13   
14     try {  
15         // 注意这里是调用了getWritableDatabase()方法  
16         return getWritableDatabase();  
17     } catch (SQLiteException e) {  
18         if (mName == null)  
19             throw e; // Can't open a temp database read-only!  
20         Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);  
21     }  
22   
23     // 如果无法以可读写模式打开数据库 则以只读方式打开  
24   
25     SQLiteDatabase db = null;  
26     try {  
27         mIsInitializing = true;  
28         String path = mContext.getDatabasePath(mName).getPath();// 获取数据库路径  
29         // 以只读方式打开数据库  
30         db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);  
31         if (db.getVersion() != mNewVersion) {  
32             throw new SQLiteException("Can't upgrade read-only database from version " + db.getVersion() + " to "  
33                     + mNewVersion + ": " + path);  
34         }  
35   
36         onOpen(db);  
37         Log.w(TAG, "Opened " + mName + " in read-only mode");  
38         mDatabase = db;// 为mDatabase指定新打开的数据库  
39         return mDatabase;// 返回打开的数据库  
40     } finally {  
41         mIsInitializing = false;  
42         if (db != null && db != mDatabase)  
43             db.close();  
44     }  
45 }
复制代码

在getReadableDatabase()方法中,首先判断是否已存在数据库实例并且是打开状态,如果是,则直接返回该实例,否则试图获取一个可读写模式的数据库实例,如果遇到磁盘空间已满等情况获取失败的话,再以只读模式打开数据库,获取数据库实例并返回,然后为mDatabase赋值为最新打开的数据库实例。既然有可能调用到getWritableDatabase()方法,我们就要看一下了:

复制代码
public synchronized SQLiteDatabase getWritableDatabase() {  
    if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {  
        // 如果mDatabase不为空已打开并且不是只读模式 则返回该实例  
        return mDatabase;  
    }  
  
    if (mIsInitializing) {  
        throw new IllegalStateException("getWritableDatabase called recursively");  
    }  
  
    // If we have a read-only database open, someone could be using it  
    // (though they shouldn't), which would cause a lock to be held on  
    // the file, and our attempts to open the database read-write would  
    // fail waiting for the file lock. To prevent that, we acquire the  
    // lock on the read-only database, which shuts out other users.  
  
    boolean success = false;  
    SQLiteDatabase db = null;  
    // 如果mDatabase不为空则加锁 阻止其他的操作  
    if (mDatabase != null)  
        mDatabase.lock();  
    try {  
        mIsInitializing = true;  
        if (mName == null) {  
            db = SQLiteDatabase.create(null);  
        } else {  
            // 打开或创建数据库  
            db = mContext.openOrCreateDatabase(mName, 0, mFactory);  
        }  
        // 获取数据库版本(如果刚创建的数据库,版本为0)  
        int version = db.getVersion();  
        // 比较版本(我们代码中的版本mNewVersion为1)  
        if (version != mNewVersion) {  
            db.beginTransaction();// 开始事务  
            try {  
                if (version == 0) {  
                    // 执行我们的onCreate方法  
                    onCreate(db);  
                } else {  
                    // 如果我们应用升级了mNewVersion为2,而原版本为1则执行onUpgrade方法  
                    onUpgrade(db, version, mNewVersion);  
                }  
                db.setVersion(mNewVersion);// 设置最新版本  
                db.setTransactionSuccessful();// 设置事务成功  
            } finally {  
                db.endTransaction();// 结束事务  
            }  
        }  
  
        onOpen(db);  
        success = true;  
        return db;// 返回可读写模式的数据库实例  
    } finally {  
        mIsInitializing = false;  
        if (success) {  
            // 打开成功  
            if (mDatabase != null) {  
                // 如果mDatabase有值则先关闭  
                try {  
                    mDatabase.close();  
                } catch (Exception e) {  
                }  
                mDatabase.unlock();// 解锁  
            }  
            mDatabase = db;// 赋值给mDatabase  
        } else {  
            // 打开失败的情况:解锁、关闭  
            if (mDatabase != null)  
                mDatabase.unlock();  
            if (db != null)  
                db.close();  
        }  
    }  
}
复制代码

大家可以看到,几个关键步骤是,首先判断mDatabase如果不为空已打开并不是只读模式则直接返回,否则如果mDatabase不为空则加锁,然后开始打开或创建数据库,比较版本,根据版本号来调用相应的方法,为数据库设置新版本号,最后释放旧的不为空的mDatabase并解锁,把新打开的数据库实例赋予mDatabase,并返回最新实例。

看完上面的过程之后,大家或许就清楚了许多,如果不是在遇到磁盘空间已满等情况,getReadableDatabase()一般都会返回和getWritableDatabase()一样的数据库实例,所以我们在DBManager构造方法中使用getWritableDatabase()获取整个应用所使用的数据库实例是可行的。当然如果你真的担心这种情况会发生,那么你可以先用getWritableDatabase()获取数据实例,如果遇到异常,再试图用getReadableDatabase()获取实例,当然这个时候你获取的实例只能读不能写了

最后,让我们看一下如何使用这些数据操作方法来显示数据,界面核心逻辑代码:

复制代码
public class SQLiteActivity extends Activity {

    public DiaryDao diaryDao;

    //因为getWritableDatabase内部调用了mContext.openOrCreateDatabase(mName, 0, mFactory);  
    //所以要确保context已初始化,我们可以把实例化Dao的步骤放在Activity的onCreate里
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        diaryDao = new DiaryDao(SQLiteActivity.this);
        initDatabase();
    }

    class ViewOcl implements View.OnClickListener {

        @Override
        public void onClick(View v) {

            String strSQL;
            boolean flag;
            String message;
            switch (v.getId()) {
            case R.id.btnAdd:
                String title = txtTitle.getText().toString().trim();
                String weather = txtWeather.getText().toString().trim();;
                String context = txtContext.getText().toString().trim();;
                String publish = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                        .format(new Date());
                // 动态组件SQL语句
                strSQL = "insert into diary values(null,'" + title + "','"
                        + weather + "','" + context + "','" + publish + "')";
                flag = diaryDao.execOther(strSQL);
                //返回信息
                message = flag?"添加成功":"添加失败";
                Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
                break;
            case R.id.btnDelete:
                strSQL = "delete from diary where tid = 1";
                flag = diaryDao.execOther(strSQL);
                //返回信息
                message = flag?"删除成功":"删除失败";
                Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
                break;
            case R.id.btnQuery:
                strSQL = "select * from diary order by publish desc";
                String data = diaryDao.execQuery(strSQL);
                Toast.makeText(getApplicationContext(), data, Toast.LENGTH_LONG).show();
                break;
            case R.id.btnUpdate:
                strSQL = "update diary set title = '测试标题1-1' where tid = 1";
                flag = diaryDao.execOther(strSQL);
                //返回信息
                message = flag?"更新成功":"更新失败";
                Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
                break;
            }
        }
    }

    private void initDatabase() {
        // 创建数据库对象
        SqliteDBHelper sqliteDBHelper = new SqliteDBHelper(SQLiteActivity.this);
        sqliteDBHelper.getWritableDatabase();
        System.out.println("数据库创建成功");
    }
}
复制代码

 

Android sqlite3数据库管理工具

Android SDK的tools目录下提供了一个sqlite3.exe工具,这是一个简单的sqlite数据库管理工具。开发者可以方便的使用其对sqlite数据库进行命令行的操作。

程序运行生成的*.db文件一般位于"/data/data/项目名(包括所处包名)/databases/*.db",因此要对数据库文件进行操作需要先找到数据库文件:

1、进入shell 命令

<span style="margin: 0px; padding: 0px; line-height: 1.5; font-family: 'courier new', courier; font-size: 14px;">adb shell</span>

2、找到数据库文件

<span style="margin: 0px; padding: 0px; line-height: 1.5; font-family: 'courier new', courier; font-size: 14px;">#cd data/data
#ls                --列出所有项目
#cd project_name   --进入所需项目名
#cd databases    
#ls                --列出现寸的数据库文件</span>

3、进入数据库

<span style="margin: 0px; padding: 0px; line-height: 1.5; font-family: 'courier new', courier; font-size: 14px;">#sqlite3 test_db   --进入所需数据库</span>

会出现类似如下字样:

<span style="margin: 0px; padding: 0px; line-height: 1.5; font-family: 'courier new', courier; font-size: 14px;">SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite></span>

至此,可对数据库进行sql操作。

4、sqlite常用命令

<span style="margin: 0px; padding: 0px; line-height: 1.5; font-family: 'courier new', courier; font-size: 14px;">>.databases        --产看当前数据库
>.tables           --查看当前数据库中的表
>.help             --sqlite3帮助
>.schema            --各个表的生成语句</span>

12. Sqlite的基本操作。

   最近开始在某个项目中实习,充分认识到了自己的不足,包括能力和性格等各种方面的缺陷。如何快速掌握开发环境,如何与其他程序员沟通交流,如何准确知道分配给自己的模块具体实现的功能等等,都是大问题,更重要的是,自己不能仅仅只是写代码而已,还要清楚自己的代码的应用环境,别人是怎样用的,自己应该提供哪些接口。这就属于扩展性的问题,不是一个新手能够马上明白的,但却是我们是否能够“脱农”的关键。

      废话不多说,本文讲诉的是我在项目中使用到的新知识---android数据库的操作。以前并没有任何关于android数据库的开发经历,所有的东西都是现学现用,所以特意总结一下。

      如果想要在android中使用数据库,使用SQLite是一个非常好的选择,因为它是android内置的数据库,提供了很多支持。

      数据库的使用无非就是CRUD,也就是"Create,Read,Update,Delete"这四个基本操作。

一.Create

      Create就是创建表,而要想创建表,首先必须要创建或者打开数据库。

      有两种方式可以做到这点:

1.手动创建或者打开数据库

 SQLiteDatabase database = openOrCreateDatabase("Student.db", MODE_PRIVATE, null);

     调用openOrCreateDatabase()方法,如果有该数据库,就打开,没有就创建一个。该方法的具体信息还是得看源码,但一般我们使用的时候,只要指定数据库的名字和指定该数据库是私有的就行。

2.使用SQLiteOpenHelper

复制代码
public class SQLHelper extends SQLiteOpenHelper {

    public SQLHelper(Context context, String name, CursorFactory factory,
                     int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {}

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}
复制代码

        然后我们再在Activity这样使用:

SQLHelper helper = new SQLHelper(this, "Student.db", null, 1);

        版本号(不能为负数)是为了方便以后升级数据库,由于是初版,版本号就是1。
        SQLiteOpenHelper是一个抽象的数据库操作类,首先执行的是OnCreate,这里我们可以执行创建表等动作,但该方法并没有真正创建数据库,创建数据库是在以下的情况:

SQLiteDatabase database = helper.getWritableDatabase();

        调用getWritableDatabase()或者getReadableDatabase()时,就会真正创建数据库。

        创建或打开数据库后,我们就可以建表。

        使用数据库并不是一件难事,难就难在如何建模。这里我们就只建三个简单的表:

        Teacher(teacherId, name, classId), Student(studentId,name,classId),Class(classId,className,studentId,teacherId),其中teacherId,studentId,classId分别是Teacher,Student和Class的主键。

        SQLite可以直接执行SQL语句,所以,我们可以这样建表:

 String CREATE_CLASS = "Create Table If Not Exists Class(classId integer primary key,"
                + "className varchar(100),"
                + "studentId integer References Student(studentId),"
                + "teacherId integer References Teacher(teacherId))";
 SQLiteDatabase db = helper.getWritableDatabase();
 db.execSQL(CREATE_CLASS);

        每次使用完数据库都要记得及时关闭数据库:      

 db.close();

        按照上面的方法,我们可以很快的建好三个表。

二.Updata

        更新这部分包括:插入,修改这两个动作。

        先讲最基本的动作:插入。

        我们现在要想将学生插入到班级中,像是这样:

 ClassInfoProvider provider = new ClassInfoProvider(this);
 Student student = new Student(2857, "郑文彪");
 ClassInfo classInfo = new ClassInfo("电信1班", 1);
 provider.addStudent(student, classInfo);

         这里我们有三个类:

复制代码
public class ClassInfo {
    private String name;
    private int id;

    public ClassInfo() {
    }

    public ClassInfo(String name, int id) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
  }
复制代码
复制代码
public class Teacher {
    private int teacherId;
    private String name;
    private String className;

    public Teacher() {
    }

    public Teacher(int teacherId, String name) {
        this.name = name;
        this.teacherId = teacherId;
    }

    public int getTeacherId() {
        return this.teacherId;
    }

    public void setTeacherId(int teacherId) {
        this.teacherId = teacherId;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClassName() {
        return this.className;
    }

    public void setClassName(String name) {
        this.className = name;
    }
}
复制代码
复制代码
public class Student {
    private int studendtd;
    private String name;
    private String className;

    public Student() {
    }

    public Student(int studentId, String name) {
        this.name = name;
        this.studentId = studentId;
    }

    public int getStudentId() {
        return this.studentId;
    }

    public void setStudendId(int studentId) {
        this.studentId = studendtd;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClassName() {
        return this.className;
    }

    public void setClassName(String name) {
        this.className = name;
    }
}
复制代码

       这三个类就是存放Student,Teacher和Class的基本信息。
       然后我们开始将学生插入到班级中:

复制代码
 public void addStudent(Student student, ClassInfo classInfo) {
        String INSERT_STUDENT_INTO_CLASS = "Insert Into Class(className, studentId, classId) Values('" + classInfo.getName() + "',"
                + student.getStudentId() + "," + classInfo.getId() + ")";
        String INSERT_STUDENT = "Insert Into Student(studentId, name, classId) Values(" + student.getStudentId() + ","
                + "'" + student.getName() + "'," + classInfo.getId() + ")";
        SQLiteDatabase db = helper.getWritableDatabase();
        db.execSQL(INSERT_STUDENT);
        db.execSQL(INSERT_STUDENT_INTO_CLASS);
        db.close();
    }
复制代码

       这是直接执行SQL语句的做法。
       SQLiteOpenHelper封装了一个insert方法可以方便我们执行插入行为:

复制代码
 SQLiteDatabase db = helper.getWritableDatabase();
 ContentValues values = new ContentValues();
 values.put("className", classInfo.getName());
 values.put("studentId", student.getStudentId());
 values.put("classId", classInfo.getId());
 db.insert("Class", null, values);
 ContentValues values1 = new ContentValues();
 values1.put("studentId", student.getStudentId());
 values1.put("name", student.getName());
 values1.put("classId", classInfo.getId());
 db.insert("Student", null, values1);
 db.close();
复制代码

       ContentValues其实就是一个字典Map,key值就是表中的列值,而value就是对应的字段。insert方法的第一个参数是要插入的表名,第二个参数就是相应的列,这里我们用null表示所有的列,然后就是我们要插入的字段。
       这样的方法确实可以简化我们的操作,至少不用在我们的代码中写那么长的SQL语句,容易犯错,又很烦,尤其是在插入的动作不断执行的时候。但有个地方值得注意:如果我们的数据库是提供接口方法给其他模块使用,而且以后要修改的人或者查看的人并不是我们自己,他们可能就必须知道这些方法的参数是什么,但直接执行SQL语句,只要他有数据库的基础知识,就会明白这是在干嘛,也知道如何修改。更糟糕的情况就是以后android的接口方法发生变化的话,那么,这些代码可能就会出现问题。当然,我们愿意相信他们不会修改接口,因为对接口的修改是一种错误的行为,尤其在接口已经发布的情况下。

       现在我们的表已经有数据了,如果我们想要修改的话,像是将学生的姓名进行更改,可以这样操作:

public void updateStudent(int id, String name) {
        SQLiteDatabase db = helper.getWritableDatabase();
        String UPDATE_STUDENT = "Update Student Set name =" + "'" + name + "' Where id=" + id + "";
        db.execSQL(UPDATE_STUDENT);
        db.close();
    }

       当然,我们同样可以简化:

 public void updateStudent(int id, String name) {
     SQLiteDatabase db = helper.getWritableDatabase();
     ContentValues values = new ContentValues();
     values.put("name", name);
     db.update("Student", values, "studentId=?", new String[]{id + ""});
db.close(); }

       update方法中,值得注意的是最后面两个参数,whereClause和whereArgs。whereClause表示要修改哪里,whereArgs表示修改的字段,无论我们是要修改一个字段还是一个以上,这里都需要一个String[]。

三.Read
      所谓的Read,就是一系列查询动作。

      我们要在Student这个表中查询名为"郑文彪"的学号,就是studentId:

复制代码
  public int getStudentId(String name) {
        int id = 0;
        String SELECT_STUDENTID = "Select studentId From Student Where name=?";
        SQLiteDatabase db = helper.getWritableDatabase();
        Cursor cursor = db.rawQuery(SELECT_STUDENTID, new String[]{name});
        if (cursor.moveToNext()) {
            id = cursor.getInt(0);
        }
        cursor.close();
db.close();
return id; }
复制代码

      这里我们需要利用光标Cursor。Cursor指向当前的数据记录,然后我们可以从光标中获取相应的数据。
四.Delete

       Delete包括表的删除,数据记录的删除。

       首先是数据记录的删除。现在我们想要删除姓名为"郑文彪"的学生的记录:

 public void deleteStudent(Student student) {
        SQLiteDatabase db = helper.getWritableDatabase();
        String DELETE_STUDENT = "Delete From Class Where studentId=?";
        db.execSQL(DELETE_STUDENT, new String[]{student.getStudentId() + ""});
        db.close();
    }

        然后这样调用该方法:

 Student student = new Student(2857, "郑文彪");
 ClassInfo classInfo = new ClassInfo("电信1班", 1);
 provider.addStudent(student, classInfo);
 provider.deleteStudent(student1);

       同样可以简化:

   db.delete("Class", "studentId=?", new String[]{student.getStudentId() + ""});

      接着是删除表,这个很简单:

String DROP_CLASS = "Drop Table Class";
SQLiteDatabase db = helper.getWritableDatabase();
db.execSQL(DROP_CLASS);

     在做测试的时候,由于需要经常运行,所以数据库中表的数据会非常冗余,尤其是将键值设为自增的时候。所以,我们需要在每次测试后删除表,这样下次测试的时候就不会受到影响。
五.结语

     SQLite完全是现学现做,所以了解得并不是很深,写得是一般,但还请各位能够指出不足之处。


13. Android中的MVC模式。

14. MergeViewStub的作用。

FrameLayout 先来看官方文档的定义:FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象 — 比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。 
我的理解是,把FrameLayout当作画布canvas,固定从屏幕的左上角开始填充图片,文字等。看看示例,原来可以利用 android:layout_gravity来设置位置的:

FrameLayout的布局结构

 


   

 

效果图

  


布局优化 使用tools里面的hierarchyviewer.bat来查看layout的层次。在启动模拟器启动所要分析的程序,再启动hierarchyviewer.bat,选择模拟器以及该程序,点击“Load View Hierarchy”,就会开始分析。可以save as png。 

<merge> 减少视图层级结构
从上图可以看到存在两个FrameLayout,红色框住的。如果能在layout文件中把FrameLayout声明去掉就可以进一步优化布局代码了。 但是由于布局代码需要外层容器容纳,如果
直接删除FrameLayout则该文件就不是合法的布局文件。这种情况下就可以使用<merge> 标签了。
修改为如下代码就可以消除多余的FrameLayout了:

复制代码
 1 01    <?xml
 2 02    version="1.0"
 3 03    encoding="utf-8"?>
 4 04     
 5 05    <merge xmlns:android="http://schemas.android.com/apk/res/android">
 6 06    <ImageView
 7 07    android:id="@+id/image"
 8 08    android:layout_width="fill_parent"
 9 09    android:layout_height="fill_parent"
10 10    android:scaleType="center"
11 11    android:src="@drawable/candle"
12 12    />
13 13    <TextView
14 14    android:id="@+id/text1"
15 15    android:layout_width="wrap_content"
16 16    android:layout_height="wrap_content"
17 17    android:layout_gravity="center"
18 18    android:textColor="#00ff00"
19 19    android:text="@string/hello"
20 20    />
21 21    <Button
22 22    android:id="@+id/start"
23 23    android:layout_width="wrap_content"
24 24    android:layout_height="wrap_content"
25 25    android:layout_gravity="bottom"
26 26    android:text="Start"
27 27    />
28 28    </merge>
复制代码

 


   

 

<merge>也有一些使用限制: 只能用于xml layout文件的根元素;在代码中使用LayoutInflater.Inflater()一个以merge为根元素的
布局文件时候,需要使用View inflate (int resource, ViewGroup root, boolean attachToRoot)指定一个ViewGroup 作为其容器,并且要设置attachToRoot 为true。

<include> 重用layout代码
如果在某个布局里面需要用到另一个相同的布局设计,可以通过<include> 标签来重用layout代码:

复制代码
 1 01    <?xml
 2 02    version="1.0"
 3 03    encoding="utf-8"?>
 4 04     
 5 05    <LinearLayout
 6 06    xmlns:android="http://schemas.android.com/apk/res/android"
 7 07    android:orientation="vertical"
 8 08    android:layout_width="fill_parent"
 9 09    android:layout_height="fill_parent">
10 10     
11 11    <include
12 12    android:id="@+id/layout1"
13 13    layout="@layout/relative"
14 14    />
15 15    <include
16 16    android:id="@+id/layout2"
17 17    layout="@layout/relative"
18 18    />
19 19    <include
20 20    android:id="@+id/layout3"
21 21    layout="@layout/relative"
22 22    />
23 23     
24 24    </LinearLayout>
25  
复制代码

 


效果图


这里要注意的是,"@layout/relative"不是引用Layout的id,而是引用res/layout/relative.xml,其内容是前面文章介绍RelativeLayout的布局代码。
另外,通过<include>,除了可以覆写id属性值,还可以修改其他属性值,例如android:layout_width,android:height等。

<viewstub> 延迟加载
(转自http://rainhomepage.appspot.com/2010/01/use-viewstub-to-optimize-the-layout-of)

ViewStub 是一个不可见的,大小为0的View,最佳用途就是实现View的延迟加载,在需要的时候再加载View,可Java中常见的性能优化方法延迟加载一样。

当调用ViewStub的setVisibility函数设置为可见或则调用 inflate初始化该View的时候,ViewStub引用的资源开始初始化,然后引用的资源替代ViewStub自己的位置填充在ViewStub的 位置。因此在没有调用setVisibility(int) 或则 inflate()函数之前 ViewStub一种存在组件树层级结构中,但是由于ViewStub非常轻量级,这对性能影响非常小。 可以通过ViewStub的inflatedId属

复制代码
1 1    <ViewStub
2 2    android:id="@+id/stub"
3 3     
4 4    android:inflatedId="@+id/subTree"
5 5    android:layout="@layout/mySubTree"
6 6    android:layout_width="120dip"
7 7    android:layout_height="40dip"
8 8    />
复制代码

 

性来重新定义引用的layout id。 例如:

 
   

 

上面定义的ViewStub ,可以通过id “stub”来找到,在初始化资源“mySubTree”后,stub从父组件中删除,然后"mySubTree"替代stub的位置。初始资源"mySubTree"得到的组件可以通过inflatedId 指定的id "subTree"引用。 然后初始化后的资源被填充到一个120dip宽、40dip高的地方。 

推荐使用下面的方式来初始化ViewStub:

1    ViewStub stub = (ViewStub) findViewById(R.id.stub);
2    View inflated = stub.inflate();

 


   

 

当调用inflate()函数的时候,ViewStub 被引用的资源替代,并且返回引用的view。 这样程序可以直接得到引用的view而不用再次调用函数 findViewById()来查找了。

ViewStub目前有个缺陷就是还不支持 <merge /> 标签。

layoutopt (Layout Optimization工具)
这工具可以分析所提供的Layout,并提供优化意见。在tools文件夹里面可以找到layoutopt.bat。
用法
layoutopt <list of xml files or directories>
参数
一个或多个的Layout xml文件,以空格间隔;或者多个Layout xml文件所在的文件夹路径
例子
layoutopt G:/StudyAndroid/UIDemo/res/layout/main.xml
layoutopt G:/StudyAndroid/UIDemo/res/layout/main.xml G:/StudyAndroid/UIDemo/res/layout/relative.xml
layoutopt G:/StudyAndroid/UIDemo/res/layout


15. Json有什么优劣势。

JSON(Javascript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于Javascript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, Javascript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。
JSON概念很简单,就是服务器直接生成Javascript语句,客户端获取后直接用eval方法来获得这个对象,这样就可以省去解析XML的性损失。

如要从后台载入信息,写成XML,如下:
<contact>
<friend>
<name>Michael</name>
<email>17bity@gmail.com</email>
<homepage>http://www.jialing.net</homepage>
</friend>
<friend>
<name>John</name>
<email>john@gmail.com</email>
<homepage>http://www.john.com</homepage>
</friend>
<friend>
<name>Peggy</name>
<email>peggy@gmail.com</email>
<homepage>http://www.peggy.com</homepage>
</friend>
</contact>


而写成JSON:

[
{
name:"Michael",
email:"17bity@gmail.com",
homepage:"http://www.jialing.net"
},
{
name:"John",
email:"john@gmail.com",
homepage:"http://www.jobn.com"
},
{
name:"Peggy",
email:"peggy@gmail.com",
homepage:"http://www.peggy.com"
}
]

简单的不只是表达上,最重要的是可以丢弃让人晕头转向的DOM解析了。因为只要符合Javascript的声明规范,JavaScrip会自动帮你解析 好 的。Ajax中使用JSON的基本方法是前台载入后台声明Javascript对象的字符串,用eval方法来将它转为实际的对象,最后通过 DHTML更新页面信息。

JSON不仅减少了解析XML解析带来的性能问题和兼容性问题,而且对于Javascript来说非常容易使用,可以方便的通过遍历数组以及访问对象属性 来获取数据,其可读性也不错,基本具备了结构化数据的性质。不得不说是一个很好的办法,而且事实上google maps就没有采用XML传递数据,而是采用了JSON方案。

JSON的另外一个优势是"跨域性",例如你在www.Web.cn的网页里使用



JSON的定义

    一种轻量级的数据交换格式,具有良好的可读和便于快速编写的特性。业内主流技术为其提供了完整的解决方案(有点类似于正则表达式 ,获得了当今大部分语言的支持),从而可以在不同平台间进行数据交换。JSON采用兼容性很高的文本格式,同时也具备类似于C语言体系的行为。

 

XML的定义

    扩展标记语言 (Extensible Markup Language, XML) ,用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 XML是标准通用标记语言 (SGML) 的子集,非常适合 Web 传输。XML 提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。

[  XML ]
使 用XML作为传输格式的优势:
1. 格式统一, 符合标准
2. 容易与其他系统进行远程交互, 数据共享比较方便

缺点:
1. XML文件格式文件庞大, 格式复杂, 传输占用带宽
2. 服务器端和客户端都需要花费大量代码来解析XML, 不论服务器端和客户端代码变的异常复杂和不容易维护
3. 客户端不同浏览器之间解析XML的方式不一致, 需要重复编写很多代码
4. 服务器端和客户端解析XML花费资源和时间


[ JSON ]
那么除了XML格式, 还有没有其他格式, 有一种叫做JSON (JavaScript Object Notation) 的轻量级数据交换格式能够替代XML的工作.

优点:
1. 数据格式比较简单, 易于读写, 格式都是压缩的, 占用带宽小
2. 易于解析这种语言, 客户端JavaScript可以简单的通过eval_r()进行JSON数据的读取
3. 支持多种语言, 包括ActionScript, C, C#, ColdFusion, Java, JavaScript, Perl, PHP, Python, Ruby等语言服务器端语言, 便于服务器端的解析
4. 在PHP世界, 已经有PHP-JSON和JSON-PHP出现了, 便于PHP序列化后的程序直接调用. PHP服务器端的对象、数组等能够直接生JSON格式, 便于客户端的访问提取.
5. 因为JSON格式能够直接为服务器端代码使用, 大大简化了服务器端和客户端的代码开发量, 但是完成的任务不变, 且易于维护

缺点:
1. 没有XML格式这么推广的深入人心和使用广泛, 没有XML那么通用性
2. JSON格式目前在Web Service中推广还属于初级阶段

JSON  XML 优缺点的比较

1.       在可读性方面,JSONXML的数据可读性基本相同。JSONXML的可读性可谓不相上下,一边是建议的语法,一边是规范的标签形式,很难分出胜负。

2.       在可扩展性方面,XML天生有很好的扩展性,JSON当然也有,没有什么是XML能扩展,JSON不能的。

3.       在编码难度方面,XML有丰富的编码工具,比如Dom4jJDom等,JSON也有json.org提供的工具,但是JSON的编码明显比XML容易许多,即使不借助工具也能写出JSON的代码,可是要写好XML就不太容易了。

4.       在解码难度方面,XML的解析得考虑子节点父节点,让人头昏眼花,而JSON的解析难度几乎为0。这一点XML输的真是没话说。

5.       在流行度方面,XML已经被业界广泛的使用,而JSON才刚刚开始,但是在Ajax这个特定的领域,未来的发展一定是XML让位于JSON。到时Ajax应该变成Ajaj(Asynchronous Javascript and JSON)了。

6.       JSONXML同样拥有丰富的解析手段。

7.       JSON相对于XML来讲,数据的体积小。

8.       JSONJavaScript的交互更加方便。

9.       JSON对数据的描述性比XML较差。

10.   JSON的速度要远远快于XML


  1.数据交换格式比较之关于XML和JSON:

  XML:extensible markup language,一种类似于HTML的语言,他没有预先定义的标签,使用DTD(document type definition)文档类型定义来组织数据;格式统一,跨平台和语言,早已成为业界公认的标准。具体的可以问Google或百度。相比之JSON这种轻量级的数据交换格式,XML可以称为重量级的了。

  JSON : JavaScript Object Notation 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScript Programming Language , Standard ECMA-262 3rd Edition - December 1999 的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。

  2.数据交换格式比较之关于轻量级和重量级:

  轻量级和重量级是相对来说的,那么XML相对于JSON的重量级体现在哪呢?我想应该体现在解析上,XML目前设计了两种解析方式:DOM和 SAX;

  DOM是把一个数据交换格式XML看成一个DOM对象,需要把XML文件整个读入内存,这一点上JSON和XML的原理是一样的,但是XML要考虑父节点和子节点,这一点上JSON的解析难度要小很多,因为JSON构建于两种结构:key/value,键值对的集合;值的有序集合,可理解为数组;

  SAX不需要整个读入文档就可以对解析出的内容进行处理,是一种逐步解析的方法。程序也可以随时终止解析。这样,一个大的文档就可以逐步的、一 点一点的展现出来,所以SAX适合于大规模的解析。这一点,JSON目前是做不到得。

  所以,JSON和XML的轻/重量级的区别在于:JSON只提供整体解析方案,而这种方法只在解析较少的数据时才能起到良好的效果;而XML提 供了对大规模数据的逐步解析方案,这种方案很适合于对大量数据的处理。

  3.数据交换格式比较之关于数据格式编码及解析的难度:

  在编码上,虽然XML和JSON都有各自的编码工具,但是JSON的编码要比XML简单,即使不借助工具,也可以写出JSON代码,但要写出好的XML代码就有点困难;与XML一样,JSON也是基于文本的,且它们都使用Unicode编码,且其与数据交换格式XML一样具有可读性。

  主观上来看,JSON更为清晰且冗余更少些。JSON网站提供了对JSON语法的严格描述,只是描述较简短。从总体来看,XML比较适合于标记 文档,而JSON却更适于进行数据交换处理。

  在解析上,在普通的web应用领域,开发者经常为XML的解析伤脑筋,无论是服务器端生成或处理XML,还是客户端用 JavaScript 解析XML,都常常导致复杂的代码,极低的开发效率。

  实际上,对于大多数web应用来说,他们根本不需要复杂的XML来传输数据,XML宣称的扩展性在此就很少具有优势;许多Ajax应用甚至直接返回HTML片段来构建动态web页面。和返回XML并解析它相比,返回HTML片段大大降低了系统的复杂性,但同时缺少了一定的灵活性。同XML或 HTML片段相比,数据交换格式JSON 提供了更好的简单性和灵活性。在web serivice应用中,至少就目前来说XML仍有不可动摇的地位。


16. 动画有哪两类,各有什么特点?

  android支持两种动画模式,tween animation,frame animation
View Animation(Tween Animation):补间动画,给出两个关键帧,通过一些算法将给定属性值在给定的时间内在两个关键帧间渐变。
  View animation只能应用于View对象,而且只支持一部分属性,这种实现方式可以使视图组件移动、放大、缩小以及产生透明度的变化.

另一种Frame动画,传统的动画方法,通过顺序的播放排列好的图片来实现,类似电影补间动画和帧动画。
补间动画和Frame动画的定义:
  所谓补间动画,是指通过指定View的初末状态和变化时间、方式,对View的内容完成一系列的图形变换来实现动画效果。主要包括四种效果:Alpha、Scale、Translate和Rotate。
帧动画就是Frame动画,即指定每一帧的内容和停留时间,然后播放动画。。

17. HandlerLoop消息队列模型,各部分的作用。

Android系统的消息队列和消息循环都是针对具体线程的,一个线程可以存在(当然也可以不存在)一个消息队列(Message Queue)和一个消息循环(Looper)。Android中除了UI线程(主线程),创建的工作线程默认是没有消息循环和消息队列的。如果想让该线程具有消息队列和消息循环,并具有消息处理机制,就需要在线程中首先调用Looper.prepare()来创建消息队列,然后调用Looper.loop()进入消息循环。如以下代码所示:

 

Java代码
  1. class LooperThread extends Thread {   
  2.       public Handler mHandler;   
  3.   
  4.       public void run() {   
  5.           Looper.prepare();   
  6.   
  7.           mHandler = new Handler() {   
  8.               public void handleMessage(Message msg) {   
  9.                   // process incoming messages here   
  10.               }   
  11.           };   
  12.   
  13.           Looper.loop();   
  14.       }   
  15.   }  
[java]  view plain  copy
  1. class LooperThread extends Thread {  
  2.       public Handler mHandler;  
  3.   
  4.       public void run() {  
  5.           Looper.prepare();  
  6.   
  7.           mHandler = new Handler() {  
  8.               public void handleMessage(Message msg) {  
  9.                   // process incoming messages here  
  10.               }  
  11.           };  
  12.   
  13.           Looper.loop();  
  14.       }  
  15.   }  

 

       这样该线程就具有了消息处理机制了。如果不调用Looper.prepare()来创建消息队列,会报"java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()"的错误。

 

       通过下图可以清晰显示出UI Thread, Worker Thread, Handler, Massage Queue, Looper之间的关系:

解释上图中的几个基本概念:

 

       1.Message

       消息对象,顾名思义就是记录消息信息的类。这个类有几个比较重要的字段:

       a.arg1和arg2:我们可以使用两个字段用来存放我们需要传递的整型值,在Service中,我们可以用来存放Service的ID。

       b.obj:该字段是Object类型,我们可以让该字段传递某个多项到消息的接受者中。

       c.what:这个字段可以说是消息的标志,在消息处理中,我们可以根据这个字段的不同的值进行不同的处理,类似于我们在处理Button事件时,通过switch(v.getId())判断是点击了哪个按钮。

       在使用Message时,我们可以通过new Message()创建一个Message实例,但是Android更推荐我们通过Message.obtain()或者Handler.obtainMessage()获取Message对象。这并不一定是直接创建一个新的实例,而是先从消息池中看有没有可用的Message实例,存在则直接取出并返回这个实例。反之如果消息池中没有可用的Message实例,则根据给定的参数new一个新Message对象。通过分析源码可得知,Android系统默认情况下在消息池中实例化10个Message对象。

 

       2.MessageQueue

       消息队列,用来存放Message对象的数据结构,按照“先进先出”的原则存放消息。存放并非实际意义的保存,而是将Message对象以链表的方式串联起来的。MessageQueue对象不需要我们自己创建,而是有Looper对象对其进行管理,一个线程最多只可以拥有一个MessageQueue。我们可以通过Looper.myQueue()获取当前线程中的MessageQueue。

 

       3.Looper

       MessageQueue的管理者,在一个线程中,如果存在Looper对象,则必定存在MessageQueue对象,并且只存在一个Looper对象和一个MessageQueue对象。倘若我们的线程中存在Looper对象,则我们可以通过Looper.myLooper()获取,此外我们还可以通过Looper.getMainLooper()获取当前应用系统中主线程的Looper对象。在这个地方有一点需要注意,假如Looper对象位于应用程序主线程中,则Looper.myLooper()和Looper.getMainLooper()获取的是同一个对象。

 

       4.Handler

       消息的处理者。通过Handler对象我们可以封装Message对象,然后通过sendMessage(msg)把Message对象添加到MessageQueue中;当MessageQueue循环到该Message时,就会调用该Message对象对应的handler对象的handleMessage()方法对其进行处理。由于是在handleMessage()方法中处理消息,因此我们应该编写一个类继承自Handler,然后在handleMessage()处理我们需要的操作。

 

       另外,我们知道,Android UI操作并不是线程安全的,所以无法在子线程中更新UI。但Andriod提供了几种方法,可以在子线程中通知UI线程更新界面:

 

  • Activity.runOnUiThread( Runnable )
  • View.post( Runnable )
  • View.postDelayed( Runnable, long )
  • Handler

       比较常用的是通过Handler,用Handler来接收子线程发送的数据,并用此数据配合主线程更新UI。那么,只要在主线程中创建Handler对象,在子线程中调用Handler的sendMessage方法,就会把消息放入主线程的消息队列,并且将会在Handler主线程中调用该handler的handleMessage方法来处理消息。 

 

Java代码  复制代码  收藏代码
  1. package com.superonion;   
  2.   
  3. import android.app.Activity;   
  4. import android.os.Bundle;   
  5. import android.os.Message;   
  6. import android.util.Log;   
  7. import android.os.Handler;   
  8.   
  9. public class MyHandler extends Activity {   
  10.     static final String TAG = "Handler";   
  11.     Handler h = new Handler(){   
  12.         public void handleMessage (Message msg)   
  13.         {   
  14.             switch(msg.what)   
  15.             {   
  16.             case HANDLER_TEST:   
  17.                 Log.d(TAG, "The handler thread id = " + Thread.currentThread().getId() + "\n");   
  18.                 break;   
  19.             }   
  20.         }   
  21.     };   
  22.   
  23.     static final int HANDLER_TEST = 1;   
  24.     /** Called when the activity is first created. */  
  25.     @Override  
  26.     public void onCreate(Bundle savedInstanceState) {   
  27.         super.onCreate(savedInstanceState);   
  28.         Log.d(TAG, "The main thread id = " + Thread.currentThread().getId() + "\n");   
  29.   
  30.         new myThread().start();   
  31.         setContentView(R.layout.main);   
  32.     }   
  33.   
  34.     class myThread extends Thread   
  35.     {   
  36.         public void run()   
  37.         {   
  38.             Message msg = new Message();   
  39.             msg.what = HANDLER_TEST;   
  40.             h.sendMessage(msg);   
  41.             Log.d(TAG, "The worker thread id = " + Thread.currentThread().getId() + "\n");   
  42.         }   
  43.     }   
  44. }  
[java]  view plain  copy
  1. package com.superonion;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.os.Message;  
  6. import android.util.Log;  
  7. import android.os.Handler;  
  8.   
  9. public class MyHandler extends Activity {  
  10.     static final String TAG = "Handler";  
  11.     Handler h = new Handler(){  
  12.         public void handleMessage (Message msg)  
  13.         {  
  14.             switch(msg.what)  
  15.             {  
  16.             case HANDLER_TEST:  
  17.                 Log.d(TAG, "The handler thread id = " + Thread.currentThread().getId() + "\n");  
  18.                 break;  
  19.             }  
  20.         }  
  21.     };  
  22.   
  23.     static final int HANDLER_TEST = 1;  
  24.     /** Called when the activity is first created. */  
  25.     @Override  
  26.     public void onCreate(Bundle savedInstanceState) {  
  27.         super.onCreate(savedInstanceState);  
  28.         Log.d(TAG, "The main thread id = " + Thread.currentThread().getId() + "\n");  
  29.   
  30.         new myThread().start();  
  31.         setContentView(R.layout.main);  
  32.     }  
  33.   
  34.     class myThread extends Thread  
  35.     {  
  36.         public void run()  
  37.         {  
  38.             Message msg = new Message();  
  39.             msg.what = HANDLER_TEST;  
  40.             h.sendMessage(msg);  
  41.             Log.d(TAG, "The worker thread id = " + Thread.currentThread().getId() + "\n");  
  42.         }  
  43.     }  
  44. }  

 

 

       以上代码中,Handler在主线程中创建后,子线程通过sendMessage()方法就可以将消息发送到主线程中,并在handleMessage()方法中处理。



18. 怎样退出终止App

最近两天为了解决Android上面退出程序问题折腾了半死,在google & baidu 上面找了很久、很久出来的完全千篇一律,说的方法有三,但是经过我试验后全部不行。

      三个方法分别是:

  1. killProcess, 这种方式当你kill后 Activity 会返回到上一个Activity
  2. Android Level 8(包含8)前使用一个API来操作,Level8以后又是另外一种,所以不能通用
  3. 使用 FLAG_ACTIVITY_CLEAR_TOP,从 A 到 B
下面介绍自己的方式:
大家都知道 Android 的 Activity 是存着历史栈的,比如从 A -> B -> C,C 完成 finish 后回到 B,把所有的Activity 都 finish了,程序就自然退出了。 当然在 finish 的同时也需要是否自己程序的其他资源。所以需要想个办法把 Activity 给存起来。然后在程序退出的地方调用它们的 finish()方法。
使用全局变量。对了,第一个想到的就是继承 Application,代码入下。
[java]  view plain  copy
  1. public class AgentApplication extends Application {  
  2.   
  3. private List<Activity> activities = new ArrayList<Activity>();  
  4.   
  5.     public void addActivity(Activity activity) {  
  6.         activities.add(activity);  
  7.     }  
  8.   
  9.     @Override  
  10.     public void onTerminate() {  
  11.         super.onTerminate();  
  12.           
  13.         for (Activity activity : activities) {  
  14.             activity.finish();  
  15.         }  
  16.           
  17.         onDestroy();  
  18.           
  19.         System.exit(0);  
  20.     }  
  21. }  

然后在 Activity  onCreate 的时候来调用  addActivity (),有人可能想到这个Application需要在所有的 Activity  onCreate的时候都使用,需要做一个单例实例。其实根本不需要。在 Activity 中使用  this.getApplication() 就可以了。
最后在你需要推出程序的地方调用 application.onTerminate() 就可以了。记住:super.onTerminate() 必须调用,代码中的 onDestroy()是我自己的释放其他资源的方法,不是系统的。

运行以上代码后,在LogCat 中会出现一行提示:
Process  包名 (pid  xxxxx)  has died.  证明你的程序退出了。现在你可以测试了。


19. Asset目录与res目录的区别。

*res/raw和assets的相同点:

1.两者目录下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制。


*res/raw和assets的不同点:
1.res/raw中的文件会被映射到R.java文件中,访问的时候直接使用资源ID即R.id.filename;assets文件夹下的文件不会被映射到R.java中,访问的时候需要AssetManager类。
2.res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹

*读取文件资源:

1.读取res/raw下的文件资源,通过以下方式获取输入流来进行写操作

  • InputStream is = getResources().openRawResource(R.id.filename);  

2.读取assets下的文件资源,通过以下方式获取输入流来进行写操作

  • AssetManager am = null;  
  • am = getAssets();  
  • InputStream is = am.open("filename");  

20. Android怎么加速启动Activity

21. Android内存优化方法:ListView优化,及时关闭资源,图片缓存等等。

22. Android中弱引用与软引用的应用场景。

如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

如果一个对象只具有弱引用,那么在垃圾回收器线程扫描的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

弱引用与软引用的根本区别在于:只具有弱引用的对象拥有更短暂的生命周期,可能随时被回收。而只具有软引用的对象只有当内存不够的时候才被回收,在内存足够的时候,通常不被回收。

在java.lang.ref包中提供了几个类:SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。ReferenceQueue类表示引用队列,它可以和这三种引用类联合使用,以便跟踪Java虚拟机回收所引用的对象的活动。

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

下面以使用软引用为例来详细说明。弱引用的使用方式与软引用是类似的。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。

首先定义一个HashMap,保存软引用对象。

复制代码 代码如下:

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再来定义一个方法,保存Bitmap的软引用到HashMap。
复制代码 代码如下:

 public void addBitmapToCache(String path) {

        // 强引用的Bitmap对象

        Bitmap bitmap = BitmapFactory.decodeFile(path);

        // 软引用的Bitmap对象

        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);

        // 添加该对象到Map中使其缓存

        imageCache.put(path, softBitmap);

    }


获取的时候,可以通过SoftReference的get()方法得到Bitmap对象。
复制代码 代码如下:

public Bitmap getBitmapByPath(String path) {

        // 从缓存中取软引用的Bitmap对象

        SoftReference<Bitmap> softBitmap = imageCache.get(path);

        // 判断是否存在软引用

        if (softBitmap == null) {

            return null;

        }

        // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空

        Bitmap bitmap = softBitmap.get();

        return bitmap;

    }


使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

需要注意的是,在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。


经验分享:

到底什么时候使用软引用,什么时候使用弱引用呢?

个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

另外,和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这种机制。


23. Bitmap的四中属性,与每种属性队形的大小。

我们知道Android系统分配给每个应用程序的内存是有限的,Bitmap作为消耗内存大户,我们对Bitmap的管理稍有不当就可能引发OutOfMemoryError,而Bitmap对象在不同的Android版本中存在一些差异,今天就给大家介绍下这些差异,并提供一些在使用Bitmap的需要注意的地方。

在Android2.3.3(API 10)及之前的版本中,Bitmap对象与其像素数据是分开存储的,Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)中或者说Derict Memory(直接内存)中,这使得存储在Native Memory中的像素数据的释放是不可预知的,我们可以调用recycle()方法来对Native Memory中的像素数据进行释放,前提是你可以清楚的确定Bitmap已不再使用了,如果你调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现Canvas: trying to use a recycled bitmap错误,而在Android3.0(API 11)之后,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做,也许你会问,为什么我在显示Bitmap对象的时候还是会出现OutOfMemoryError呢?

在说这个问题之前我顺便提一下,在Android2.2(API 8)之前,使用的是Serial垃圾收集器,从名字可以看出这是一个单线程的收集器,这里的”单线程的意思并不仅仅是使用一个CPU或者一条收集线程去收集垃圾,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,Android2.3之后,这种收集器就被代替了,使用的是并发的垃圾收集器,这意味着我们的垃圾收集线程和我们的工作线程互不影响。

简单的了解垃圾收集器之后,我们对上面的问题举一个简单的例子,假如系统启动了垃圾回收线程去收集垃圾,而此时我们一下子产生大量的Bitmap对象,此时是有可能会产生OutOfMemoryError,因为垃圾回收器首先要判断某个对象是否还存活(JAVA语言判断对象是否存活使用的是根搜索算法 GC Root Tracing),然后利用垃圾回收算法来对垃圾进行回收,不同的垃圾回收器具有不同的回收算法,这些都是需要时间的, 发生OutOfMemoryError的时候,我们要明确到底是因为内存泄露(Memory Leak)引发的还是内存溢出(Memory overflow)引发的,如果是内存泄露我们需要利用工具(比如MAT)查明内存泄露的代码并进行改正,如果不存在泄露,换句话来说就是内存中的对象确实还必须活着,那我们可以看看是否可以通过某种途径,减少对象对内存的消耗,比如我们在使用Bitmap的时候,应该根据View的大小利用BitmapFactory.Options计算合适的inSimpleSize来对Bitmap进行相对应的裁剪,以减少Bitmap对内存的使用,如果上面都做好了还是存在OutOfMemoryError(一般这种情况很少发生)的话,那我们只能调大Dalvik heap的大小了,在Android 3.1以及更高的版本中,我们可以在AndroidManifest.xml的application标签中增加一个值等于“true”的android:largeHeap属性来通知Dalvik虚拟机应用程序需要使用较大的Java Heap,但是我们也不鼓励这么做。



Bitmap:

(1)     public Bitmap (int width,int height,int stride,     PixelFormat format,IntPtr scan0)

用指定的大小、像素格式和像素数据初始化 Bitmap 类的新实例。

(2)     LockBits():,就是把图像的内存区域根据格式锁定,拿到那块内存的首地址。这样就可以直接改写这段内存了。这个方法的设计是挺好,可惜都是C++作为源泉来的,.NET Framework里面根本就不推荐用指针,需要用Marshal.Copy把内容Copy到一个byte数组里面,然后处理完了再Copy回去。

(3)     UnlockBits():从系统内存解锁此 Bitmap。

 

BitmapData:位图图像的属性

(1)   Height获取或设置 Bitmap 对象的像素高度。有时也称作扫描行数。

(2)   PixelFormat: 获取或设置返回此 BitmapData 对象的 Bitmap 对象中像素信息的格式。

(3)   Reserved: 保留。不要使用。

(4)   Scan0: 获取或设置位图中第一个像素数据的地址。它也可以看成是位图中的第一个扫描行。

(5)   Stride: 获取或设置 Bitmap 对象的跨距宽度(也称为扫描宽度)。

Stride:跨距是单行像素(一个扫描行)的宽度,舍入为一个 4 字节的边界。跨距总是大于或等于实际像素宽度。如果跨距为正,则位图自顶向下。如果跨距为负,则位图颠倒。Stride是指图像每一行需要占用的字节数。根据BMP格式的标准,Stride一定要是4的倍数。据个例子,一幅1024*768的24bppRgb的图像,每行有效的像素信息应该是1024*3 = 3072。因为已经是4的倍数,所以Stride就是3072。那么如果这幅图像是35*30,那么一行的有效像素信息是105,但是105不是4的倍数,所以填充空字节,Stride应该是108。这一行计算出来的offset就是3。一要注意必须是4的倍数,二单位是字节!

(6)   Width: 获取或设置 Bitmap 对象的像素宽度。这也可以看作是一个扫描行中的像素数。

 

PixelFormat:

(1)     Format24bppRgb,也就是24位色。在这种格式下3个字节表示一种颜色,也就是我们通常所知道的R,G,B,所以每个字节表示颜色的一个分量。

(2)     Format32bppArgb,除了RGB,在图像中还存在一个通道,叫做A。这个A就是用来描述当前像素是透明,半透明,还是全透明的分量。这个通道是2个叫Catmull和Smith在上世纪70年代初发明的。通过这个分量,我们可以进行alpha混合的一些计算。从而使表面的图像和背景图像混合,从而造成透明半透明的效果。在这种格式下A作为一个byte,取值可以从0到255,那么0表示图像完全透明,则完全不可见,255则表示图像完全不透明。每个像素都可以实现这种透明或者半透明的效果。更详细解释可以参考http://en.wikipedia.org/wiki/Alpha_compositing,或者去买本数字图像处理的书回来看。

(3)     Format32bppPArgb,这叫做premultiplied alpha,就是说在RGB分量里面,alpha分量的数据已经被预先乘进去了。比如说,一个半透明的红色点,在ARGB下,矢量是(255,0,0,128),而在PARGB下就变成了(128,0,0,128)。这是为了不要每次都做乘法。

(4)     Bitmap保存成为一个文件,那么必须用png格式,才能够保存alpha通道的信息。如果你存为JPG/BMP/GIF,那么alpha通道的信息将会被丢失。如果存为BMP,那么文件格式将变成Format32bppRgb,其中1个字节不再使用;如果保存为JPEG,那么是Format24bppRgb;存为GIF,格式将变成Format8bppIndexed。根据标准,BMP/JPG本来就不支持透明通道,所以没有可能保留透明信息。GIF倒是支持透明,但是GIF中颜色的信息都是索引,所以Alpha的解释对GIF完全没有效果,

 

BitmapInfoHeader:

biHeight:说明图象的高度,以象素为单位。

    如果该值是一个正数,说明BtimapBottom up DIB,起始点是左下角,也就是从图像的最下面一行扫描,位图数组中得到的第一行数据实际是图形的最下面的一行。图像是倒向的;

    如果该值是一个负数,则说明图像是TopDown DIB,起始点是左上角,图像从最上面一行扫描,图像正向的。

   大多数的BMP文件都是倒向的位图,也就是时,高度值是一个正数。(注:当高度值是一个负数时(正向图像),图像将不能被压缩(也就是说biCompression成员将不能是BI_RLE8BI_RLE4


24. ViewView Group分类。自定义View过程:onMeasure()onLayout()onDraw()

25. Touch事件分发机制。

http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html

Android 编程下 Touch 事件的分发和消费机制

Android 中与 Touch 事件相关的方法包括:dispatchTouchEvent(MotionEvent ev)onInterceptTouchEvent(MotionEvent ev)onTouchEvent(MotionEvent ev);能够响应这些方法的控件包括:ViewGroup 及其子类Activity。方法与控件的对应关系如下表所示:

Touch 事件相关方法   方法功能  
  ViewGroup   
     Activity     
  public boolean dispatchTouchEvent(MotionEvent ev) 事件分发 
 Yes  Yes
  public boolean onInterceptTouchEvent(MotionEvent ev)  
事件拦截 
 Yes  No
  public boolean onTouchEvent(MotionEvent ev) 事件响应 
 Yes  Yes

从这张表中我们可以看到 ViewGroup 及其子类对与 Touch 事件相关的三个方法均能响应,而 Activity 对 onInterceptTouchEvent(MotionEvent ev) 也就是事件拦截不进行响应。另外需要注意的是 View 对dispatchTouchEvent(MotionEvent ev) 和 onInterceptTouchEvent(MotionEvent ev) 的响应的前提是可以向该 View 中添加子 View,如果当前的 View 已经是一个最小的单元 View(比如 TextView),那么就无法向这个最小 View 中添加子 View,也就无法向子 View 进行事件的分发和拦截,所以它没有 dispatchTouchEvent(MotionEvent ev) 和 onInterceptTouchEvent(MotionEvent ev),只有 onTouchEvent(MotionEvent ev)

一、Touch 事件分析

▐ 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)

Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。dispatchTouchEvent 的事件分发逻辑如下:

  • 如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;
  • 如果 return false,事件分发分为两种情况:
  1. 如果当前 View 获取的事件直接来自 Activity,则会将事件返回给 Activity 的 onTouchEvent 进行消费;
  2. 如果当前 View 获取的事件来自外层父控件,则会将事件返回给父 View 的  onTouchEvent 进行消费。
  • 如果返回系统默认的 super.dispatchTouchEvent(ev),事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。

▐ 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev) 

外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:

  • 如果 onInterceptTouchEvent 返回 true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
  • 如果 onInterceptTouchEvent 返回 false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;
  • 如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。

▐ 事件响应:public boolean onTouchEvent(MotionEvent ev)

在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:

  • 如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。
  • 如果返回了 true 则会接收并消费该事件。
  • 如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。

到这里,与 Touch 事件相关的三个方法就分析完毕了。下面的内容会通过各种不同的的测试案例来验证上文中三个方法对事件的处理逻辑。

二、Touch 案例介绍

同样在开始进行案例分析之前,我需要说明测试案例的结构,因为所有的测试都是针对这一个案例来进行的,测试中只是通过修改每个控件中与 Touch 事件相关的三个方法的返回值来体现不同的情况。先来看张图:

Touch 事件案例布局 UI

上面的图为测试案例的布局文件 UI 显示效果,布局文件代码如下:

复制代码
<?xml version="1.0" encoding="utf-8"?>
<cn.sunzn.tevent.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#468AD7"
    android:gravity="center"
    android:orientation="vertical" >

    <cn.sunzn.tevent.TouchEventChilds
        android:id="@+id/childs"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:background="#E1110D"
        android:text="@string/hello" />

</cn.sunzn.tevent.TouchEventFather>
复制代码

蓝色背景为一个自定义控件 TouchEventFather,该控件为外层 View,继承自 LinearLayout,实现代码如下:

复制代码
package cn.sunzn.tevent;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

public class TouchEventFather extends LinearLayout {

    public TouchEventFather(Context context) {
        super(context);
    }

    public TouchEventFather(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("sunzn", "TouchEventFather | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);
    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("sunzn", "TouchEventFather | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onInterceptTouchEvent(ev);
    }

    public boolean onTouchEvent(MotionEvent ev) {
        Log.d("sunzn", "TouchEventFather | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onTouchEvent(ev);
    }

}
复制代码

红色背景为一个自定义控件 TouchEventChilds,该控件为内层 View,为 TouchEventFather 的子 View,同样继承自 LinearLayout,实现代码如下:

复制代码
package cn.sunzn.tevent;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

public class TouchEventChilds extends LinearLayout {

    public TouchEventChilds(Context context) {
        super(context);
    }

    public TouchEventChilds(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("sunzn", "TouchEventChilds | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);
    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("sunzn", "TouchEventChilds | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onInterceptTouchEvent(ev);
    }

    public boolean onTouchEvent(MotionEvent ev) {
        Log.d("sunzn", "TouchEventChilds | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onTouchEvent(ev);
    }

}
复制代码

接着实现 Activity 的代码,因为控件所有的事件都是通过 Activity 的 dispatchTouchEvent 进行分发的;除此之外还需要重写 Activity 的 onTouchEvent 方法,这是因为如果一个控件直接从 Activity 获取到事件,这个事件会首先被传递到控件的 dispatchTouchEvent 方法,如果这个方法 return false,事件会以冒泡方式返回给 Activity 的 onTouchEvent 进行消费。实现代码如下:

复制代码
package cn.sunzn.tevent;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;

public class TouchEventActivity extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("sunzn", "TouchEventActivity | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);
    }

    public boolean onTouchEvent(MotionEvent event) {
        Log.w("sunzn", "TouchEventActivity | onTouchEvent --> " + TouchEventUtil.getTouchAction(event.getAction()));
        return super.onTouchEvent(event);
    }

}
复制代码

最后再附上 TouchEventUtil 的代码,TouchEventUtil 中并没有做多少事情,只是将以上 2 个自定义控件中各个方法的 MotionEvent 集中到一个工具类中并将其对应的动作以 String 形式返回,这样处理更便于实时观察控件的事件。代码如下:

复制代码
package cn.sunzn.tevent;

import android.view.MotionEvent;

public class TouchEventUtil {
    
    public static String getTouchAction(int actionId) {
        String actionName = "Unknow:id=" + actionId;
        switch (actionId) {
        case MotionEvent.ACTION_DOWN:
            actionName = "ACTION_DOWN";
            break;
        case MotionEvent.ACTION_MOVE:
            actionName = "ACTION_MOVE";
            break;
        case MotionEvent.ACTION_UP:
            actionName = "ACTION_UP";
            break;
        case MotionEvent.ACTION_CANCEL:
            actionName = "ACTION_CANCEL";
            break;
        case MotionEvent.ACTION_OUTSIDE:
            actionName = "ACTION_OUTSIDE";
            break;
        }
        return actionName;
    }
    
}
复制代码

三、Touch 案例分析

 Case 1 

拦截条件
控件名称 dispatchTouchEvent 返回值 onInterceptTouchEvent 返回值 onTouchEvent 返回值
TouchEventActivity super.dispatchTouchEvent(ev) --- super.onTouchEvent(ev)
TouchEventFather false super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
TouchEventChilds super.dispatchTouchEvent(ev) super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
运行结果
Level Time PID Application Tag  Text
 W  05-10 03:41:19.743  414 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_DOWN
 E  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_DOWN
 W  05-10 03:41:19.743  414 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_DOWN
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_UP
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_UP
结果分析
代码运行后,事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的dispatchTouchEvent,而该控件的 dispatchTouchEvent 返回 false,表示对获取到的事件停止向下传递,同时也不对该事件进行消费。由于 TouchEventFather 获取的事件直接来自 TouchEventActivity ,则会将事件返回给 TouchEventActivity  的 onTouchEvent 进行消费,最后直接由 TouchEventActivity 来响应手指移动和抬起事件。

  Case 2

拦截条件
控件名称 dispatchTouchEvent 返回值 onInterceptTouchEvent 返回值 onTouchEvent 返回值
TouchEventActivity super.dispatchTouchEvent(ev) --- super.onTouchEvent(ev)
TouchEventFather true super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
TouchEventChilds super.dispatchTouchEvent(ev) super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
运行结果
Level Time PID Application Tag  Text
 W  05-10 03:41:19.743  414 cn.sunzn.tevent  sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_DOWN
 E  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_DOWN
 W  05-10 03:41:19.743  414 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
 E  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
 E  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
 E  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_MOVE
 W  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_UP
E  05-10 03:41:19.743  414  cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_UP
结果分析
代码运行后,事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent,而该控件的 dispatchTouchEvent 返回 true,表示分发事件到 TouchEventFather 控件并由该控件的 dispatchTouchEvent 进行消费;TouchEventActivity 不断的分发事件到 TouchEventFather 控件的dispatchTouchEvent,而 TouchEventFather 控件的 dispatchTouchEvent 也不断的将获取到的事件进行消费。

 Case 3

拦截条件
控件名称 dispatchTouchEvent 返回值 onInterceptTouchEvent 返回值 onTouchEvent 返回值
TouchEventActivity super.dispatchTouchEvent(ev) --- super.onTouchEvent(ev)
TouchEventFather super.dispatchTouchEvent(ev) true super.onTouchEvent(ev)
TouchEventChilds super.dispatchTouchEvent(ev) super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
运行结果
Level Time PID Application Tag  Text
W 05-10 05:34:46.333 481 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_DOWN
E 05-10 05:34:46.333 481 cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_DOWN
I 05-10 05:34:46.333 481 cn.sunzn.tevent sunzn   TouchEventFather | onInterceptTouchEvent --> ACTION_DOWN
D 05-10 05:34:46.333 481 cn.sunzn.tevent sunzn   TouchEventFather | onTouchEvent --> ACTION_DOWN
W 05-10 05:34:46.333 481 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_DOWN
W 05-10 05:34:46.343 481 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
W 05-10 05:34:46.343 481 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
W 05-10 05:34:46.423 481 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
W 05-10 05:34:46.423 481 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
W 05-10 05:34:46.433 481 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
W 05-10 05:34:46.433 481 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
W 05-10 05:34:46.442 481 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_UP
W 05-10 05:34:46.442 481 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_UP
结果分析
代码运行后,事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent,而该控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示对事件进行分发并向下传递给 TouchEventFather 控件的 onInterceptTouchEvent 方法,该方法返回 true 表示对所获取到的事件进行拦截并将事件传递给 TouchEventFather 控件的 onTouchEvent 进行处理,TouchEventFather 控件的 onTouchEvent 返回 super.onTouchEvent(ev) 表示对事件没有做任何处理直接将事件返回给上级控件,由于 TouchEventFather 获取的事件直接来自 TouchEventActivity,所以 TouchEventFather 控件的 onTouchEvent 会将事件以冒泡方式直接返回给 TouchEventActivity 的 onTouchEvent 进行消费,后续的事件则会跳过 TouchEventFather 直接由 TouchEventActivity 的 onTouchEvent 消费来自 TouchEventActivity 自身分发的事件。

 Case 4

拦截条件
控件名称 dispatchTouchEvent 返回值 onInterceptTouchEvent 返回值 onTouchEvent 返回值
TouchEventActivity super.dispatchTouchEvent(ev) --- super.onTouchEvent(ev)
TouchEventFather super.dispatchTouchEvent(ev) false super.onTouchEvent(ev)
TouchEventChilds super.dispatchTouchEvent(ev) super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
运行结果
Level Time PID Application Tag  Text
W 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_DOWN
E 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_DOWN
I 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventFather | onInterceptTouchEvent --> ACTION_DOWN
E 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventChilds | dispatchTouchEvent --> ACTION_DOWN
I 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventChilds | onInterceptTouchEvent --> ACTION_DOWN
D 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventChilds | onTouchEvent --> ACTION_DOWN
D 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventFather | onTouchEvent --> ACTION_DOWN
W 05-10 06:31:47.565 512 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_DOWN
W 05-10 06:31:47.652 512 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
W 05-10 06:31:47.652 512 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
W 05-10 06:31:47.732 512 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
W 05-10 06:31:47.732 512 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
W 05-10 06:31:47.812 512 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
W 05-10 06:31:47.812 512 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_MOVE
W 05-10 06:31:47.892 512 cn.sunzn.tevent sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_UP
W 05-10 06:31:47.892 512 cn.sunzn.tevent sunzn   TouchEventActivity | onTouchEvent --> ACTION_UP
结果分析
代码运行后,事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent,而该控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示对事件进行分发并向下传递给 TouchEventFather 控件的 onInterceptTouchEvent 方法,该方法返回 false 表示事件会被放行并传递到子控件 TouchEventChilds 的 dispatchTouchEvent 方法,同样 TouchEventChilds 的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示对事件进行分发并向下传递给 TouchEventChilds 控件的 onInterceptTouchEvent 方法TouchEventChilds 的 onInterceptTouchEvent 方法返回 super.onInterceptTouchEvent(ev) 默认会将事件传递给 TouchEventChilds 的 onTouchEvent 进行处理,TouchEventChilds 的 onTouchEvent 返回 super.onTouchEvent(ev) 表示对事件没有做任何处理直接将事件返回给上级控件,由于 TouchEventChilds 获取的事件直接来自 TouchEventFather,所以 TouchEventChilds 控件的 onTouchEvent 会将事件以冒泡方式直接返回给 TouchEventFather 的 onTouchEvent 进行消费,而 TouchEventFather 的 onTouchEvent 也返回了 super.onTouchEvent(ev),同样 TouchEventFather 的 onTouchEvent 也会将事件返回给上级控件,而 TouchEventFather 获取的事件直接来自 TouchEventActivity,所以 TouchEventFather 控件的 onTouchEvent 会将事件以冒泡方式直接返回给 TouchEventActivity 的 onTouchEvent 进行消费,后续的事件则会跳过 TouchEventFather 和 TouchEventChilds 直接由 TouchEventActivity 的 onTouchEvent 消费来自 TouchEventActivity 自身分发的事件。

 Case 5

拦截条件
控件名称 dispatchTouchEvent 返回值 onInterceptTouchEvent 返回值 onTouchEvent 返回值
TouchEventActivity super.dispatchTouchEvent(ev) --- super.onTouchEvent(ev)
TouchEventFather super.dispatchTouchEvent(ev) false super.onTouchEvent(ev)
TouchEventChilds true super.onInterceptTouchEvent(ev) super.onTouchEvent(ev)
运行结果
Level Time PID Application Tag  Text
 W  05-10 08:11:18.403 574  cn.sunzn.tevent  sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_DOWN
 E  05-10 08:11:18.403 574  cn.sunzn.tevent  sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_DOWN
 I  05-10 08:11:18.403 574  cn.sunzn.tevent  sunzn   TouchEventFather | onInterceptTouchEvent --> ACTION_DOWN
 E  05-10 08:11:18.403 574  cn.sunzn.tevent  sunzn   TouchEventChilds | dispatchTouchEvent --> ACTION_DOWN
 W  05-10 08:11:19.652 574  cn.sunzn.tevent  sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_MOVE
 E  05-10 08:11:19.652 574  cn.sunzn.tevent  sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_MOVE
 I  05-10 08:11:19.652 574  cn.sunzn.tevent  sunzn   TouchEventFather | onInterceptTouchEvent --> ACTION_MOVE
E  05-10 08:11:19.652 574 cn.sunzn.tevent sunzn   TouchEventChilds | dispatchTouchEvent --> ACTION_MOVE
 W  05-10 08:11:19.872 574 cn.sunzn.tevent  sunzn   TouchEventActivity | dispatchTouchEvent --> ACTION_UP
E 05-10 08:11:19.872 574 cn.sunzn.tevent sunzn   TouchEventFather | dispatchTouchEvent --> ACTION_UP
I 05-10 08:11:19.872 574 cn.sunzn.tevent sunzn   TouchEventFather | onInterceptTouchEvent --> ACTION_UP
 E  05-10 08:11:19.872 574 cn.sunzn.tevent  sunzn   TouchEventChilds | dispatchTouchEvent --> ACTION_UP
结果分析
代码运行后,事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent,该控件的 dispatchTouchEvent 返回super.dispatchTouchEvent(ev)事件会分发到 TouchEventFather 的 onInterceptTouchEvent,onInterceptTouchEvent 返回 false 表示放行当先事件;事件会被传递到子控件 TouchEventChilds 的 dispatchTouchEvent 方法,dispatchTouchEvent 返回 true 表示事件被分发到 TouchEventChilds 控件并由该控件的 dispatchTouchEvent 方法消费。后续的事件也会不断的重复上面的逻辑最终被 TouchEventChilds 的 dispatchTouchEvent 消费。


26. Android长连接,怎么处理心跳机制。

27. Zygote的启动过程。

 这样,Zygote进程就启动完成了,学习到这里,我们终于都对Android系统中的进程有了一个深刻的认识了,这里总结一下:

        1. 系统启动时init进程会创建Zygote进程,Zygote进程负责后续Android应用程序框架层的其它进程的创建和启动工作。

        2. Zygote进程会首先创建一个SystemServer进程,SystemServer进程负责启动系统的关键服务,如包管理服务PackageManagerService和应用程序组件管理服务ActivityManagerService。

        3. 当我们需要启动一个Android应用程序时,ActivityManagerService会通过Socket进程间通信机制,通知Zygote进程为这个应用程序创建一个新的进程。


28. Android IPC:Binder原理。

Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有的进程间通信IPC手段包括(Internet Process Connection): 管道(Pipe)、信号(Signal)和跟踪(Trace)、插口(Socket)、报文队列(Message)、共享内存(Share Memory)和信号量(Semaphore)。本文详细介绍Binder作为Android主要IPC方式的优势。

 

一、引言

基于Client-Server的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是Android系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,诸如媒体播放,视音频频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的Server负责管理,应用程序只需做为Client与这些Server建立连接便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能。Client-Server方式的广泛采用对进程间通信(IPC)机制是一个挑战。目前linux支持的IPC包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及socket中只有socket支持Client-Server的通信方式。当然也可以在这些底层机制上架设一套协议来实现Client-Server通信但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。

另一方面是传输性能。socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。

表 1 各种IPC方式数据拷贝次数

IPC

数据拷贝次数

共享内存

0

Binder

1

Socket/管道/消息队列

2

 

还有一点是出于安全性考虑。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID和PID(用户ID进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID和PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,systemV的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

 

二、面向对象的 Binder IPC

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问server。在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端Server中,而后者实体位于本地内存中。‘指针’是C++的术语,而更通常的说法是引用,即Client通过Binder的引用访问Server。而软件领域另一个术语‘句柄’也可以用来表述Binder在Client中的存在方式。从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。本文中会使用‘引用’或‘句柄’这个两广泛使用的术语。

面向对象思想的引入将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的Binder对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是Binder在英文里的原意。

当然面向对象只是针对应用程序而言,对于Binder驱动和内核其它模块一样使用C语言实现,没有类和对象的概念。Binder驱动为面向对象的进程间通信提供底层支持。

 

三、Binder 通信模型

Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

 

3.1 Binder 驱动

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的。它工作于内核态,驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

3.2 ServiceManager 与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

细心的读者可能会发现其中的蹊跷:SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:SMgr和其它进程同样采用Binder通信,SMgr是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。SMgr提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向SMgr注册自己Binder就必需通过0这个引用号和SMgr的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对SMgr而言的,一个应用程序可能是个提供服务的Server,但对SMgr来说它仍然是个Client。

3.3 Client 获得实名Binder的引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,SMgr象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。

3.4 匿名 Binder

并不是所有Binder都需要注册给SMgr广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向SMgr注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。

 

四、 Binder协议(略)

 

五、Binder表述

 

六、Binder 内存映射和接收缓存区管理

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。我们注意到Binder驱动实现了mmap()系统调用,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。在介绍Binder协议时已经提到,这是由命令BC_FREE_BUFFER完成的。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

 

七、Binder 接收线程管理

Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,提供Binder实体,线程T1从Client进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。在这过程中,T2仿佛T1在进程S中的代理,代表T1执行远程任务,而给T1的感觉就是象穿越到S中执行一段代码又回到了C1。为了使这种穿越更加真实,驱动会将T1的一些属性赋给T2,特别是T1的优先级nice,这样T2会使用和T1类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来T2除了和T1优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。

对于Server进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求,即阻塞在accept()上。这个socket就象一只会生蛋的鸡,一旦收到来自Client的请求就会生一个蛋 – 创建新socket并从accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client实现交互。

可是对于Binder来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder。这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是Binder协议引入了专门命令或消息帮助用户管理线程池,包括:

· INDER_SET_MAX_THREADS

· BC_REGISTER_LOOP

· BC_ENTER_LOOP

· BC_EXIT_LOOP

· BR_SPAWN_LOOPER

首先要管理线程池就要知道池子有多大,应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder驱动还做了一点小小的优化。当进程P1的线程T1向进程P2发送请求时,驱动会先查看一下线程T1是否也正在处理来自P2某个线程请求但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有Binder实体并互相对发时请求时。假如驱动在进程P2中发现了这样的线程,比如说T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发送了请求尚未得到返回包,说明T2肯定(或将会)阻塞在读取返回包的状态。这时候可以让T2顺便做点事情,总比等在那里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。

 

八、数据包接收队列与(线程)等待队列管理

通常数据传输的接收端有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。当超市里的进货(数据包)太多,货物会堆积在仓库里;购物的人(线程)太多,会排队等待在收银台,道理是一样的。在驱动中,每个进程有一个全局的接收队列,也叫to-do队列,存放不是发往特定线程的数据包;相应地有一个全局等待队列,所有等待从全局接收队列里收数据的线程在该队列里排队。每个线程有自己私有的to-do队列,存放发送给该线程的数据包;相应的每个线程都有各自私有等待队列,专门用于本线程等待接收自己to-do队列里的数据。虽然名叫队列,其实线程私有等待队列中最多只有一个线程,即它自己。

由于发送时没有特别标记,驱动怎么判断哪些数据包该送入全局to-do队列,哪些数据包该送入特定线程的to-do队列呢?这里有两条规则。规则1:Client发给Server的请求数据包都提交到Server进程的全局to-do队列。不过有个特例,就是上节谈到的Binder对工作线程启动的优化。经过优化,来自T1的请求不是提交给P2的全局to-do队列,而是送入了T2的私有to-do队列。规则2:对同步请求的返回数据包(由BC_REPLY发送的包)都发送到发起请求的线程的私有to-do队列中。如上面的例子,如果进程P1的线程T1发给进程P2的线程T2的是同步请求,那么T2返回的数据包将送进T1的私有to-do队列而不会提交到P1的全局to-do队列。

数据包进入接收队列的潜规则也就决定了线程进入等待队列的潜规则,即一个线程只要不接收返回数据包则应该在全局等待队列中等待新任务,否则就应该在其私有等待队列中等待Server的返回数据。还是上面的例子,T1在向T2发送同步请求后就必须等待在它私有等待队列中,而不是在P1的全局等待队列中排队,否则将得不到T2的返回的数据包。

这些潜规则是驱动对Binder通信双方施加的限制条件,体现在应用程序上就是同步请求交互过程中的线程一致性:1) Client端,等待返回包的线程必须是发送请求的线程,而不能由一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包;2) Server端,发送对应返回数据包的线程必须是收到请求数据包的线程,否则返回的数据包将无法送交发送请求的线程。这是因为返回数据包的目的Binder不是用户指定的,而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程不是收到请求包的线程驱动将无从知晓返回包将送往何处。

接下来探讨一下Binder驱动是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端(client)在发出请求数据包后须要等待应答端(Server)的返回数据包,而异步交互的发送端发出请求数据包后交互即结束。对于这两种交互的请求数据包,驱动可以不管三七二十一,统统丢到接收端的to-do队列中一个个处理。但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:对于某个Binder实体,只要有一个异步交互没有处理完毕,例如正在被某个线程处理或还在任意一条to-do队列中排队,那么接下来发给该实体的异步交互包将不再投递到to-do队列中,而是阻塞在驱动为该实体开辟的异步交互接收队列(Binder节点的async_todo域)中,但这期间同步交互依旧不受限制直接进入to-do队列获得处理。一直到该异步交互处理完毕下一个异步交互方可以脱离异步交互队列进入to-do队列中。之所以要这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完毕以免影响请求端的响应速度,而异步交互属于‘发射后不管’,稍微延时一点不会阻塞其它线程。所以用专门队列将过多的异步交互暂存起来,以免突发大量异步交互挤占Server端的处理能力或耗尽线程池里的线程,进而阻塞同步交互。

 

九、总结

Binder使用Client-Server通信方式,安全性好,简单高效,再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,成为Android进程间通信的中流砥柱。



29. 你用过什么框架,是否看过源码,是否知道底层原理。

自己项目中一直都是用的开源的xUtils框架,包括BitmapUtils、DbUtils、ViewUtils和HttpUtils四大模块,这四大模块都是项目中比较常用的。最近决定研究一下xUtils的源码,用了这么久总得知道它的实现原理吧。我是先从先从BitmapUtils模块开始的。BitmapUtils和大多数图片加载框架一样,都是基于内存-文件-网络三级缓存。也就是加载图片的时候首先从内存缓存中取,如果没有再从文件缓存中取,如果文件缓存没有取到,就从网络下载图片并且加入内存和文件缓存。

  这篇帖子先分析内存缓存是如何实现的。好吧开始进入正题。
  BitmapUtils内存缓存的核心类LruMemoryCache,LruMemoryCache代码和v4包的LruCache一样,只是加了一个存储超期的处理,这里分析LruCache源码。LRU即Least Recently Used,近期最少使用算法。也就是当内存缓存达到设定的最大值时将内存缓存中近期最少使用的对象移除,有效的避免了OOM的出现。
 
        讲到LruCache不得不提一下LinkedHashMap,因为LruCache中Lru算法的实现就是通过LinkedHashMap来实现的。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系,这种顺序有两种,一种是LRU顺序,一种是插入顺序,这可以由其构造函数public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)指定。所以,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。LruCache中将LinkedHashMap的顺序设置为LRU顺序来实现LRU缓存,每次调用get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用put插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。关于LinkedHashMap详解请前往http://www.cnblogs.com/children/archive/2012/10/02/2710624.html。
 
        下面看下LruCache的源码,我都注释的很详细了。
复制代码
  1 /*
  2  * Copyright (C) 2011 The Android Open Source Project
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *      http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16 
 17 package android.support.v4.util;
 18 
 19 import java.util.LinkedHashMap;
 20 import java.util.Map;
 21 
 22 /**
 23  * Static library version of {@link android.util.LruCache}. Used to write apps
 24  * that run on API levels prior to 12. When running on API level 12 or above,
 25  * this implementation is still used; it does not try to switch to the
 26  * framework's implementation. See the framework SDK documentation for a class
 27  * overview.
 28  */
 29 public class LruCache<K, V> {
 30     private final LinkedHashMap<K, V> map;
 31 
 32     /** Size of this cache in units. Not necessarily the number of elements. */
 33     private int size;    //当前cache的大小
 34     private int maxSize; //cache最大大小
 35 
 36     private int putCount;       //put的次数
 37     private int createCount;    //create的次数
 38     private int evictionCount;  //回收的次数
 39     private int hitCount;       //命中的次数
 40     private int missCount;      //未命中次数
 41 
 42     /**
 43      * @param maxSize for caches that do not override {@link #sizeOf}, this is
 44      *     the maximum number of entries in the cache. For all other caches,
 45      *     this is the maximum sum of the sizes of the entries in this cache.
 46      */
 47     public LruCache(int maxSize) {
 48         if (maxSize <= 0) {
 49             throw new IllegalArgumentException("maxSize <= 0");
 50         }
 51         this.maxSize = maxSize;
 52         //将LinkedHashMap的accessOrder设置为true来实现LRU
 53         this.map = new LinkedHashMap<K, V>(0, 0.75f, true);  
 54     }
 55 
 56     /**
 57      * Returns the value for {@code key} if it exists in the cache or can be
 58      * created by {@code #create}. If a value was returned, it is moved to the
 59      * head of the queue. This returns null if a value is not cached and cannot
 60      * be created.
 61      * 通过key获取相应的item,或者创建返回相应的item。相应的item会移动到队列的尾部,
 62      * 如果item的value没有被cache或者不能被创建,则返回null。
 63      */
 64     public final V get(K key) {
 65         if (key == null) {
 66             throw new NullPointerException("key == null");
 67         }
 68 
 69         V mapValue;
 70         synchronized (this) {
 71             mapValue = map.get(key);
 72             if (mapValue != null) {
 73                 //mapValue不为空表示命中,hitCount+1并返回mapValue对象
 74                 hitCount++;
 75                 return mapValue;
 76             }
 77             missCount++;  //未命中
 78         }
 79 
 80         /*
 81          * Attempt to create a value. This may take a long time, and the map
 82          * may be different when create() returns. If a conflicting value was
 83          * added to the map while create() was working, we leave that value in
 84          * the map and release the created value.
 85          * 如果未命中,则试图创建一个对象,这里create方法返回null,并没有实现创建对象的方法
 86          * 如果需要事项创建对象的方法可以重写create方法。因为图片缓存时内存缓存没有命中会去
 87          * 文件缓存中去取或者从网络下载,所以并不需要创建。
 88          */
 89         V createdValue = create(key);
 90         if (createdValue == null) {
 91             return null;
 92         }
 93         //假如创建了新的对象,则继续往下执行
 94         synchronized (this) {
 95             createCount++;  
 96             //将createdValue加入到map中,并且将原来键为key的对象保存到mapValue
 97             mapValue = map.put(key, createdValue);   
 98             if (mapValue != null) {
 99                 // There was a conflict so undo that last put
100                 //如果mapValue不为空,则撤销上一步的put操作。
101                 map.put(key, mapValue);
102             } else {
103                 //加入新创建的对象之后需要重新计算size大小
104                 size += safeSizeOf(key, createdValue);
105             }
106         }
107 
108         if (mapValue != null) {
109             entryRemoved(false, key, createdValue, mapValue);
110             return mapValue;
111         } else {
112             //每次新加入对象都需要调用trimToSize方法看是否需要回收
113             trimToSize(maxSize);
114             return createdValue;
115         }
116     }
117 
118     /**
119      * Caches {@code value} for {@code key}. The value is moved to the head of
120      * the queue.
121      *
122      * @return the previous value mapped by {@code key}.
123      */
124     public final V put(K key, V value) {
125         if (key == null || value == null) {
126             throw new NullPointerException("key == null || value == null");
127         }
128 
129         V previous;
130         synchronized (this) {
131             putCount++;
132             size += safeSizeOf(key, value);  //size加上预put对象的大小
133             previous = map.put(key, value);
134             if (previous != null) {
135                 //如果之前存在键为key的对象,则size应该减去原来对象的大小
136                 size -= safeSizeOf(key, previous);
137             }
138         }
139 
140         if (previous != null) {
141             entryRemoved(false, key, previous, value);
142         }
143         //每次新加入对象都需要调用trimToSize方法看是否需要回收
144         trimToSize(maxSize);
145         return previous;
146     }
147 
148     /**
149      * @param maxSize the maximum size of the cache before returning. May be -1
150      *     to evict even 0-sized elements.
151      * 此方法根据maxSize来调整内存cache的大小,如果maxSize传入-1,则清空缓存中的所有对象
152      */
153     private void trimToSize(int maxSize) {
154         while (true) {
155             K key;
156             V value;
157             synchronized (this) {
158                 if (size < 0 || (map.isEmpty() && size != 0)) {
159                     throw new IllegalStateException(getClass().getName()
160                             + ".sizeOf() is reporting inconsistent results!");
161                 }
162                 //如果当前size小于maxSize或者map没有任何对象,则结束循环
163                 if (size <= maxSize || map.isEmpty()) {
164                     break;
165                 }
166                 //移除链表头部的元素,并进入下一次循环
167                 Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
168                 key = toEvict.getKey();
169                 value = toEvict.getValue();
170                 map.remove(key);
171                 size -= safeSizeOf(key, value);
172                 evictionCount++;  //回收次数+1
173             }
174 
175             entryRemoved(true, key, value, null);
176         }
177     }
178 
179     /**
180      * Removes the entry for {@code key} if it exists.
181      *
182      * @return the previous value mapped by {@code key}.
183      * 从内存缓存中根据key值移除某个对象并返回该对象
184      */
185     public final V remove(K key) {
186         if (key == null) {
187             throw new NullPointerException("key == null");
188         }
189 
190         V previous;
191         synchronized (this) {
192             previous = map.remove(key);
193             if (previous != null) {
194                 size -= safeSizeOf(key, previous);
195             }
196         }
197 
198         if (previous != null) {
199             entryRemoved(false, key, previous, null);
200         }
201 
202         return previous;
203     }
204 
205     /**
206      * Called for entries that have been evicted or removed. This method is
207      * invoked when a value is evicted to make space, removed by a call to
208      * {@link #remove}, or replaced by a call to {@link #put}. The default
209      * implementation does nothing.
210      *
211      * <p>The method is called without synchronization: other threads may
212      * access the cache while this method is executing.
213      *
214      * @param evicted true if the entry is being removed to make space, false
215      *     if the removal was caused by a {@link #put} or {@link #remove}.
216      * @param newValue the new value for {@code key}, if it exists. If non-null,
217      *     this removal was caused by a {@link #put}. Otherwise it was caused by
218      *     an eviction or a {@link #remove}.
219      */
220     protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
221 
222     /**
223      * Called after a cache miss to compute a value for the corresponding key.
224      * Returns the computed value or null if no value can be computed. The
225      * default implementation returns null.
226      *
227      * <p>The method is called without synchronization: other threads may
228      * access the cache while this method is executing.
229      *
230      * <p>If a value for {@code key} exists in the cache when this method
231      * returns, the created value will be released with {@link #entryRemoved}
232      * and discarded. This can occur when multiple threads request the same key
233      * at the same time (causing multiple values to be created), or when one
234      * thread calls {@link #put} while another is creating a value for the same
235      * key.
236      */
237     protected V create(K key) {
238         return null;
239     }
240 
241     private int safeSizeOf(K key, V value) {
242         int result = sizeOf(key, value);
243         if (result < 0) {
244             throw new IllegalStateException("Negative size: " + key + "=" + value);
245         }
246         return result;
247     }
248 
249     /**
250      * Returns the size of the entry for {@code key} and {@code value} in
251      * user-defined units.  The default implementation returns 1 so that size
252      * is the number of entries and max size is the maximum number of entries.
253      *
254      * <p>An entry's size must not change while it is in the cache.
255      * 用来计算单个对象的大小,这里默认返回1,一般需要重写该方法来计算对象的大小
256      * xUtils中创建LruMemoryCache时就重写了sizeOf方法来计算bitmap的大小
257      * mMemoryCache = new LruMemoryCache<MemoryCacheKey, Bitmap>(globalConfig.getMemoryCacheSize()) {
258      *       @Override
259      *       protected int sizeOf(MemoryCacheKey key, Bitmap bitmap) {
260      *           if (bitmap == null) return 0;
261      *           return bitmap.getRowBytes() * bitmap.getHeight();
262      *       }
263      *   };
264      *
265      */
266     protected int sizeOf(K key, V value) {
267         return 1;
268     }
269 
270     /**
271      * Clear the cache, calling {@link #entryRemoved} on each removed entry.
272      * 清空内存缓存
273      */
274     public final void evictAll() {
275         trimToSize(-1); // -1 will evict 0-sized elements
276     }
277 
278     /**
279      * For caches that do not override {@link #sizeOf}, this returns the number
280      * of entries in the cache. For all other caches, this returns the sum of
281      * the sizes of the entries in this cache.
282      */
283     public synchronized final int size() {
284         return size;
285     }
286 
287     /**
288      * For caches that do not override {@link #sizeOf}, this returns the maximum
289      * number of entries in the cache. For all other caches, this returns the
290      * maximum sum of the sizes of the entries in this cache.
291      */
292     public synchronized final int maxSize() {
293         return maxSize;
294     }
295 
296     /**
297      * Returns the number of times {@link #get} returned a value.
298      */
299     public synchronized final int hitCount() {
300         return hitCount;
301     }
302 
303     /**
304      * Returns the number of times {@link #get} returned null or required a new
305      * value to be created.
306      */
307     public synchronized final int missCount() {
308         return missCount;
309     }
310 
311     /**
312      * Returns the number of times {@link #create(Object)} returned a value.
313      */
314     public synchronized final int createCount() {
315         return createCount;
316     }
317 
318     /**
319      * Returns the number of times {@link #put} was called.
320      */
321     public synchronized final int putCount() {
322         return putCount;
323     }
324 
325     /**
326      * Returns the number of values that have been evicted.
327      */
328     public synchronized final int evictionCount() {
329         return evictionCount;
330     }
331 
332     /**
333      * Returns a copy of the current contents of the cache, ordered from least
334      * recently accessed to most recently accessed.
335      */
336     public synchronized final Map<K, V> snapshot() {
337         return new LinkedHashMap<K, V>(map);
338     }
339 
340     @Override public synchronized final String toString() {
341         int accesses = hitCount + missCount;
342         int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
343         return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
344                 maxSize, hitCount, missCount, hitPercent);
345     }

下面以使用软引用为例来详细说明。弱引用的使用方式与软引用是类似的。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。

首先定义一个HashMap,保存软引用对象。

复制代码 代码如下:

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再来定义一个方法,保存Bitmap的软引用到HashMap。
复制代码 代码如下:

 public void addBitmapToCache(String path) {

        // 强引用的Bitmap对象

        Bitmap bitmap = BitmapFactory.decodeFile(path);

        // 软引用的Bitmap对象

        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);

        // 添加该对象到Map中使其缓存

        imageCache.put(path, softBitmap);

    }


获取的时候,可以通过SoftReference的get()方法得到Bitmap对象。
复制代码 代码如下:

public Bitmap getBitmapByPath(String path) {

        // 从缓存中取软引用的Bitmap对象

        SoftReference<Bitmap> softBitmap = imageCache.get(path);

        // 判断是否存在软引用

        if (softBitmap == null) {

            return null;

        }

        // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空

        Bitmap bitmap = softBitmap.get();

        return bitmap;

    }


使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

需要注意的是,在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。


经验分享:

到底什么时候使用软引用,什么时候使用弱引用呢?

个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

另外,和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这种机制。


30. Android5.06.0新特性。

Android的话,多是一些项目中的实践,使用多了,自然就知道了,还有就是多逛逛一些名人的博客,书上能讲到的东西不多。另外android底层的东西,有时间的话可以多了解一下,加分项。

推荐书籍:《疯狂android讲义》《深入理解android

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值