Android基础知识总结篇之一

  自用笔记:本文属于自用笔记,不做详解,仅供参考。

活动-Activity

在 AndroidManifest 文件中注册活动

1
2
3
4
5
6
7
8
<activity
android:name=".FirstActivity"
android:label="This is FirstActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
  • 由于最外层的 <manifest> 标签中已经通过 package 属性指定了程序的包名,所以在注册活动时,可以用 .FirstActivity来作为 com.example.activitytest.FirstActivity的缩写
  • android:label 指定活动中标题栏的内容,如果是主活动的 label,还会成为启动器中应用程序显示的名称。

隐藏标题栏

1
2
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.first_layout);

requestWindowFeature 要在 setContentView()之前执行,否则会报错

使用Menu

  • XML:

    1
    2
    3
    4
    5
    6
    7
    8
    <menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
    android:id="@+id/add_item"
    android:title="Add"/>
    <item
    android:id="@+id/remove_item"
    android:title="Remove"/>
    </menu>
  • 重写 onCreateOptionsMenu()方法:

    1
    2
    3
    4
    public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
    }
  • 自定义菜单响应事件(重写 onOptionsItemSelected() 方法):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case R.id.add_item:
    Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
    break;
    case R.id.remove_item:
    Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();
    break;
    default:
    }
    return true;
    }

销毁活动

finish();

ActivityCollector 活动管理器

  • 建立 ActivityCollector 类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ActivityCollector {
    public static List<Activity> activities = new ArrayList<Activity>();

    public static void addActivity(Activity activity) {
    activities.add(activity);
    }

    public static void removeActivity(Activity activity) {
    activities.remove(activity);
    }

    public static void finishAll() {
    for (Activity activity : activities) {
    if (!activity.isFinishing()) {
    activity.finish();
    }
    }
    }
    }
  • 在 Activity的 onCreate() 和 onDestory() 方法中实现add和remove:
    例如使用一个BaseActivity:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class BaseActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("BaseActivity", getClass().getSimpleName());
    ActivityCollector.addActivity(this);
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    ActivityCollector.removeActivity(this);
    }
    }

活动被回收时保存临时数据 onSaveInstanceState()

Activity 中还提供了一个 onSaveInstanceState()回调方法,
这个方法会保证一定在活动被回收之前调用,
因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。

  • 保存数据

    1
    2
    3
    4
    5
    6
    @Override
    protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    String tempData = "Something you just typed";
    outState.putString("data_key", tempData);
    }
  • 恢复数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate");
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.activity_main);

    //下面是通过savedInstanceState来读取数据
    if (savedInstanceState != null) {
    String tempData = savedInstanceState.getString("data_key");
    Log.d(TAG, tempData);
    }
    }

知晓当前的活动名称

getClass().getSimpleName();

活动启动的最佳写法

1
2
3
4
5
6
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}

使用 Intent 在活动之间穿梭

显式 Intent

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);

隐式 Intent

  • XML:
    1
    2
    3
    4
    5
    6
    7
    <activity android:name=".SecondActivity" >
    <intent-filter>
    <action android:name="com.example.activitytest.ACTION_START" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="com.example.activitytest.MY_CATEGORY"/>
    </intent-filter>
    </activity>

每个Intent中只能指定一个 action, 但却能指定多个 category。

  • 使用隐式 Intent来启动 Activity:
    1
    2
    3
    Intent intent = new Intent("com.example.activitytest.ACTION_START");
    intent.addCategory("com.example.activitytest.MY_CATEGORY");
    startActivity(intent);

注意: android.intent.category.DEFAULT 是默认的 category,启动时会自动添加到Intent中
如果要添加其他 category,使用 intent.addCategory()方法

更多隐式 Intent 的用法

  • 启动浏览器:
    1
    2
    3
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setData(Uri.parse("http://www.baidu.com"));
    startActivity(intent);

可以通过更详细的标签来为活动指定响应某些类型的数据,例如响应指定协议、主机名、端口、路径等的数据,详见 P48

  • 启动电话拨号:
    1
    2
    3
    Intent intent = new Intent(Intent.ACTION_DIAL);
    intent.setData(Uri.parse("tel:10086"));
    startActivity(intent);

向下一个活动传递数据

  • 传递数据

    1
    2
    3
    4
    String data = "Hello SecondActivity";
    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    intent.putExtra("extra_data", data);
    startActivity(intent);
  • 取得 intent中的数据

    1
    2
    Intent intent = getIntent();
    String data = intent.getStringExtra("extra_data");

返回数据给上一个活动

  • 第一个活动 startActivityForResult:
    1
    2
    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    startActivityForResult(intent, 1);

1是请求码,请求码只要是个唯一值就可以。

  • 第二个活动 setResult:
    1
    2
    3
    4
    Intent intent = new Intent();
    intent.putExtra("data_return", "Hello FirstActivity");
    setResult(RESULT_OK, intent);
    finish();

这个 intent 只用来传递数据
setResult()方法的两个参数:

  1. RESULT_OK是返回码,一般用 RESULT_OKRESULT_CANCELED 这两个值
  2. intent 是带有数据的intent,主要用来传递数据
  • 然后第一个活动接收数据,重写 onActivityResult() 方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
    case 1:
    if (resultCode == RESULT_OK) {
    String returnedData = data.getStringExtra("data_return");
    Log.d("FirstActivity", returnedData);
    }
    break;
    default:
    }
    }

onActivityResult()方法带有三个参数:

  1. 第一个参数 requestCode, 即我们在启动活动时传入的请求码。
  2. 第二个参数 resultCode, 即我们在返回数据时传入的处理结果。
  3. 第三个参数 data,即携带着返回数据的 Intent。
  • 重写 onBackPressed() 方法处理返回按键的事件:
    1
    2
    3
    4
    5
    6
    7
    @Override
    public void onBackPressed() {
    Intent intent = new Intent();
    intent.putExtra("data_return", "Hello FirstActivity");
    setResult(RESULT_OK, intent);
    finish();
    }

控件

引入布局,include 标签

1
<include layout="@layout/title" />

自定义控件

  • 自定义控件类:
    1
    2
    3
    4
    5
    6
    public class TitleLayout extends LinearLayout {
    public TitleLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    LayoutInflater.from(context).inflate(R.layout.title, this);
    }
    }

通过 LayoutInflater 的 from()方法可以构建出一个 LayoutInflater对象,
然后调用 inflate()方法就可以动态加载一个布局文件,
inflate()方法接收两个参数,
第一个参数是要加载的布局文件的 id,这里我们传入 R.layout.title,
第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为 TitleLayout,于是直接传入 this。

  • xml中使用自定义控件:

    1
    2
    3
    4
    <com.example.uicustomviews.TitleLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    ></com.example.uicustomviews.TitleLayout>
  • 自定义布局中得到活动对象:
    例如:

    1
    ((Activity) getContext()).finish();

ListView

  • viewHolder的使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    public View getView(int position, View convertView, ViewGroup parent) {
    Fruit fruit = getItem(position);
    View view;
    ViewHolder viewHolder;
    if (convertView == null) {
    view = LayoutInflater.from(getContext()).inflate(resourceId, null);
    viewHolder = new ViewHolder();
    viewHolder.fruitImage = (ImageView) view.findViewById
    (R.id.fruit_image);
    viewHolder.fruitName = (TextView) view.findViewById
    (R.id.fruit_name);
    view.setTag(viewHolder); // 将ViewHolder存储在View中
    } else {
    view = convertView;
    viewHolder = (ViewHolder) view.getTag(); // 重新获取ViewHolder
    }
    viewHolder.fruitImage.setImageResource(fruit.getImageId());
    viewHolder.fruitName.setText(fruit.getName());
    return view;
    }

    //ViewHolder类;
    class ViewHolder {
    ImageView fruitImage;
    TextView fruitName;
    }

px、pt、dp、sp

  • px:像素
  • pt:磅数
  • dp:密度无关像素,也称dip

    • 密度:Android 中的密度就是屏幕每英寸所包含的像素数,
      通常以 dpi 为单位。比如一个手机屏幕的宽是 2 英寸长是 3 英寸,
      如果它的分辨率是 320480 像素,那这个屏幕的密度就是 160dpi,
      如果它的分辨率是 640
      960,那这个屏幕的密度就是 320dpi,
      因此密度值越高的屏幕显示的效果就越精细。
    • 得知当前屏幕的密度值:

      float xdpi = getResources().getDisplayMetrics().xdpi;
      float ydpi = getResources().getDisplayMetrics().ydpi;
      Log.d(“MainActivity”, “xdpi is “ + xdpi);
      Log.d(“MainActivity”, “ydpi is “ + ydpi);

    • 根据 Android 的规定,
      在 160dpi 的屏幕上, 1dp 等于 1px,
      而在 320dpi 的屏幕上, 1dp就等于 2px。
      因此,使用 dp 来指定控件的宽和高,
      就可以保证控件在不同密度的屏幕中的显示比例保持一致。

  • sp:可伸缩像素

制作 Nine-Patch 图片


碎片

简单使用

1
2
3
4
5
6
AnotherRightFragment fragment = new AnotherRightFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.
beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.commit();
  • 动态添加碎片的步骤:
  1. 创建待添加的碎片实例。
  2. 获取到 FragmentManager,在活动中可以直接调用 getFragmentManager()方法得到。
  3. 开启一个事务,通过调用 beginTransaction()方法开启。
  4. 向容器内加入碎片, 一般使用 replace()方法实现, 需要传入容器的 id待添加的碎片实例
  5. 提交事务,调用 commit()方法来完成。

在碎片中模拟返回栈

transaction.addToBackStack(null);

FragmentTransaction的addToBackStack()方法,它可以接收一个名字用于描述返回栈的状态, 一般传入 null 即可。

  • 碎片和活动之间进行通信
    • 活动中得到碎片:
      1
      RightFragment rightFragment = (RightFragment)getFragmentManager().findFragmentById(R.id.right_fragment);
  • 碎片中得到活动:
    1
    MainActivity activity = (MainActivity) getActivity();

碎片的状态

  • 运行状态
    当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态。

    1. 活动运行;
    2. 碎片可见
  • 暂停状态
    当一个活动进入暂停状态时(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联的可见碎片就会进入到暂停状态。

    1. 活动暂停;
    2. 碎片可见
  • 停止状态
    当一个活动进入停止状态时, 与它相关联的碎片就会进入到停止状态。 或者通过调用 FragmentTransaction 的remove()、replace()方法将碎片从活动中移除,但有在事务提交之前调用 addToBackStack()方法,这时的碎片也会进入到停止状态。总的来说,进入停止状态的碎片对用户来说是完全不可见的,有可能会被系统回收。
    两种情况:

    • 活动停止碎片就停止;
    • 碎片自己停止:
      • 调用了FragmentTransaction 的remove()、replace()方法;
      • 并且该碎片已经添加到了返回栈
  • 销毁状态
    碎片总是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进入到销毁状态。或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但在事务提交之前并没有调用addToBackStack()方法,这时的碎片也会进入到销毁状态。
    两种情况:

    • 活动销毁碎片就销毁;
    • 碎片自己销毁:
      • 调用了FragmentTransaction 的remove()、replace()方法;
      • 并且该碎片没有被添加到返回栈

碎片的生命周期

关于Fragment的生命周期,我相信了解过的开发人员都应该把以下方法脱口而出:onAttach, onCreate, onCreateView, onViewCreated, onActivityCreated, onStart, onResume, onPause, onStop, onDestroyView, onDestroy, onDetach.

当Fragment以静态的方式,即通过在布局文件中以其它控件的方式设置时,它的生命周期随所在Activity的生命周期而发生变化。此时其生命周期的方法调用过程是这样的:

  1. 当首次展示布局页面时,其生命周期方法调用的顺序是:

    onAttach -> onCreate -> onCreateView -> onViewCreated -> onActivityCreated -> onStart -> onResume

  2. 而当关闭手机屏幕或者手机屏幕变暗时,其其生命周期方法调用的顺序是:

    onPause -> onStop

  3. 当再次对手机屏幕解锁或者手机屏幕变亮时,其生命周期方法调用的顺序是:

    onStart -> onResume

  4. 而当对当前Fragment所在屏幕按返回键时,其生命周期方法调用的顺序是:

    onPause -> onStop -> onDestoryView -> onDestory -> onDetach


广播机制

广播的两种类型

  1. 标准广播

    是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高, 但同时也意味着它是无法被截断的。

  2. 有序广播

    是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。

注册广播接收器

动态注册

动态注册是指在代码中注册

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MainActivity extends Activity {
//通过intent过滤器来广播接收器筛选需要接受的广播;
private IntentFilter intentFilter;
//这是自定义的 BroadcastReceiver
private NetworkChangeReceiver networkChangeReceiver;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
//通过intent过滤器来广播接收器筛选需要接受的广播;
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
//注册广播接收器,注意有注册一定要有取消注册
registerReceiver(networkChangeReceiver, intentFilter);
}

@Override
protected void onDestroy() {
super.onDestroy();
//取消注册这个广播接收器
unregisterReceiver(networkChangeReceiver);
}
// NetworkChangeReceiver 内部类
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
/*
* 通过getSystemService(Context.CONNECTIVITY_SERVICE)方法获得ConnectivityManager对象
* 通过ConnectivityManager对象的getActiveNetworkInfo()方法来获得网络连接信息
* */
ConnectivityManager connectionManager = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
//对网络连接信息进行判断
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable",
Toast.LENGTH_SHORT).show();
}
}
}
}

需要注意:

  1. 当网络状态发生变化时,系统发出的正是一条值为android.net.conn.CONNECTIVITY_CHANGE的广播,也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的action就行了
  2. 动态注册的广播接收器一定都要取消注册才行, 这里我们是在 onDestroy()方法中通过调用 unregisterReceiver()方法来实现的。

还要注意网络权限的声明

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

静态注册实现开机启动

静态注册是指在AndroidManifest.xml 中注册

方法如下:

  • 1、新建一个 BroadcastReceiver 的子类:

    1
    2
    3
    4
    5
    6
    public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
    }
    }

    需要注意:

    不要在 onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,
    因为在广播接收器中是不允许开启线程的
    当 onReceive()方法运行了较长时间而没有结束时, 程序就会报错。

  • 2、在AndroidManifest.xml 中注册receiver:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!--监听系统开机广播也是需要声明权限的,-->
    <!--可以看到,我们使用<uses-permission>标签, -->
    <!--又加入了一条 android.permission.RECEIVE_BOOT_COMPLETED 权限。-->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    ……
    <receiver android:name=".BootCompleteReceiver" >
    <intent-filter>
    <!--Android系统启动完成后会发出一条值为 android.intent.action.BOOT_COMPLETED 的广播 -->
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
    </receiver>
    </application>

    需要注意:

    1. 监听系统开机广播也是需要声明权限的,可以看到,我们使用<uses-permission>标签又加入了一条 android.permission.RECEIVE_BOOT_COMPLETED 权限。
    2. Android系统启动完成后会发出一条值为 android.intent.action.BOOT_COMPLETED 的广播。

发送自定义广播

发送标准广播

  • 1、新建广播接收器

    1
    2
    3
    4
    5
    6
    7
    public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "received in MyBroadcastReceiver",
    Toast.LENGTH_SHORT).show();
    }
    }
  • 2、在 AndroidManifest.xml 中对这个广播接收器进行注册
    在Application标签中加入receiver标签:

    1
    2
    3
    4
    5
    <receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
    <action android:name="com.example.broadcasttest. MY_BROADCAST"/>
    </intent-filter>
    </receiver>
待会儿我们需要发出一条 `com.example.broadcasttest. MY_BROADCAST`的广播
  • 3、在代码中发送标准广播

    1
    2
    3
    Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
    //这个是发送标准广播
    sendBroadcast(intent);

发送有序广播

  • 1、新建另一个广播接收器

    1
    2
    3
    4
    5
    6
    7
    public class AnotherBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "received in AnotherBroadcastReceiver",
    Toast.LENGTH_SHORT).show();
    }
    }
  • 2、在 AndroidManifest.xml 中对这个广播接收器进行注册
    在Application标签中加入receiver标签:

    1
    2
    3
    4
    5
    <receiver android:name=".AnotherBroadcastReceiver">
    <intent-filter>
    <action android:name="com.example.broadcasttest. MY_BROADCAST"/>
    </intent-filter>
    </receiver>
这里让 AnotherBroadcastReceiver 同样接收的是 
`com.example.broadcasttest.MY_BROADCAST` 这条广播
  • 3、在代码中发送有序广播

    1
    2
    3
    Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
    //这个是发送有序广播
    sendOrderedBroadcast(intent, null);

    sendOrderedBroadcast() 方法接收两个参数,

    第一个参数仍然是 Intent
    第二个参数是一个与权限相关的字符串, 这里传入 null 就行了。

    这样的广播看起来还不是有序的,因为没有设置receiver的优先级,
    并且也没有优先级高的的receiver接收到广播后截断它。

  • 4、设置receiver的优先级

    1
    2
    3
    4
    5
    6
      <receiver android:name=".MyBroadcastReceiver">
    <!-- 设置优先级为100 -->
    <intent-filter android:priority="100" >
    <action android:name="com.example.broadcasttest. MY_BROADCAST"/>
    </intent-filter>
    </receiver>
> 通过 `android:priority` 属性给广播接收器设置了优先级,
> 优先级比较高的广播接收器就可以先收到广播。 
> 这里将 MyBroadcastReceiver 的优先级设成了 100
> 所以它会`在 AnotherBroadcastReceiver 之前收到广播`
  • 5、让 MyBroadcastReceiver 来截断广播
    修改 MyBroadcastReceiver 中的代码,加入 abortBroadcast()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "received in MyBroadcastReceiver",
    Toast.LENGTH_SHORT).show();
    //截断广播
    abortBroadcast();
    }
    }

    这样的话,广播经过MyBroadcastReceiver 之后会终止传递,AnotherBroadcastReceiver就接收不到了。

使用本地广播

本地广播机制简介

使用本地广播机制发出的广播,
只能够在应用程序的内部进行传递,
并且广播接收器也只能接收来自本应用程序发出的广播,
这样所有的安全性问题就都不存在了。

本地广播主要就是使用了一个 LocalBroadcastManager 来对广播进行管理,
并提供了发送广播和注册广播接收器的方法。
注意:本地广播是无法通过静态注册的方式来接收的。

本地广播的示例代码

例如下面这个 Activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MainActivity extends Activity {
private IntentFilter intentFilter;
//使用 LocalReceiver 和 LocalBroadcastManager
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//得到 LocalBroadcastManager 的实例:
localBroadcastManager = LocalBroadcastManager.getInstance(this);

Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
// 使用 localBroadcastManager 发送本地广播:
localBroadcastManager.sendBroadcast(intent);
}
});

intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
// 注册本地广播监听器
localBroadcastManager.registerReceiver(localReceiver, intentFilter);
}

@Override
protected void onDestroy() {
super.onDestroy();
// 取消注册本地广播监听器:
localBroadcastManager.unregisterReceiver(localReceiver);
}

// 本地广播接收器类 LocalReceiver:
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast",
Toast.LENGTH_SHORT).show();
}
}
}

注意:本地广播是无法通过静态注册的方式来接收的。
因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,
而发送本地广播时,我们的程序肯定是已经启动了,
因此也完全不需要使用静态注册的功能。

使用本地广播的优势

  1. 可以明确地知道正在发送的广播不会离开我们的程序, 因此不需要担心机密数据泄漏的问题。
  2. 其他的程序无法将广播发送到我们程序的内部, 因此不需要担心会有安全漏洞的隐患。
  3. 发送本地广播比起发送系统全局广播将会更加高效。

关于强制下线功能

具体参见《第一行代码》中的5.5章节内容,有以下几点需要Mark的:

  1. 如果要在广播接收器里弹出一个对话框:

    1. 需要把该对话框的类型设置为 TYPE_SYSTEM_ALERT

      1
      2
      3
      4
      5
      6
      7
      8
      AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
      //dialogBuilder.setTitle("Warning");
      //dialogBuilder.set......
      //......
      AlertDialog alertDialog = dialogBuilder.create();
      // 需要设置AlertDialog的类型,保证在广播接收器中可以正常弹出
      alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
      alertDialog.show();
    2. 需要在 AndroidManifest.xml 中声明 android.permission.SYSTEM_ALERT_WINDOW 权限:

      因为我们在广播接收器ForceOfflineReceiver里弹出了一个系统级别的对话框

      修改AndroidManifest.xml,加入以下内容:

      1
      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  2. 如果要在广播接收器里启动一个活动
    需要给 Intent 加入 FLAG_ACTIVITY_NEW_TASK 这个标志:

    1
    2
    3
    4
    5
    Intent intent = new Intent(context,LoginActivity.class);
    // 给 Intent 加入 FLAG_ACTIVITY_NEW_TASK 标志:
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    // 在广播接收器中启动一个Activity:
    context.startActivity(intent);

版本控制之Git

git 常用命令

  • ubuntu安装git

    1
    sudo apt-get install git-core
  • 创建代码仓库

    配置个人身份:

    1
    2
    git config --global user.name "Tony"
    git config --global user.email "tony@gmail.com"

    查看配置的身份:

    1
    2
    git config --global user.name
    git config --global user.email

    创建仓库:

    1
    git init

    仓库创建完成后,
    会在 BroadcastBestPractice项目的根目录下生成一个隐藏的.git文件夹,
    这个文件夹就是用来记录本地所有的 Git 操作的。

    删除仓库:

    如果你想要删除本地仓库,只需要删除这个文件夹就行了。

  • 提交本地代码

    添加要提交的代码:
    添加单文件:

    1
    git add AndroidManifest.xml

    添加一个目录下的所有文件:

    1
    git add src

    添加所有的文件:

    1
    git add .

    提交代码:

    1
    git commit -m "First commit."

    commit 命令的后面我们一定要通过-m 参数来加上提交的描述信息,
    没有描述信息的提交被认为是不合法的。


数据存储

文件存储

  1. 存数据:Context类的openFileOutput()方法

    Context类的openFileOutput()方法接收两个参数:

    • 第一个参数:文件名

      即创建文件时使用的名字,
      注意这里指定的文件名不可以包含路径,
      因为所有的文件都是默认存储到 /data/data/<packagename>/files/目录下的。

    • 第二个参数:文件的操作模式

      MODE_PRIVATEMODE_APPEND两种:
      1、MODE_PRIVATE 是默认的操作模式,表示当指定同样文件名的时候, 所写入的内容将会覆盖原文件中的内容。
      2、MODE_APPEND 则表示如果该文件已存在就往文件里面追加内容。

    • 【注】MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,表示允许其他的应用程序对我们程序中的文件进行读写操作

      由于过于危险,这两种模式现已在 Android 4.2 版本中被废弃

    openFileOutput()方法返回的是一个 FileOutputStream 对象。

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public void save() {
    String data = "Data to save";
    FileOutputStream out = null;
    BufferedWriter writer = null;
    try {
    // openFileOutput() 方法返回的是一个 FileOutputStream 对象
    out = openFileOutput("data", Context.MODE_PRIVATE);
    // 把 FileOutputStream 对象包装成 BufferedWriter 对象
    writer = new BufferedWriter(new OutputStreamWriter(out));
    writer.write(data);
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    try {
    if (writer != null) {
    writer.close();
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
  2. 取数据:Context类的openFileInput()方法

    Context类的openFileInput()方法接收一个参数:

    参数:要读取的文件名
    系统会自动到/data/data/<package name>/files/目录下去加载这个文件
    返回:一个 FileInputStream 对象

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public String load() {
    FileInputStream in = null;
    BufferedReader reader = null;
    StringBuilder content = new StringBuilder();
    try {
    in = openFileInput("data");
    // 把 FileInputStream 对象包装成 BufferedReader 对象
    reader = new BufferedReader(new InputStreamReader(in));
    String line = "";
    // 惯用写法
    while ((line = reader.readLine()) != null) {
    content.append(line);
    }
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    if (reader != null) {
    try {
    reader.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    return content.toString();
    }

SharedPreferences 存储

  • 得到 SharedPreferences 对象的三种方法:

    1. Context 类中的 getSharedPreferences()方法

      此方法接收两个参数:

      1. 第一个参数用于指定 SharedPreferences 文件的名称

        如果指定的文件不存在则会创建一个。
        SharedPreferences 文件都是存放在/data/data/<packagename>/shared_prefs/目录下的。

      2. 第二个参数用于指定操作模式,主要有两种模式可以选择,MODE_PRIVATEMODE_MULTI_PROCESS

        • MODE_PRIVATE 仍然是默认的操作模式,和直接传入 0 效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences 文件进行读写
        • MODE_MULTI_PROCESS 则一般是用于会有多个进程中对同一个 SharedPreferences 文件进行读写的情况
        • 类似地, MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 这两种模式已在 Android 4.2 版本中被废弃.
    2. Activity 类中的 getPreferences()方法

      此方法接收一个参数:

      因为它自动将当前活动的类名作为 SharedPreferences 的文件名。
      其他用法和 Context 中的 getSharedPreferences()方法很相似。

3. `PreferenceManager` 类中的 `getDefaultSharedPreferences()`方法

    1. 这是一个静态方法;
    2. 它接收一个 Context 参数;
    3. 自动使用当前应用程序的包名作为前缀来命名 SharedPreferences 文件。
  • 向 SharedPreferences 文件中存储数据

    1. 用 SharedPreferences 对象的 edit()方法来获取一个 SharedPreferences.Editor 对象。
    2. 向 SharedPreferences.Editor 对象中添加数据,比如添加一个布尔型数据就使用putBoolean 方法,添加一个字符串则使用 putString()方法,以此类推。
    3. 调用 commit()方法将添加的数据提交,从而完成数据存储操作。

      示例代码:

      1
      2
      3
      4
      5
      SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
      editor.putString("name", "Tom");
      editor.putInt("age", 28);
      editor.putBoolean("married", false);
      editor.commit();
  • 从 SharedPreferences 中读取数据

    示例代码:

    1
    2
    3
    4
    SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
    String name = pref.getString("name", "");
    int age = pref.getInt("age", 0);
    boolean married = pref.getBoolean("married", false);

SQLite 数据库存储

一、创建数据库

主要依靠 SQLiteOpenHelper 类,这个类是一个抽象类,有两个抽象方法:

onCreate()
onUpgrade()

其子类需要重写这两个方法。

  • SQLiteOpenHelper 用来创建或打开数据库:
    getReadableDatabase()getWritableDatabase()都可以创建或打开一个现有的数据库:如果数据库已存在则直接打开,否则创建一个新的数据库。
    区别如下:

    getReadableDatabase():当数据库不可写入的时候(如磁盘空间已满),返回的对象将以只读的方式去打开数据库
    getWritableDatabase():当数据库不可写入的时候(如磁盘空间已满),getWritableDatabase()方法则将出现异常

    SQLiteOpenHelper 的两个构造方法一般使用参数少的那个构造方法即可:
    有4个参数:

    第一个参数:Context
    第二个参数:数据库名
    第三个参数允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入 null
    第四个参数:当前数据库的版本号,可用于对数据库进行升级操作。

    构造出SQLiteOpenHelper实例之后,再调用它的 getReadableDatabase()getWritableDatabase()方法来创建数据库。
    数据库文件会存放在/data/data/<package name>/databases/目录下。
    此时,重写的 onCreate()方法也会得到执行,所以通常会在这里去处理一些创建表的逻辑。

    示例代码如下:
    创建一个SQLiteOpenHelper的子类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_BOOK = "create table book ("
    + "id integer primary key autoincrement, "
    + "author text, "
    + "price real, "
    + "pages integer, "
    + "name text)";
    private Context mContext;
    public MyDatabaseHelper(Context context, String name,
    CursorFactory factory, int version) {
    super(context, name, factory, version);
    mContext = context;
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
    //数据库创建时执行建表语句:
    db.execSQL(CREATE_BOOK);
    Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
    }
Activity中通过 SQLiteOpenHelper 来创建数据库:
1
2
3
4
private MyDatabaseHelper dbHelper;
// 参数分别是 Context,数据库名,自定义Cursor,数据库版本号
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
dbHelper.getWritableDatabase();

二、升级数据库

最佳写法:
onCreate() 时写最新版的建表语句:

1
2
3
4
5
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}

然后 onUpgrade()这样写:

1
2
3
4
5
6
7
8
9
10
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
case 2:
db.execSQL("alter table Book add column category_id integer");
default:
}
}

注意:switch 中每一个 case 的最后都是没有使用 break 的,这是为了保证在跨版本升级的时候,每一次的数据库修改都能被全部执行到。

三、添加数据(增)

SQLiteDatabase 中 insert()方法有三个参数:

  • 第一个参数:数据库中的表名;
  • 第二个参数:用于在未指定添加数据的情况下给某些可为空的列自动赋值 NULL,一般我们用不到这个功能, 直接传入 null 即可;
  • 第三个参数:一个 ContentValues 对象,它提供了一系列的 put()方法重载,用于向 ContentValues 中添加数据。只需要将表中的每个列名以及相应的待添加数据传入即可。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
// 开始组装第一条数据
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values); // 插入第一条数据
// 把 ContentValues 的内容清理一下:
values.clear();
// 开始组装第二条数据
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values); // 插入第二条数据

四、更新数据(改)

SQLiteDatabase 中 update()方法有四个参数:

  • 第一个参数:数据库中的表名;
  • 第二个参数:ContentValues 对象,要把更新数据在这里组装进去;
  • 第三个、第四个参数:用于去约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。

示例代码:

1
2
3
4
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });

五、删除数据(删)

SQLiteDatabase 中 delete()方法有四个参数:

  • 第一个参数:数据库中的表名;
  • 第二个、第三个参数:又是用于去约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。

示例代码:

1
2
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] { "500" });

六、查询数据(查)

使用 SQLiteDatabase 中 query()方法,该方法参数及其含义如下:

query()方法参数 对应 SQL 部分 描述
table from table_name 指定查询的表名
columns select column1, column2 指定查询的列名
selection where column = value 指定 where 的约束条件
selectionArgs - 为 where 中的占位符提供具体的值
groupBy group by column 指定需要 group by 的列
having having column = value 对 group by 后的结果进一步约束
orderBy order by column1, column2 指定查询结果的排序方式

示例代码(最简单的情况):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 查询Book表中所有的数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
// cursor.getColumnIndex("name")是拿到"name"的角标
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
} while (cursor.moveToNext());
}
cursor.close();

注意:

Cursor 的 getColumnIndex()方法是获取到某一列在表中对应的位置索引;
最后要调用 close()方法来关闭 Cursor.

七、使用SQL操作数据库

用法:

db.execSQL( SQL语句 );
db.rawQuery( SQL查询语句 );

例子:

1
2
3
4
5
6
7
8
9
10
11
12
// 添加数据的方法如下:
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
new String[] { "The Da Vinci Code", "Dan Brown", "454", "16.96" });
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
new String[] { "The Lost Symbol", "Dan Brown", "510", "19.95" });
// 更新数据的方法如下:
db.execSQL("update Book set price = ? where name = ?",
new String[] { "10.99", "The Da Vinci Code" });
// 删除数据的方法如下:
db.execSQL("delete from Book where pages > ?", new String[] { "500" });
// 查询数据的方法如下:
db.rawQuery("select * from Book", null);

八、使用事务

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 开启事务
db.beginTransaction();
try {
db.delete("Book", null, null);
/*
if (true) {
// 在这里手动抛出一个异常,让事务失败
throw new NullPointerException();
}
*/
ContentValues values = new ContentValues();
values.put("name", "Game of Thrones");
values.put("author", "George Martin");
values.put("pages", 720);
values.put("price", 20.85);
db.insert("Book", null, values);
// 设置事务已经执行成功
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 结束事务
db.endTransaction();
}

上述代码中,如果在删除旧数据的操作完成后手动抛出了一个 NullPointerException(把引用部分代码取消引用),这样添加新数据的代码就执行不到了。不过由于事务的存在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。


0%