Android 四大组件
Android四大组件1、Activity1.1 初始ActivityActivity 是什么?Activity有什么作用?用户和应用程序交互的接口摆放各种空间的容器怎样创建Activity?继承Activity重写onCreate()方法为Activity提供布局xml文件清单文件中配置创建OtherActivity类,并继承Activity类package com.tinno.createac
Android四大组件
1、Activity
1.1、 初始Activity
-
Activity 是什么?
-
Activity有什么作用?
- 用户和应用程序交互的接口
- 摆放各种空间的容器
-
怎样创建Activity?
-
继承Activity
-
重写onCreate()方法
-
为Activity提供布局xml文件
-
清单文件中配置
-
-
创建OtherActivity类,并继承Activity类
package com.tinno.createactivity; import android.app.Activity; import android.os.Bundle; import androidx.annotation.Nullable; /** * 演示创建Activity * 1、继承 Activity * 2、重写onCreate() 方法 * 3、提供xml布局文件 需要在onCreate()方法中调用setContentView() 方法加载xml布局 * 4、配置 */ public class OtherActivity extends Activity { /** * 表示当Activity被创建时回调的方法,由系统框架调用 * Bundle 键为 string 的 map 集合 * @param savedInstanceState */ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_other); } }
-
为OtherActivity类提供布局文件xml文件
activity_other.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_show" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/tv_show" android:textSize="25sp" android:textColor="#00AA00"/> </RelativeLayout>
-
在配置文件中配置,使OtherActivity启动
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.createactivity"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> </activity> <!-- android:name=“需要配置的包名.类名 必选属性” android:label="表示应用程序列表中程序图标下方的文字" android:icon="表示应用程序的图标" --> <activity android:name=".OtherActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
-
启动服务 出现绿色文字 (我是OtherActivity)
-
两个Activity之间的跳转
MainActivity.java ==> 使用意图进行跳转
package com.tinno.createactivity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } // 点击按钮启动OtherActivity public void onClick(View view){ Intent intent = new Intent(MainActivity.this,OtherActivity.class); //通过intent意图对象描述启动的Activity startActivity(intent); // 启动 } }
MainActivity
的布局文件activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击跳转" android:onClick="onClick" tools:ignore="OnClick" /> </RelativeLayout>
配置 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.createactivity"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- android:name=“需要配置的包名.类名 必选属性” android:label="表示应用程序列表中程序图标下方的文字" android:icon="表示应用程序的图标" --> <activity android:name=".OtherActivity"> </activity> </application> </manifest>
-
启动服务
点击跳转 --> 显示 我是OtherActivity
1.2 、Activity的生命周期
- 什么是生命周期?
- 研究Activity的生命周期有什么作用?
- Activity生命周期的执行顺序?
- 横竖屏切换时Activity生命周期的如何变化?
1、Activiy生命周期的方法
Method | Description | Killable | Next |
---|---|---|---|
onCreate | 当启动新的Activity的时候调用 | No | onStart() |
onStart | 当Activity对用户即将可见时调用 | No | onResume() or onStop() |
onResume | 当Activity界面可与用户交互时调用 | No | onPause() |
onPause | 当系统要启动一个其他的Activity时调用,用户保存当前数据 | Yes | onResume() or onStop() |
onStop | 该Activity已经不可见时调用 | Yes | onRestart() or onDestroy() |
onRestart | 重新启动Activity时调用(此方法是重启留在缓存中的Activity) | No | onStart() |
onDestroy | 当Activity被finish或手机内存不足被销毁时候调用 | Yes | nothing |
- onCreate():表示 activity 被创建调用的方法
- onStart():表示activity能够被用户看到时回调的方法
- onResume():表示activity获取用户焦点时 能与用户交互时调用
- onPause():表示activity失去用户焦点时回调的方法
- onStop():表示activity被完全遮挡时回调的方法
- onRestart():表示activity处于停止状态重新被启动时回调的方法
- onDestroy():表示activity被销毁时回调的方法
2、Activity生命周期执行顺序
3、横竖屏切换时Activity生命周期如何变化?
默认情况下 Activity 会关闭并且重新启动
- 表示设置activity固定方向:
android:screenOrientation="portrait"
- 表示activity横竖屏切换时不会调用生命周期函数 (4.0版本之后):
设置 android:configChanges 属性 --> orientation、keyboardHidden、screenSize 多个属性值用 |
分隔
<activity android:name=".OtherActivity"
android:configChanges="orientation|keyboardHidden|screenSize">
</activity>
1.3、Activity之间的传值
- Activity之间通过Intent传值
- Activity之间通过Bundle传值
- 使用Application全局对象传值
- 启动Activity回传数据
1.3.1、Activity之间通过 Intent 传值
使用putExtra() 进行发送方的数据传递
getStringExtra()、getIntExtra()、getCharExtra() … 等方法进行接收方的数据获取
发送方:
以 key - value 对的形式存储需要传递的数据
接收方:
获取激活的 Intent 对象,根据 key 获取传递数据
-
定义两个Activity类,并为连个Activity类编写布局文件:
MainActivity.java
package com.tinno.activitypassvalue; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; /** * 演示activity之间通过 Intent 传值 */ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } // 点击按钮传递数据到指定的 activity 中 public void send(View view){ Intent intent = new Intent(this,ResultActivity.class); //putExtra(String key,value) key表示唯一性标识当前值的健 , value为具体类型 intent.putExtra("姓名","张三"); intent.putExtra("性别",'男'); intent.putExtra("年龄",30); intent.putExtra("成绩",98.5); intent.putExtra("bl",true); startActivity(intent); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="点击传值" android:onClick="send"/> </LinearLayout>
ResultActivity.java
package com.tinno.activitypassvalue; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.widget.TextView; import androidx.annotation.Nullable; public class ResultActivity extends Activity { private TextView tv; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_result); tv = (TextView) findViewById(R.id.tv_show); //1、获取激活组件的 Intent 对象 Intent intent = getIntent(); //2、根据 key 获取传递的数据 String name = intent.getStringExtra("姓名"); int age = intent.getIntExtra("年龄",0); //getIntExtra(表示获取数据的key,如果根据key没有获取数据显示的默认值) double score = intent.getDoubleExtra("成绩",0.0); char sex = intent.getCharExtra("性别",'男'); boolean bl = intent.getBooleanExtra("bl",false); //3、将数据展示到 TextView控件中 tv.setText("name: " + name + "\n age: " + age + "\n score: " + score + "\n sex: " + sex + "\n bl: " + bl); } }
activity_result.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_show" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="25sp" android:textColor="#AA0000" android:layout_gravity="center" android:text="这是ResultActivity的布局文件"/> </LinearLayout>
-
将 Activity 在
AndroidManifest.xml
中进行注册<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.activitypassvalue"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ResultActivity"> </activity> </application> </manifest>
-
运行结果 :
点击传值 按钮 --> 数据显示
1.3.2、Activity之间通过Bundle传值
1.3.2.1、数据传递方:
- 新建一个Bundle类
- Bundle类中 key - value 键值对的形式存储数据
- 创建一个 Intent 对象,将 Bundle 存入 Intent 对象
1.3.2.2、数据接收方
- 获取激活的 Intent 对象
- 获取传递的 Bundle 对象
- 根据 Bundle 中的 key 值获取指定的 value 值
1.3.2.3、步骤如 1.3.1
-
**定义两个Activity类,并为连个Activity类编写布局文件: **
MainActivity.java
package com.tinno.activitypassvaluebundle; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; /** * 演示采用Bundle进行传值 */ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } // 点击按钮通过 Bundler 将数据传递到目标 activity public void send(View view){ //1、创建 intent 意图对象 Intent intent = new Intent(this,ResultActivity.class); //2、创建 bundle 对象,用来存储需要传递的数据 Bundle bundle = new Bundle(); //3、将需要传递的数据存储到 Bundle 对象中 bundle.putString("name","李四"); bundle.putInt("age",28); bundle.putDouble("score",95.6); bundle.putChar("sex",'女'); //4、将 bundle 对象存储到 intent 对象中 intent.putExtras(bundle); //5、启动activity startActivity(intent); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击传值" android:onClick="send"/> </RelativeLayout>
ResultActivity.java
package com.tinno.activitypassvaluebundle; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.widget.TextView; import androidx.annotation.Nullable; public class ResultActivity extends Activity { private TextView tv_showInfo; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_result); tv_showInfo = (TextView) findViewById(R.id.tv_showInfo); //1、获取激活的 Intent 对象 Intent intent = getIntent(); //2、获取传递的 bundle 对象 Bundle bundle = intent.getExtras(); //3、在 bundle 根据 key 获取具体的数据 String name = bundle.getString("name"); int age = bundle.getInt("age"); double score = bundle.getDouble("score"); char sex = bundle.getChar("sex"); //4、将数据展示到 TextView 中 tv_showInfo.setText("name: " + name +"\n age: " + age + "\n score: " + score +"\n sex: " + sex); } }
activity_result.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_showInfo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="32sp" android:textColor="#AA00AA" android:textStyle="bold"/> </LinearLayout>
-
将 Activity 在
AndroidManifest.xml
中进行注册<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.activitypassvaluebundle"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ResultActivity"> </activity> </application> </manifest>
-
运行结果 :
点击传值 按钮 --> 数据显示
1.3.3、使用 Application 全局对象传值
发送方
将数据存储到 Application
接收方
读取 Application 中的数据
注意
: Application 需要注册和配置
1、创建类继承MyAppliction继承Appliction,将需要存储的数据定义为application的属性
2、在发送的activity 中发送数据
3、接收的 activity 中,获取数据操作
4、需要进行响应的注册(在AndroidManifest.xml文件中进行注册)
-
定义两个Activity类,并为两个Activity类编写布局文件:
MainActivity.java
package com.tinno.activitypassvalueappliaction; import androidx.appcompat.app.AppCompatActivity; import android.app.Application; import android.content.Intent; import android.os.Bundle; import android.view.View; /** * 演示 Application 传递数据 * * 1、创建类继承MyAppliction继承Appliction,将需要存储的数据定义为application的属性 * 2、在发送的activity 中发送数据 * 3、接收的 activity 中,获取数据操作 * 4、需要进行响应的注册(在AndroidManifest.xml文件中进行注册) * <application * android:name="自定义Application包名.类名" * /> */ public class MainActivity extends AppCompatActivity { private MyApplication application; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } /** * 点击按钮将数据存储到Application中 * @param view */ public void click(View view){ application = (MyApplication) getApplication(); application.setName("小明"); application.setAge(18); startActivity(new Intent(this,ResultActivity.class)); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击传值" android:onClick="click" android:layout_gravity="center"/> </LinearLayout>
ResultActivity.java
package com.tinno.activitypassvalueappliaction; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; import androidx.annotation.Nullable; public class ResultActivity extends Activity { private TextView tv; private MyApplication application; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_result); tv = (TextView) findViewById(R.id.tv); application = (MyApplication) getApplication(); String name = application.getName(); int age = application.getAge(); tv.setText("name: " + name + "\n age: " + age); } }
activity_result.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00AA00" android:textSize="32sp" android:text="你好" android:layout_gravity="center"/> </LinearLayout>
-
定义一个自定义类 MyApplication 继承自 Application,用来作为传值的实体类
MyApplication
package com.tinno.activitypassvalueappliaction; import android.app.Application; public class MyApplication extends Application { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
-
在
AndroidManifest.xml
对目标Activity进行注册和 application 进行注册<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.activitypassvalueappliaction"> <application android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ResultActivity"> </activity> </application> </manifest>
-
结果 -> 点击传值 按钮跳转到 ResultActivity中,并获取数据
1.3.4、启动Activity回传数据
-
定义两个Activity类,并为两个Activity类编写布局文件:
MainActivity.java
package com.tinno.activitypassforresult; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.TextView; /** * 启动Activity回传数据 */ public class MainActivity extends AppCompatActivity { private EditText et_num1,et_num2; private TextView tv_result; private static final int REQUEST_CODE=1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取两个输入框的内容和结果框的内容 et_num1 = (EditText) findViewById(R.id.et_num1); et_num2 = (EditText) findViewById(R.id.et_num2); tv_result = (TextView) findViewById(R.id.et_res); } // 点击按钮发送数据到目标的 activity public void onClick(View view){ Intent intent = new Intent(this,ResultActivity.class); String num1 = et_num1.getText().toString(); String num2 = et_num2.getText().toString(); intent.putExtra("num1",num1); intent.putExtra("num2",num2); startActivityForResult(intent,REQUEST_CODE); //startActivityForResult(请求的Intent对象,大于0的整数请求码) } /** * 用来处理setResult()方法回传的数据 * @param requestCode 本次请求的请求码 * @param resultCode 结果码 * @param data 回传的意图对象 */ @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK){ String result = data.getStringExtra("info"); Log.e("xx", "onActivityResult: " + result ); tv_result.setText(result); } } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <EditText android:id="@+id/et_num1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:textSize="20sp" android:ems="3"/> <TextView android:id="@+id/et_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@id/et_num1" android:layout_alignBottom="@id/et_num1" android:layout_toRightOf="@id/et_num1" android:textSize="20sp" android:text="+"/> <EditText android:id="@+id/et_num2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@id/et_add" android:layout_alignBottom="@id/et_add" android:layout_toRightOf="@id/et_add" android:textSize="20sp" android:ems="3"/> <TextView android:id="@+id/et_amount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@id/et_num2" android:layout_alignBottom="@id/et_num2" android:layout_toRightOf="@id/et_num2" android:textSize="20sp" android:text="="/> <TextView android:id="@+id/et_res" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@id/et_amount" android:layout_alignBottom="@id/et_amount" android:layout_toRightOf="@id/et_amount" android:textSize="20sp" android:text="?"/> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/et_res" android:layout_marginTop="22dp" android:layout_toRightOf="@id/et_res" android:onClick="onClick" android:text="计算结果"/> </RelativeLayout>
ResultActivity.java
package com.tinno.activitypassforresult; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.TextView; import androidx.annotation.Nullable; public class ResultActivity extends Activity { private TextView tv_num1,tv_num2; private EditText et_result; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_result); tv_num1 = (TextView) findViewById(R.id.textView1); tv_num2 = (TextView) findViewById(R.id.textView3); et_result = (EditText) findViewById(R.id.editText1); Intent intent = getIntent(); tv_num1.setText(intent.getStringExtra("num1")); tv_num2.setText(intent.getStringExtra("num2")); } // 点击按钮将数据进行回传到发送 activity的对象中 public void send(View view){ String result = et_result.getText().toString(); //获取结果数据 Log.e("xx", "获取的结果为: " + result ); Intent intent = new Intent(); intent.putExtra("info",result); setResult(Activity.RESULT_OK,intent); //setResult(请求的结果码,表示回传的数据的intent对象) ResultActivity.this.finish(); //关闭当前 activity } }
activity_result.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginLeft="20dp" android:layout_marginTop="19dp" android:textSize="20sp" android:text="*"/> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@id/textView1" android:layout_marginLeft="28dp" android:layout_toRightOf="@id/textView1" android:textSize="20sp" android:text="+"/> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@id/textView2" android:layout_marginLeft="28dp" android:layout_toRightOf="@id/textView2" android:textSize="20sp" android:text="*"/> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@id/textView3" android:layout_marginLeft="28dp" android:layout_toRightOf="@id/textView3" android:textSize="20sp" android:text="="/> <EditText android:id="@+id/editText1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@id/textView4" android:layout_alignParentRight="true" android:layout_marginRight="32dp" android:ems="3"/> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignRight="@id/editText1" android:layout_below="@id/editText1" android:layout_marginTop="30dp" android:onClick="send" android:text="回传数据"/> </RelativeLayout>
-
在
Androidmanifest.xml
进行注册<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.activitypassforresult"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ResultActivity"> </activity> </application> </manifest>
-
结果 ==> 在MainActivity中输入两个数字,点击
计算结果
按钮 跳转到 ResultActivity ,在ResultActivity进行结果计算,点击回传数据
按钮,将数据传回到 MainActivity 中
1.4、Task、Back Stack
Task:
多个Activity一起完成一项工作时,它们的集合被称作一个Task。
Back Stack:
一个Task的所有的Activity被放置在一个stack结构
中,根据他们启动的顺序被添加。Stack不会进行重新排列,只会在打开新Activity时添加其到栈顶,或finish时从栈顶移除。所以Activity在此stack中表现为“last in,fisrt out”,因为上述特点,多个Activity在打开和关闭时,stack表现出“回退栈”这样的效果
https://blog.csdn.net/ckq5254/article/details/79474827
-
定义两个Activity类,并为两个Activity类编写布局文件:
MainActivity.java
package com.tinno.taskdemo; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; /** * 演示Task */ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void click(View view){ startActivity(new Intent(this,OtherActivity.class)); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击启动OtherActivity" android:layout_alignParentRight="true" android:layout_alignParentLeft="true" android:onClick="click"/> </RelativeLayout>
OtherActivity.java
package com.tinno.taskdemo; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.view.View; import androidx.annotation.Nullable; public class OtherActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_other); } /** * 点击按钮打开发送短信的界面 * @param view */ public void click(View view){ // 打开设置页面 Settings.ACTION_SETTINGS // 打开短信界面,发送短信 Intent.ACTION_SENDTO,Uri.parse("smsto:10000")) Intent intent = new Intent(Settings.ACTION_SETTINGS); startActivity(intent); } }
activity_other.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击启动设置页面" android:layout_alignParentLeft="true" android:layout_alignParentRight="true" android:onClick="click"/> </RelativeLayout>
-
在
Androidmanifest.xml
进行注册<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.taskdemo"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".OtherActivity"> </activity> </application> </manifest>
-
结果 点击
启动OTHERACTIVITY
按钮跳转到 OtherActivity中,在点击点击启动设置页面
按钮 跳转到 系统设置页面
1.5、Activity的启动模式
1.5.1、什么是启动模式
Activity 启动时的策略
**AndroidManifest.xml 中的标签的 android:lauchMode 属性设置 **
1.5.2、启动模式有什么作用
可以更好根据用户需求在Back Stack中管理Activity 提高用户体验
当我们多次启动同一个Activity的时候,系统会创建多个实例,并把它们一一放入任务栈当中,当我们单击back键的时候,会发现这些Activity会一一回退。任务栈是一种“后进先出”的栈结构,每次按一下back键就会有一个Activity出栈,直到栈空为止,当栈中无任何Activity的时候,系统就会回收这个任务栈。
1.5.3、Activity 的启动模式
-
Standard:标准模式
标准模式,这也是系统的默认模式。每次启动一个Activity都会重新创建一个新的实例,不管这个实例是否已经存在。一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。比如ActivityA启动了ActivityB(B是标准模式),那么B就会进入到A所在的栈中。
例如:栈内情况为ABCD,其中ABCD为四个Activity,A位于栈底,D位于栈顶,这个时候假设要再次启动D,如果D的启动模式为standard,那么由于D被重新创建,导致栈内的情况变为ABCDD。
-
SingleTop:栈顶复用模式
栈顶复用模式,这种模式下,如果新的Activity已经位于任务栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此方法的参数我们可以取出当前请求的信息。需要注意的是,这个Activity的onCreate,onStart不会被系统调用,因为它并没有发生改变。如果新Activity的实例已经存在但是不是位于栈顶,那么这个Activity仍然会被重新创建。
例如:栈内情况为ABCD,其中ABCD为四个Activity,A位于栈底,D位于栈顶,这个时候假设要再次启动D,如果D的启动模式为singleTop,栈内的情况变为ABCD。
-
SingleTask:栈内复用模式
栈内复用模式,这是一种单实例模式,在这种模式下,只要Activity在一个栈中存在,那么多次启动此Activity都不会创建实例,和singleTop一样,系统也会回调其onNewIntent。具体点说,当一个具有singleTask模式的Activity请求启动后,比如Activity A,系统首先会寻找是否存在A想要的任务栈,如果不存在,就会重新创建一个任务栈,然后创建A的实例后把A放到栈中。如果存在A所需的任务栈,这时要看A是否在栈中有实例存在,如果有实例存在,那么系统就会把A调到栈顶并调用它的onNewIntent方法,如果实例不存在,就创建A的实例并把A压入栈中:
例如:1)比如目前任务栈S1中情况为ABC,这个时候Activity D 以singleTask模式请求启动,其所需要的任务栈为S2,由于S2和D的实例均不存在,所以系统会先创建任务栈S2,然后再创建D的实例并将其入栈到S2
2)另外一种情况,假设D所需的任务栈为S1,其他情况如上面,那么由于S1已经存在,所以系统会直接创建D的实例并将其入栈到S1
3)如果D所需的任务栈为S1,并且当前任务栈S1的情况为ADBC,根据栈内复用的原则,此时D不会重新创建,系统会把D切换到栈顶并调用其onNewIntent方法,同时由于singleTask默认具有clearTop的效果,会导致栈内所有在D上面的Activity全部出栈,于是最终S1中的情况为AD。
-
SingleInstance:单实例模式
这是一种特殊的singleTask模式,它除了具有singleTask模式的所有特性之外,还加强了意见,那就是具有此模式的Activity只能单独地位于一个任务栈中,比如Activity A是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的Activity,除非这个特性的任务栈被系统销毁了。
回退时展示的页面为 singleInstance 模式的 Activity
回退时展示页面为非 singleInstance 模式的 Activity
其实,不管回退时当前展示页面是何种模式的Activity,每次点击返回时都是先将当前展示页面所处的任务栈中的Activity弹栈,点击到当前任务栈中Activity全部弹完了,接着点击回去弹最后面展示出来的不是属于本任务栈的Activity所对应的任务栈(很拗口,看下图)。依次一直到任务退出。
代码演示
-
新建两个Activity 和 对应的 xml布局文件
MainActivity.java
package com.tinno.activitylauchmode; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; /** * 演示Activity的启动模式 */ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void onClick(View view){ Intent intent = null; switch (view.getId()){ case R.id.btn01: //启动第一个按钮 intent = new Intent(this,MainActivity.class); break; case R.id.btn02: //启动第二个按钮 intent = new Intent(this,OtherActivity.class); break; } startActivity(intent); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/btn01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动Activity01" android:onClick="onClick"/> <Button android:id="@+id/btn02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动Activity02" android:onClick="onClick" android:layout_below="@+id/btn01"/> </RelativeLayout>
OtherActivity.java
package com.tinno.activitylauchmode; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import androidx.annotation.Nullable; public class OtherActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_other); } public void click(View view){ Intent intent = null; switch (view.getId()){ case R.id.button01: intent = new Intent(this,MainActivity.class); break; case R.id.button02: intent = new Intent(this,OtherActivity.class); break; } startActivity(intent); } }
activity_other.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动Activity01" android:onClick="click"/> <Button android:id="@+id/button02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动Activity02" android:onClick="click" android:layout_below="@+id/button01"/> </RelativeLayout>
-
在
AndroidManifest.xml
中进行注册,并设置启动方式<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinno.activitylauchmode"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TencentClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".OtherActivity" android:launchMode="singleInstance"> </activity> </application> </manifest>
2、ContentProvider(内容提供者)
2.1、内容提供者原理
ContentProvider(内容提供者)是 Android 的四大组件之一,管理 Android 以结构化方式存放的数据,以相对安全的方式封装数据(表)并且提供简易的处理机制和统一的访问接口供其他程序调用。
Android 的数据存储方式总共有五种,分别是:Shared Preferences、网络存储、文件存储、外储存储、SQLite。但一般这些存储都只是在单独的一个应用程序之中达到一个数据的共享,有时候我们需要操作其他应用程序的一些数据,就会用到 ContentProvider。而且 Android 为常见的一些数据提供了默认的 ContentProvider(包括音频、视频、图片和通讯录等)。
2.2、ContentProvider 使用详解
2.2.1、URI(Uniform Resource Identifier)
其它应用可以通过 ContentResolver 来访问 ContentProvider 提供的数据,而 ContentResolver 通过 uri 来定位自己要访问的数据,所以我们要先了解 uri。URI(Universal Resource Identifier)统一资源定位符,如果您使用过安卓的隐式启动就会发现,在隐式启动的过程中我们也是通过 uri 来定位我们需要打开的 Activity 并且可以在 uri 中传递参数。
URI 为系统中的每一个资源赋予一个名字,比方说通话记录。每一个 ContentProvider 都拥有一个公共的 URI,用于表示 ContentProvider 所提供的数据。URI 的格式如下:
// 规则
[scheme:][//host:port][path][?query]
// 示例
content://com.wang.provider.myprovider/tablename/id:
- 标准前缀(scheme)——content://,用来说明一个Content Provider控制这些数据;
- URI 的标识 (host:port)—— com.wang.provider.myprovider,用于唯一标识这个 ContentProvider,外部调用者可以根据这个标识来找到它。对于第三方应用程序,为了保证 URI 标识的唯一性,它必须是一个完整的、小写的类名。这个标识在元素的authorities属性中说明,一般是定义该 ContentProvider 的包.类的名称;
- 路径(path)——tablename,通俗的讲就是你要操作的数据库中表的名字,或者你也可以自己定义,记得在使用的时候保持一致就可以了;
- 记录ID(query)——id,如果URI中包含表示需要获取的记录的 ID,则返回该id对应的数据,如果没有ID,就表示返回全部;
对于第三部分路径(path)做进一步的解释,用来表示要操作的数据,构建时应根据实际项目需求而定。如:
-
操作tablename表中id为11的记录,构建路径:/tablename/11;
-
操作tablename表中id为11的记录的name字段:tablename/11/name;
-
操作tablename表中的所有记录:/tablename;
-
操作来自文件、xml或网络等其他存储方式的数据,如要操作xml文件中tablename节点下name字段:/ tablename/name;
-
若需要将一个字符串转换成Uri,可以使用Uri类中的parse()方法,如:
Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
再来看一个例子:
http://www.baidu.com:8080/wenku/jiatiao.html?id=123456&name=jack
uri 的各个部分在安卓中都是可以通过代码获取的,下面我们就以上面这个 uri 为例来说下获取各个部分的方法:
- getScheme(): 获取 Uri 中的 scheme 字符串部分,在这里是 http
- getHost(): 获取 Authority 中的 Host 字符串,即 www.baidu.com
- getPost(): 获取 Authority 中的 Port 字符串,即 8080
- getPath(): 获取 Uri 中 path 部分,即 wenku/jiatiao.html
- getQuery(): 获取 Uri 中的 query 部分,即 id=15&name=du
2.2.2、MIME
MIME 是指定某个扩展名的文件用一种应用程序来打开,就像你用浏览器查看 PDF 格式的文件,浏览器会选择合适的应用来打开一样。Android 中的工作方式跟 HTTP 类似,ContentProvider 会根据 URI 来返回 MIME 类型,ContentProvider 会返回一个包含两部分的字符串。MIME 类型一般包含两部分,如:
text/html
text/css
text/xml
application/pdf
分为类型和子类型,Android 遵循类似的约定来定义MIME类型,每个内容类型的 Android MIME 类型有两种形式:多条记录(集合)和单条记录。
- 集合记录(dir):
vnd.android.cursor.dir/自定义
- 单条记录(item):
vnd.android.cursor.item/自定义
vnd 表示这些类型和子类型具有非标准的、供应商特定的形式。Android中类型已经固定好了,不能更改,只能区别是集合还是单条具体记录,子类型可以按照格式自己填写。
在使用 Intent 时,会用到 MIME,根据 Mimetype 打开符合条件的活动。
2.2.3、UriMatcher
Uri 代表要操作的数据,在开发过程中对数据进行获取时需要解析 Uri,Android 提供了两个用于操作 Uri 的工具类,分别为 UriMatcher 和 ContentUris 。掌握它们的基本概念和使用方法,对一个 Android 开发者来说是一项必要的技能。
UriMatcher 类用于匹配 Uri,它的使用步骤如下:
- 将需要匹配的Uri路径进行注册,代码如下:
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配“content://com.wang.provider.myprovider/tablename”路径,返回匹配码为1
sMatcher.addURI("content://com.wang.provider.myprovider", " tablename ", 1);
//如果match()方法匹配content://com.wang.provider.myprovider/tablename/11路径,返回匹配码为2
sMatcher.addURI("com.wang.provider.myprovider", "tablename/#", 2);
此处采用 addURI 注册了两个需要用到的 URI;注意,添加第二个 URI 时,路径后面的 id 采用了通配符形式 “#”,表示只要前面三个部分都匹配上了就 OK。
- 注册完需要匹配的 Uri 后,可以使用 sMatcher.match(Uri) 方法对输入的 Uri 进行匹配,如果匹配就返回对应的匹配码,匹配码为调用 addURI() 方法时传入的第三个参数。
switch (sMatcher.match(Uri.parse("content://com.zhang.provider.yourprovider/tablename/100"))) {
case 1:
//match 1, todo something
break;
case 2
//match 2, todo something
break;
default:
//match nothing, todo something
break;
}
2.2.4、ContentUris
ContentUris 类用于操作 Uri 路径后面的 ID 部分,它有两个比较实用的方法:withAppendedId(Uri uri, long id) 和 parseId(Uri uri)。
- withAppendedId(Uri uri, long id) 用于为路径加上 ID 部分:
Uri uri = Uri.parse("content://cn.scu.myprovider/user")
//生成后的Uri为:content://cn.scu.myprovider/user/7
Uri resultUri = ContentUris.withAppendedId(uri, 7);
- parseId(Uri uri) 则从路径中获取 ID 部分:
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
//获取的结果为:7
long personid = ContentUris.parseId(uri);
2.3、ContentProvider 主要方法
ContentProvider 是一个抽象类,如果我们需要开发自己的内容提供者我们就需要继承这个类并复写其方法,需要实现的主要方法如下:
public boolean onCreate():
在创建 ContentProvider 时使用public Cursor query():
用于查询指定 uri 的数据返回一个 Cursorpublic Uri insert():
用于向指定uri的 ContentProvider 中添加数据public int delete():
用于删除指定 uri 的数据public int update():
用户更新指定 uri 的数据public String getType():
用于返回指定的 Uri 中的数据 MIME 类型
数据访问的方法 insert,delete 和 update 可能被多个线程同时调用,此时必须是线程安全的。
如果操作的数据属于集合类型,那么 MIME 类型字符串应该以 vnd.android.cursor.dir/ 开头,
- 要得到所有 tablename 记录: Uri 为 content://com.wang.provider.myprovider/tablename,那么返回的MIME类型字符串应该为:vnd.android.cursor.dir/table。
如果要操作的数据属于非集合类型数据,那么 MIME 类型字符串应该以 vnd.android.cursor.item/ 开头,
- 要得到 id 为 10 的 tablename 记录,Uri 为 content://com.wang.provider.myprovider/tablename/10,那么返回的 MIME 类型字符串为:vnd.android.cursor.item/tablename 。
方法使用示例
使用 ContentResolver 对 ContentProvider 中的数据进行操作的代码如下:
ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
// 添加一条记录
ContentValues values = new ContentValues();
values.put("name", "wang1");
values.put("age", 28);
resolver.insert(uri, values);
// 获取tablename表中所有记录
Cursor cursor = resolver.query(uri, null, null, null, "tablename data");
while(cursor.moveToNext()){
Log.i("ContentTest", "tablename_id="+ cursor.getInt(0)+ ", name="+ cursor.getString(1));
}
// 把id为1的记录的name字段值更改新为zhang1
ContentValues updateValues = new ContentValues();
updateValues.put("name", "zhang1");
Uri updateIdUri = ContentUris.withAppendedId(uri, 2);
resolver.update(updateIdUri, updateValues, null, null);
// 删除id为2的记录,即字段age
Uri deleteIdUri = ContentUris.withAppendedId(uri, 2);
resolver.delete(deleteIdUri, null, null);
监听数据变化
如果ContentProvider的访问者需要知道数据发生的变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者。只给出类中监听部分的代码:
public class MyProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("tablename", "tablenameid", values);
getContext().getContentResolver().notifyChange(uri, null);
}
}
而访问者必须使用 ContentObserver 对数据(数据采用 uri 描述)进行监听,当监听到数据变化通知时,系统就会调用 ContentObserver 的 onChange() 方法:
getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
public PersonObserver(Handler handler) {
super(handler);
}
public void onChange(boolean selfChange) {
//to do something
}
}
实例说明
数据源是 SQLite, 用 ContentResolver 操作 ContentProvider。
Constant.java(储存一些常量)
public class Constant {
public static final String TABLE_NAME = "user";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_NAME = "name";
public static final String AUTOHORITY = "cn.scu.myprovider";
public static final int ITEM = 1;
public static final int ITEM_ID = 2;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/user";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/user";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTOHORITY + "/user");
}
DBHelper.java (操作数据库)
public class DBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "finch.db";
private static final int DATABASE_VERSION = 1;
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) throws SQLException {
//创建表格
db.execSQL("CREATE TABLE IF NOT EXISTS "+ Constant.TABLE_NAME + "("+ Constant.COLUMN_ID +" INTEGER PRIMARY KEY AUTOINCREMENT," + Constant.COLUMN_NAME +" VARCHAR NOT NULL);");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLException {
// 这里知识简单删除并创建表格
// 如果需要保留原来的数据,需要先备份再删除
db.execSQL("DROP TABLE IF EXISTS "+ Constant.TABLE_NAME+";");
onCreate(db);
}
}
MyProvider.java (自定义的 ContentProvider )
public class MyProvider extends ContentProvider {
DBHelper mDbHelper = null;
SQLiteDatabase db = null;
private static final UriMatcher mMatcher;
static{
mMatcher = new UriMatcher(UriMatcher.NO_MATCH); // 注册 uri
mMatcher.addURI(Constant.AUTOHORITY,Constant.TABLE_NAME, Constant.ITEM);
mMatcher.addURI(Constant.AUTOHORITY, Constant.TABLE_NAME+"/#", Constant.ITEM_ID);
}
@Override
public String getType(Uri uri) { // 根据匹配规则返回对应的类型
switch (mMatcher.match(uri)) {
case Constant.ITEM:
return Constant.CONTENT_TYPE;
case Constant.ITEM_ID:
return Constant.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URI"+uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO Auto-generated method stub
long rowId;
if(mMatcher.match(uri)!=Constant.ITEM){
throw new IllegalArgumentException("Unknown URI"+uri);
}
rowId = db.insert(Constant.TABLE_NAME,null,values);
if(rowId>0){
Uri noteUri=ContentUris.withAppendedId(Constant.CONTENT_URI, rowId);
getContext().getContentResolver().notifyChange(noteUri, null);
return noteUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
@Override
public boolean onCreate() {
// TODO Auto-generated method stub
mDbHelper = new DBHelper(getContext());
db = mDbHelper.getReadableDatabase();
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO Auto-generated method stub
Cursor c = null;
switch (mMatcher.match(uri)) {
case Constant.ITEM:
c = db.query(Constant.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
case Constant.ITEM_ID:
c = db.query(Constant.TABLE_NAME, projection,Constant.COLUMN_ID + "="+uri.getLastPathSegment(), selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI"+uri);
}
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}
}
MainActivity.java(ContentResolver操作)
public class MainActivity extends Activity {
private ContentResolver mContentResolver = null;
private Cursor cursor = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.tv);
mContentResolver = getContentResolver();
tv.setText("添加初始数据 ");
for (int i = 0; i < 10; i++) {
ContentValues values = new ContentValues();
values.put(Constant.COLUMN_NAME, "fanrunqi"+i);
mContentResolver.insert(Constant.CONTENT_URI, values);
}
tv.setText("查询数据 ");
cursor = mContentResolver.query(Constant.CONTENT_URI, new String[]{Constant.COLUMN_ID,Constant.COLUMN_NAME}, null, null, null);
if (cursor.moveToFirst()) {
String s = cursor.getString(cursor.getColumnIndex(Constant.COLUMN_NAME));
tv.setText("第一个数据: "+s);
}
}
}
最后在manifest
申明 :
<provider android:name="MyProvider" android:authorities="cn.scu.myprovider" />
2.4、ContentResolver实现对系统数据进行操作 --> (ContentResolver: 内容解析者)
- ContentResolver 实现系统数据的操作(联系人【查询】、媒体库文件、通话记录、短信记录)
实例1:使用内容解析者读取手机的通话记录方法
-
创建布局文件,页面包括一个按钮和一个ListView,点击按钮读取手机的通话记录,并将读取的内容放在ListView上
,代码如下:activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="查询通话记录" android:layout_margin="5dp" android:onClick="queryLog"/> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
list_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:padding="10dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_number" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="number"/> <TextView android:id="@+id/tv_date" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="date"/> </LinearLayout>
-
在 MainActivity.java 类中对手机通话记录进行解析,并读取手机的通话记录
,代码如下:package com.tinno.contentprovider_calllog; import androidx.appcompat.app.AppCompatActivity; import android.content.ContentResolver; import android.database.Cursor; import android.net.ParseException; import android.net.Uri; import android.os.Bundle; import android.provider.CallLog; import android.view.View; import android.widget.CursorAdapter; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import java.text.SimpleDateFormat; import java.util.Date; /** * 演示查询通话记录 */ public class MainActivity extends AppCompatActivity { private ListView lv; // 内容解析者 --> 用来从内容提供者中获取数据的. private ContentResolver resolver; private String CALL_LOG_URI = "content://call_log/calls"; // 要查询的列名 private String[] columns = new String[]{ CallLog.Calls._ID,CallLog.Calls.NUMBER,CallLog.Calls.DATE,CallLog.Calls.TYPE }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); resolver = getContentResolver(); lv = (ListView) findViewById(R.id.lv); } // 查询通话记录 public void queryLog(View view){ /** * @Uri:统一资源标识符;url:统一资源定位符 * @projection 需要查询的表中的列 */ Cursor cursor = resolver.query(Uri.parse(CALL_LOG_URI), columns, null, null, null); // 将查询出来的数据放入适配器中 SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,R.layout.list_item,cursor,new String[]{CallLog.Calls.NUMBER,CallLog.Calls.DATE},new int[]{R.id.tv_number,R.id.tv_date}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); // 显示在ListView上 lv.setAdapter(adapter); } /** * @Description: long类型转换成日期 * * @param lo 毫秒数 * @return String yyyy-MM-dd HH:mm:ss */ public static String longToDate(long lo){ Date date = new Date(lo); SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sd.format(date); } }
-
在 Manifest 中加入读取手机通话记录的权限
<!-- 添加读取通话记录的权限 --> <uses-permission android:name="android.permission.READ_CALL_LOG"/>
-
实现的效果
注意:
当控制台报错
Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.CallLogProvider from ProcessRecord{f87e01b 6683:com.tinno.contentprovider_calllog/u0a502} (pid=6683, uid=10502) requires android.permission.READ_CALL_LOG or android.permission.WRITE_CALL_LOG
如图所示:
需要在
MainActivity
中加入授权权限
加入该方法@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] >permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); }
并在
onCreate
加入该方法requestPermissions(new String[]{Manifest.permission.READ_CALL_LOG},1);
如图所示
实例二:使用内容解析者读取手机短信记录
-
创建布局文件,页面包括一个按钮和一个ListView,点击按钮读取手机的短信记录,并将读取的内容放在ListView上
,代码如下:activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="查询短信记录" android:onClick="querySms"/> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
list_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_vertical" android:padding="10dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_number" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textColor="#F00" android:text="fddd"/> <TextView android:id="@+id/tv_content" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textColor="#0f0" android:text="fddd"/> <TextView android:id="@+id/tv_state" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textColor="#0ff" android:text="fddd"/> </LinearLayout>
-
在 MainActivity.java 类中对手机短信记录进行解析,并读取手机的通话记录
,代码如下:package com.tinno.contentprovider_sms; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; /** * 演示读取手机的短信内容 */ public class MainActivity extends AppCompatActivity { private ListView lv; private Cursor cursor; private ContentResolver resolver; private String SMS_URI = "content://sms"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv = (ListView) findViewById(R.id.lv); resolver = getContentResolver(); } // 查询短信记录 public void querySms(View view){ cursor = resolver.query(Uri.parse(SMS_URI), null, null, null, null); lv.setAdapter(new MyAdapter(this,cursor,CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER)); // 关闭cursor } @Override protected void onDestroy() { super.onDestroy(); if(cursor != null){ cursor.close(); } } class MyAdapter extends CursorAdapter{ public MyAdapter(Context context, Cursor c, int flags) { super(context, c, flags); } // 创建一个视图 --> 引入ListView中要展示的子视图 @Override public View newView(Context context, Cursor cursor, ViewGroup viewGroup) { return getLayoutInflater().inflate(R.layout.list_item,null); } // 绑定数据的方法 @Override @SuppressLint("Range") public void bindView(View view, Context context, Cursor cursor) { TextView tvNumber = (TextView) view.findViewById(R.id.tv_number); TextView tvContent = (TextView) view.findViewById(R.id.tv_content); TextView tvType = (TextView) view.findViewById(R.id.tv_state); String number = cursor.getString(cursor.getColumnIndex("address")); String content = cursor.getString(cursor.getColumnIndex("body")); tvNumber.setText(number); tvContent.setText(content); int type = cursor.getInt(cursor.getColumnIndex("type")); if (type == 1){ tvType.setText("接收"); }else { tvType.setText("发送"); } } } }
-
在 Manifest 中加入读取手机短信记录的权限
<!-- 添加读取设备短信权限 --> <uses-permission android:name="android.permission.READ_SMS"/>
-
实现的效果
2.5、自定义ContentProvider
自定义 ContentProvider
的流程一般如下
-
在分享数据的 APP 中创建一个类,继承
ContentProvider
-
按需实现对应的方法,不需要的直接空实现
方法 说明 onCreate() 只执行一次,用于初始化 Provider
insert() 插入 delete() 删除 update() 更新 query() 查询 getType() 获得 ContentProvider
数据的MIME
类型 -
在
AndroidManifest.xml
中注册自定义的ContentProvider
<provider <!-- 全限定类名 --> android:name = "cn.twle.android.bean.NameContentProvider" <!-- 用于匹配的 URI --> android:authorities = "cn.twle.android.providers.msprovider" <!-- 是否共享数据 --> android:exported="true"> </provider>
-
使用
UriMatcher
完成Uri
的匹配-
初始化
UriMatcher
对象private static UriMatcher matcher = new UriMatcher (UriMatcher.NO_MATCH);
-
使用静态代码块,通过
addURI()
方法将uri
添加到matcher
中static { matcher.addURI("cn.twle.android.providers.msprovider","test","1"); }
前两个参数构成
URI
, 第三个参数:匹配后返回的标识码,如果不匹配返回-1
-
在下面需要匹配
Uri
的地方使用match()
方法switch( matcher.match(uri)) { case 1: break; case 2: break; default: break; }
当然还可以使用通配符,比如
test/*
或test/#
*
代表所有字符,#
代表数字
-
-
使用
ContentUris
类为Uri
追加id
, 或者解析Uri
中的id
-
withAppendedId(uri,id)
为路径添加id
部分Uri nameUri = ContentUris.withAppendedId(uri,rowId);
-
parseId(uri)
解析uri
中的id
值long nameId = ContentUris.parseId(uri);
-
-
然后在另一个工程中,调用
getContentResolver()
方法获得Resolver
对象,再调用相应的操作方法,比如插入操作ContentValues values = new ContentValues(); values.put("name","测试"); Uri uri = Uri.parse("cn.twle.android.providers.msprovider/test"); resolver.insert(uri,values);
范例
-
创建一个 空的 Android 项目
cn.twle.android.CustomProvider
-
在
MainActivity.java
同一目录下添加一个数据库创建类DBOpenHelper.java
package cn.twle.android.customprovider; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DBOpenHelper extends SQLiteOpenHelper { final String CREATE_SQL = "CREATE TABLE test(_id INTEGER PRIMARY KEY AUTOINCREMENT,name)"; public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_SQL); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // TODO Auto-generated method stub } }
-
在
MainActivity.java
同一目录下添加一个自定义ContentProvider
类,实现onCreate()
,getType()
NameContentProvider.java
package cn.twle.android.customprovider; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; public class NameContentProvider extends ContentProvider { //初始化一些常量 private static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); private DBOpenHelper dbOpenHelper; //为了方便直接使用UriMatcher,这里addURI,下面再调用Matcher进行匹配 static{ matcher.addURI("cn.twle.android.providers.msprovider", "test", 1); } @Override public boolean onCreate() { dbOpenHelper = new DBOpenHelper(this.getContext(), "test.db", null, 1); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { switch(matcher.match(uri)) { //把数据库打开放到里面是想证明uri匹配完成 case 1: SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); long rowId = db.insert("test", null, values); if(rowId > 0) { //在前面已有的Uri后面追加ID Uri nameUri = ContentUris.withAppendedId(uri, rowId); //通知数据已经发生改变 getContext().getContentResolver().notifyChange(nameUri, null); return nameUri; } } return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
-
修改
AndroidManifest.xml
中为ContentProvider
进行注册<provider android:name="cn.twle.android.customprovider.NameContentProvider" android:authorities="cn.twle.android.providers.msprovider" android:exported="true" />
说明:android:authorities 就是uri 地址,
android:exported="true"
可以被其他app访问 -
修改
activity_main.xml
这里我们就不创建新项目了,直接用一个 App 完成所有的动作
<?xml version="1.0" encoding="utf-8" ?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical" > <Button android:text="插入数据" android:id="@+id/btn_insert" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
-
修改
MainActivity.java
实现 ContentResolver 的部分,点击按钮插入一条数据这里我们就不创建新项目了,直接用一个 App 完成所有的动作
package cn.twle.android.customprovider; import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private Button btn_insert; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_insert = (Button) findViewById(R.id.btn_insert); //读取contentprovider 数据 final ContentResolver resolver = this.getContentResolver(); btn_insert.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ContentValues values = new ContentValues(); values.put("name", "测试"); Uri uri = Uri.parse("content://cn.twle.android.providers.msprovider/test"); resolver.insert(uri, values); Toast.makeText(getApplicationContext(), "数据插入成功", Toast.LENGTH_SHORT).show(); } }); } }
-
运行 APP ,点击插入数据,然后打开 file exploer 将 ContentProvider 的 db 数据库取出,用图形查看工具查看即可发现插入数据
3、BroadcastReceiver(广播接收器)
3.1. 定义
即 广播,是一个全局的监听器,属于Android
四大组件之一
Android
广播分为两个角色:广播发送者、广播接收者
3.2. 作用
监听 / 接收 应用 App
发出的广播消息,并 做出响应
3.3. 应用场景
Android
不同组件间的通信(含 :应用内 / 不同应用之间)- 多线程通信
- 与
Android
系统在特定情况下的通信
如:电话呼入时、网络可用时
3.4. 实现原理
4.1 采用的模型
Android
中的广播使用了设计模式中的观察者模式:基于消息的发布 / 订阅事件模型
因此,Android将广播的发送者 和 接收者 解耦,使得系统方便集成,更易扩展
4.2 模型讲解
- 模型中有3个角色:
- 消息订阅者(广播接收者)
- 消息发布者(广播发布者)
- 消息中心(
AMS
,即Activity Manager Service
)
- 示意图 & 原理如下
示意图
3.5. 使用流程
- 使用流程如下:
示意图
- 下面,我将一步步介绍如何使用
BroadcastReceiver
即上图中的 开发者手动完成部分
5.1 自定义广播接收者BroadcastReceiver
- 继承
BroadcastReceivre
基类 - 必须复写抽象方法
onReceive()
方法
- 广播接收器接收到相应广播后,会自动回调
onReceive()
方法- 一般情况下,
onReceive
方法会涉及 与 其他组件之间的交互,如发送Notification
、启动Service
等- 默认情况下,广播接收器运行在
UI
线程,因此,onReceive()
方法不能执行耗时操作,否则将导致ANR
- 代码范例
mBroadcastReceiver.java
// 继承BroadcastReceivre基类
public class mBroadcastReceiver extends BroadcastReceiver {
// 复写onReceive()方法
// 接收到广播后,则自动调用该方法
// BroadcastReceiver: 也属于UI线程.onReceiver()方法中不能进行耗时操作,否则会导致ANR异常
@Override
public void onReceive(Context context, Intent intent) {
//写入接收广播后的操作
}
}
5.2 广播接收器注册
注册的方式分为两种:静态注册、动态注册
5.2.1 静态注册
- 注册方式:在AndroidManifest.xml里通过****标签声明
- 属性说明:
<receiver
android:enabled=["true" | "false"]
//此broadcastReceiver能否接收其他App的发出的广播
//默认值是由receiver中有无intent-filter决定的:如果有intent-filter,默认值为true,否则为false
android:exported=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
//继承BroadcastReceiver子类的类名
android:name=".mBroadcastReceiver"
//具有相应权限的广播发送者发送的广播才能被此BroadcastReceiver所接收;
android:permission="string"
//BroadcastReceiver运行所处的进程
//默认为app的进程,可以指定独立的进程
//注:Android四大基本组件都可以通过此属性指定自己的独立进程
android:process="string" >
//用于指定此广播接收器将接收的广播类型
//本示例中给出的是用于接收网络状态改变时发出的广播
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
- 注册示例
<receiver
//此广播接收者类是mBroadcastReceiver
android:name=".mBroadcastReceiver" >
//用于接收网络状态改变时发出的广播
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
当此 App
首次启动时,系统会自动实例化mBroadcastReceiver
类,并注册到系统中。
5.2.2 动态注册
- 注册方式:在代码中调用
Context.registerReceiver()
方法 - 具体代码如下:
// 选择在Activity生命周期方法中的onResume()中注册
@Override
protected void onResume(){
super.onResume();
// 1. 实例化BroadcastReceiver子类 & IntentFilter
mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
// 2. 设置接收广播的类型
intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
// 3. 动态注册:调用Context的registerReceiver()方法
registerReceiver(mBroadcastReceiver, intentFilter);
}
// 注册广播后,要在相应位置记得销毁广播
// 即在onPause() 中unregisterReceiver(mBroadcastReceiver)
// 当此Activity实例化时,会动态将MyBroadcastReceiver注册到系统中
// 当此Activity销毁时,动态注册的MyBroadcastReceiver将不再接收到相应的广播。
@Override
protected void onPause() {
super.onPause();
//销毁在onResume()方法中的广播
unregisterReceiver(mBroadcastReceiver);
}
}
特别注意
- 动态广播最好在
Activity
的onResume()
注册、onPause()
注销。 - 原因:
- 对于动态广播,有注册就必然得有注销,否则会导致内存泄露
重复注册、重复注销也不允许
Activity
生命周期如下:
Activity生命周期
Activity生命周期的方法是成对出现的:
- onCreate() & onDestory()
- onStart() & onStop()
- onResume() & onPause()
在onResume()注册、onPause()注销是因为onPause()在App死亡前一定会被执行,从而保证广播在App死亡前一定会被注销,从而防止内存泄露。
- 不在onCreate() & onDestory() 或 onStart() & onStop()注册、注销是因为:
当系统因为内存不足(优先级更高的应用需要内存,请看上图红框)要回收Activity占用的资源时,Activity在执行完onPause()方法后就会被销毁,有些生命周期方法onStop(),onDestory()就不会执行。当再回到此Activity时,是从onCreate方法开始执行。- 假设我们将广播的注销放在onStop(),onDestory()方法里的话,有可能在Activity被销毁后还未执行onStop(),onDestory()方法,即广播仍还未注销,从而导致内存泄露。
- 但是,onPause()一定会被执行,从而保证了广播在App死亡前一定会被注销,从而防止内存泄露。
5.2.3 两种注册方式的区别
示意图
5.3 广播发送者向AMS发送广播
5.3.1 广播的发送
- 广播 是 用”意图(
Intent
)“标识 - 定义广播的本质 = 定义广播所具备的“意图(
Intent
)” - 广播发送 = 广播发送者 将此广播的“意图(
Intent
)”通过sendBroadcast()方法发送出去
5.3.2 广播的类型
广播的类型主要分为5类:
- 普通广播(
Normal Broadcast
) - 系统广播(
System Broadcast
) - 有序广播(
Ordered Broadcast
) - 粘性广播(
Sticky Broadcast
) - App应用内广播(
Local Broadcast
)
具体说明如下:
1. 普通广播(Normal Broadcast)
即 开发者自身定义 intent
的广播(最常用)。发送广播使用如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void send(View view){
Intent intent = new Intent();
//对应BroadcastReceiver中intentFilter的action
intent.setAction("BROADCAST_ACTION");
// 发送广播 ----> mBroadcastReceiver
sendBroadcast(intent);
}
}
- 若被注册了的广播接收者中注册时
intentFilter
的action
与上述匹配,则会接收此广播(即进行回调onReceive()
)。如下mBroadcastReceiver
则会接收上述广播
public class mBroadcastReceiver extends BroadcastReceiver {
// BroadcastReceiver: 四大组件之一
// BroadcastReceiver: 也属于UI线程.onReceiver()方法中不能进行耗时操作,否则会导致ANR异常
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "接收到了广播", Toast.LENGTH_SHORT).show();
}
}
<receiver
//此广播接收者类是mBroadcastReceiver
android:name=".mBroadcastReceiver" >
<!-- 用于接收网络状态改变时发出的广播 -->
<!-- 意图过滤器 -->
<intent-filter>
<action android:name="BROADCAST_ACTION" />
</intent-filter>
</receiver>
- 若发送广播有相应权限,那么广播接收者也需要相应权限
- **中止广播 **
public class Receiver02 extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "这里是有序广播接收器", Toast.LENGTH_SHORT).show();
// 中止广播
abortBroadcast();
}
}
2. 系统广播(System Broadcast)
- Android中内置了多个系统广播:只要涉及到手机的基本操作(如开机、网络状态变化、拍照等等),都会发出相应的广播
- 每个广播都有特定的Intent - Filter(包括具体的action),Android系统广播action如下:
系统操作 | action |
---|---|
监听网络变化 | android.net.conn.CONNECTIVITY_CHANGE |
关闭或打开飞行模式 | Intent.ACTION_AIRPLANE_MODE_CHANGED |
充电时或电量发生变化 | Intent.ACTION_BATTERY_CHANGED |
电池电量低 | Intent.ACTION_BATTERY_LOW |
电池电量充足(即从电量低变化到饱满时会发出广播 | Intent.ACTION_BATTERY_OKAY |
系统启动完成后(仅广播一次) | Intent.ACTION_BOOT_COMPLETED |
按下照相时的拍照按键(硬件按键)时 | Intent.ACTION_CAMERA_BUTTON |
屏幕锁屏 | Intent.ACTION_CLOSE_SYSTEM_DIALOGS |
设备当前设置被改变时(界面语言、设备方向等) | Intent.ACTION_CONFIGURATION_CHANGED |
插入耳机时 | Intent.ACTION_HEADSET_PLUG |
未正确移除SD卡但已取出来时(正确移除方法:设置–SD卡和设备内存–卸载SD卡) | Intent.ACTION_MEDIA_BAD_REMOVAL |
插入外部储存装置(如SD卡) | Intent.ACTION_MEDIA_CHECKING |
成功安装APK | Intent.ACTION_PACKAGE_ADDED |
成功删除APK | Intent.ACTION_PACKAGE_REMOVED |
重启设备 | Intent.ACTION_REBOOT |
屏幕被关闭 | Intent.ACTION_SCREEN_OFF |
屏幕被打开 | Intent.ACTION_SCREEN_ON |
关闭系统时 | Intent.ACTION_SHUTDOWN |
重启设备 | Intent.ACTION_REBOOT |
注:当使用系统广播时,只需要在注册广播接收者时定义相关的action即可,并不需要手动发送广播,当系统有相关操作时会自动进行系统广播
3. 有序广播(Ordered Broadcast)
- 定义
发送出去的广播被广播接收者按照先后顺序接收
有序是针对广播接收者而言的
-
广播接受者接收广播的顺序规则(同时面向静态和动态注册的广播接受者)
- 按照Priority属性值从大-小排序;
- Priority属性相同者,动态注册的广播优先;
<receiver android:name=".MyReceiver" android:exported="true"> <!-- 意图过滤器 --> <intent-filter android:priority="50"> <action android:name="receiver"/> </intent-filter> </receiver> <!-- priority:优先级,值越大,约优先接收广播信息 --> <receiver android:name=".Receiver02" android:exported="true"> <intent-filter android:priority="100"> <action android:name="receiver"/> </intent-filter> </receiver>
-
特点
- 接收广播按顺序接收
- 先接收的广播接收者可以对广播进行截断,即后接收的广播接收者不再接收到此广播;
- 先接收的广播接收者可以对广播进行修改,那么后接收的广播接收者将接收到被修改后的广播
-
具体使用
有序广播的使用过程与普通广播非常类似,差异仅在于广播的发送方式:
sendOrderedBroadcast(intent,null);
4. App应用内广播(Local Broadcast)
- 背景
Android中的广播可以跨App直接通信(exported对于有intent-filter情况下默认值为true) - 冲突
可能出现的问题:- 其他App针对性发出与当前App intent-filter相匹配的广播,由此导致当前App不断接收广播并处理;
- 其他App注册与当前App一致的intent-filter用于接收广播,获取广播具体信息;
即会出现安全性 & 效率性的问题。
- 解决方案
使用App应用内广播(Local Broadcast)
- App应用内广播可理解为一种局部广播,广播的发送者和接收者都同属于一个App。
- 相比于全局广播(普通广播),App应用内广播优势体现在:安全性高 & 效率高
-
具体使用1 - 将全局广播设置成局部广播
- 注册广播时将exported属性设置为false,使得非本App内部发出的此广播不被接收;
- 在广播发送和接收时,增设相应权限permission,用于权限验证;
- 发送广播时指定该广播接收器所在的包名,此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。
通过**intent.setPackage(packageName)**指定报名
-
具体使用2 - 使用封装好的LocalBroadcastManager类
使用方式上与全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将参数的context变成了LocalBroadcastManager的单一实例
注:对于LocalBroadcastManager方式发送的应用内广播,只能通过LocalBroadcastManager动态注册,不能静态注册
//注册应用内广播接收器
//步骤1:实例化BroadcastReceiver子类 & IntentFilter mBroadcastReceiver
mBroadcastReceiver = new mBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
//步骤2:实例化LocalBroadcastManager的实例
localBroadcastManager = LocalBroadcastManager.getInstance(this);
//步骤3:设置接收广播的类型
intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
//步骤4:调用LocalBroadcastManager单一实例的registerReceiver()方法进行动态注册
localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);
//取消注册应用内广播接收器
localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
//发送应用内广播
Intent intent = new Intent();
intent.setAction(BROADCAST_ACTION);
localBroadcastManager.sendBroadcast(intent);
5. 粘性广播(Sticky Broadcast)
由于在Android5.0 & API 21中已经失效,所以不建议使用,在这里也不作过多的总结。
5.3.3、普通广播和有序广播的区别
- 普通广播:异步执行 --> 广播接收器接收广播没有先后顺序;效率较高,无法被拦截
- 有序广播:同步执行 --> 同一时刻只会有一个广播接收器能够接收到信息;根据优先级有先后顺序;可以被拦截
3.6. 特别注意
对于不同注册方式的广播接收器回调OnReceive(Context context,Intent intent)中的context返回值是不一样的:
- 对于静态注册(全局+应用内广播),回调onReceive(context, intent)中的context返回值是:ReceiverRestrictedContext;
- 对于全局广播的动态注册,回调onReceive(context, intent)中的context返回值是:Activity Context;
- 对于应用内广播的动态注册(LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Application Context。
- 对于应用内广播的动态注册(非LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Activity Context;
4、Service
4.1、Service概念及用途
通常service用来执行一些耗时操作,或者后台执行不提供用户交互界面的操作。其他的应用组件可以启动Service,即便用户切换
了其他应用,启动的Service仍可在后台运行。一个组件可以与Service绑定并与之交互,甚至是跨进程通信(IPC)。例如,一个
Service可以在后台执行网络请求、播放音乐、执行文件读写操作或者与 content provider交互等。
4.2、Service生命周期
为了创建Service,需要继承Service类。并重写它的回调方法,这些回调方法反应了Service的生命周期,并提供了绑定Service的
机制。最重要的Service的生命周期回调方法如下所示:
-
onStartCommand()
当其他组件调用startService()方法请求启动Service时,该方法被回调。一旦Service启动,它会在后台独立运行。当Service执行完以后,需调用stopSelf() 或 stopService()方法停止Service。(若您只希望bind Service,则无需调用这些方法)
-
onBind()
当其他组件调用bindService()方法请求绑定Service时,该方法被回调。该方法返回一个IBinder接口,该接口是Service与绑定的组件进行交互的桥梁。若Service未绑定其他组件,该方法应返回null。
-
onCreate()
当Service第一次创建时,回调该方法。该方法只被回调一次,并在onStartCommand() 或 onBind()方法被回调之前执行。若Service处于运行状态,该方法不会回调。
-
onDestroy()
当Service被销毁时回调,在该方法中应清除一些占用的资源,如停止线程、结束绑定注册的监听器或broadcast receiver 等。该方法是Service中的最后一个回调。
如果某个组件通过调用startService()启动了Service(系统会回调onStartCommand()方法),那么直到在Service中手动调用
stopSelf()方法、或在其他组件中手动调用stopService()方法,该Service才会停止。
如果某个组件通过调用bindService()绑定了Service(系统会回调onBind()方法),只要该组件与Service处于绑定状态,Service就
会一直运行,当Service不再与组件绑定时,该Service将被destroy。
上面两条路径并不是毫不相干的:当调用startService()后,您仍可以bind该Service。比如,当播放音乐时,需调用startService()启
动指定播放的音乐,当需要获取该音乐的播放进度时,则需要调用bindService(),在这种情况下,直到Service被unbind ,调用
stopService() 或stopSelf()都不能停止该Service。
当系统内存低时,系统将强制停止Service的运行;若Service绑定了正在与用户交互的activity,那么该Service将不大可能被系统
kill。如果创建的是前台Service,那么该Service几乎不会被kill。否则,当创建了一个长时间在后台运行的Service后,系统会降低
该Service在后台任务栈中的级别——这意味着它容易被kill,所以在开发Service时,需要使Service变得容易被restart,因为一旦
Service被kill,再restart它需要其资源可用时才行,当然这也取决于onStartCommand()方法返回的值。
onStartCommand()方法必须返回一个整数,这个整数是一个描述了在系统的kill事件中,系统应该如何继续这个服务的值。
onStartCommand()有4种返回值:
-
START_STICKY
若系统在onStartCommand()执行并返回后kill了service,那么service会被recreate并回调onStartCommand()。注意不要重新传递最后一个Intent。相反,系统回调onStartCommand()时回传一个空的Intent,除非有 pending intents传递,否则Intent将为null。该模式适合做一些类似播放音乐的操作。
-
START_NOT_STICKY
“非粘性的”。若执行完onStartCommand()方法后,系统就kill了service,不要再重新创建service,除非系统回传了一个pending intent。这避免了在不必要的时候运行service,您的应用也可以restart任何未完成的操作。
-
START_REDELIVER_INTENT
若系统在onStartCommand()执行并返回后kill了service,那么service会被recreate并回调onStartCommand()并将最后一个Intent回传至该方法。任何 pending intents都会被轮流传递。该模式适合做一些类似下载文件的操作。
-
START_STICKY_COMPATIBILITY
START_STICKY的兼容版本,但不保证服务被kill后一定能重启。
4.3、Service的注册
在manifest文件中注册service的方式如下:
<manifest
...
<application
...
<service android:name="com.hx.servicetest.MyService" />
</application>
</manifest>
除此之外,在<\service>标签中还可以配置其他属性
- android:name —>服务全限定类名(唯一不可缺省的)
- android:label —>服务的名字,如果此项不设置,那么默认显示的服务名则为类名
- android:icon —>服务的图标
- android:permission —>申明此服务的权限,这意味着只有提供了该权限的应用才能控制或连接此服务
- android:process —>表示该服务是否运行在另外一个进程,如果设置了此项,那么将会在包名后面加上这段字符串表示另一进程的名字
- android:enabled —>如果此项设置为 true,那么 Service 将会默认被系统启动,不设置默认此项为 false
- android:exported —>表示该服务是否能够被其他应用程序所控制或连接,不设置默认此项为 false
Service的启动还可以使用隐式Intent,在<\service>中配置intent-filter即可,则Service可以响应带有指定action的Intent。
<service android:name="com.hx.servicetest.MyRemoteService" >
<intent-filter>
<action android:name="com.hx.servicetest.MyAIDLService"/>
</intent-filter>
</service>
4.4、Service的启动
有了 Service 类我们如何启动他呢,有两种方法:
- Context.startService()
- Context.bindService()
当然,service也可以同时在上述两种方式下运行。这涉及到Service中两个回调方法的执行:onStartCommand()(通过start方式启
动一个service时回调的方法)、onBind()(通过bind方式启动一个service回调的方法)。
无论通过那种方式启动service(start、bind、start&bind),任何组件(甚至其他应用的组件)都可以使用service。并通过Intent
传递参数。当然,您也可以将Service在manifest文件中配置成私有的,不允许其他应用访问。
startService
其他组件调用startService()方法启动一个Service。一旦启动,Service将一直运行在后台即便启动Service的组件已被destroy。通
常,一个被start的Service会在后台执行单独的操作,也并不给启动它的组件返回结果。比如说,一个start的Service执行在后台下
载或上传一个文件的操作,完成之后,Service应自己停止。
一般使用如下两种方式创建一个start Service:
-
继承Service类
请务必在Service中开启线程来执行耗时操作,因为Service运行在主线程中。
-
继承IntentService类
IntentService继承于Service,若Service不需要同时处理多个请求,那么使用IntentService将是最好选择:您只需要重写
onHandleIntent()方法,该方法接收一个回传的Intent参数,您可以在方法内进行耗时操作,因为它默认开启了一个子线程,操
作执行完成后也无需手动调用stopSelf()方法,onHandleIntent()会自动调用该方法。
(1)继承Service类
如果你需要在Service中执行多线程而不是处理一个请求队列,那么需要继承Service类,分别处理每个Intent。在Service中执行操
作时,处理每个请求都需要开启一个新线程(new Thread()),并且同一时刻一个线程只能处理一个请求
public class MyService extends Service {
private HandlerThread mThread;
private Handler mHandler;
@Override
public void onCreate() {
super.onCreate();
MainActivity.showlog("onCreate()");
initBackThread();
}
private void initBackThread() {
mThread = new HandlerThread("ServiceStartArguments");
mThread.start();
mHandler = new Handler(mThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
//模拟耗时线程操作
MainActivity.showlog("processing...msg.arg1="+msg.arg1);
try {
Thread.sleep(5000);
} catch (Exception e) {
Thread.currentThread().interrupt();
}
MainActivity.showlog("stopSelf...msg.arg1="+msg.arg1);
//当所有操作完成后,服务自己停止
stopSelf(msg.arg1);
}
};
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MainActivity.showlog("onStartCommand()");
Message msg = mHandler.obtainMessage();
msg.arg1 = startId;
mHandler.sendMessage(msg);
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
MainActivity.showlog("onDestroy()");
}
@Override
public IBinder onBind(Intent intent) {
MainActivity.showlog("onBind()");
return null;
}
}
添加Button点击事件:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
showlog("click Start Service button");
Intent it1 = new Intent(this, MyService.class);
startService(it1);
break;
case R.id.stop_service:
showlog("click Stop Service button");
Intent it2 = new Intent(this, MyService.class);
stopService(it2);
break;
default:
break;
}
}
这样的话,一个简单的带有Service功能的程序就写好了,现在我们将程序运行起来,并点击一下Start Service按钮,可以看到
LogCat的打印日志如下:
流程:点击->onCreate->onStartCommand->耗时事件处理(5S)->onDestroy
那么如果我连续两次点击Start Service按钮呢?这个时候的打印日志如下:
流程:第一次点击->onCreate->onStartCommand->耗时事件处理(5S)
第二次点击->onStartCommand->耗时事件处理(5S)->onDestroy
这里的耗时处理是借助HandlerThread来实现的,多个任务在同一个线程中,第一个事件处理完成后再进行第二个事件处理,直到最后一个任务处理完毕,才会停止Service,使用的是stopSelf(int startId);关于stopSelf的使用,可以参考后面 Service的销毁 一节内容。
点击Start Service然后再点击Stop Service按钮就可以将MyService立即停止掉了,Log如下:
注:多个启动Service的请求可能导致onStartCommand()多次调用,但只需调用stopSelf() 、 stopService()这两个方法之一,就可
立即停止该服务。
上面如果我们的耗时任务时间够长,在MyService停止之前点击”返回”,Activity被干掉了,但是我们的服务仍然在运行,可以查看
设置–>应用–>正在运行,截图如下:
(2)继承IntentService类
在大多数情况下,start Service并不会同时处理多个请求,因为处理多线程较为危险,所以继承IntentService类带创建Service是个
不错选择。
使用IntentService的要点如下:
- 默认在子线程中处理回传到onStartCommand()方法中的Intent;
- 在重写的onHandleIntent()方法中处理按时间排序的Intent队列,所以不用担心多线程(multi-threading)带来的问题。
- 当所有请求处理完成后,自动停止service,无需手动调用stopSelf()方法;
- 默认实现了onBind()方法,并返回null;
- 默认实现了onStartCommand()方法,并将回传的Intent以序列的形式发送给onHandleIntent(),您只需重写该方法并处理Intent即可。
综上所述,您只需重写onHandleIntent()方法即可,当然,还需要创建一个构造方法,示例如下:
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}
@Override
public void onHandleIntent(Intent intent) {
//模拟耗时线程操作
MainActivity.showlog("processing...");
try {
Thread.sleep(5000);
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
}
如果您还希望在IntentService的继承类中重写其他生命周期方法,如onCreate()、onStartCommand() 或 onDestroy(),那么请先调
用各自的父类方法以保证子线程能够正常启动。
比如,要实现onStartCommand()方法,需返回其父类方法:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MainActivity.showlog("onStartCommand()");
return super.onStartCommand(intent, flags, startId);
}
注:除onHandleIntent()外,onBind()方法也无需调用其父类方法。
bindService
上面学习了startService()启动 Service,不过这样的话Service和Activity的关系并不大,只是Activity通知了Service一下:“你可以启
动了。”然后Service就去忙自己的事情了。那么有没有什么办法能让它们俩的关联更多一些呢?比如说在Activity中可以指定让
Service去执行什么任务,当然可以,只需要让Activity和Service建立关联就好了。
bindService()方法的意思是,把这个 Service 和调用 Service 的客户类绑起来,如果这个客户类被销毁,Service 也会被销毁。用
这个方法的一个好处是,bindService() 方法执行后 Service 会回调 onBind() 方法,你可以从这里返回一个实现了 IBind 接口的
类,在客户端操作这个类就能和这个服务通信了,比如得到 Service 运行的状态或其他操作。如果 Service 还没有运行,使用这个
方法启动 Service 就会 onCreate() 方法而不会调用 onStartCommand()。
观察MyService中的代码,你会发现有一个onBind()方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的,重新写
一个MyBindService,如下所示:
public class MyBindService extends Service {
private MyBinder mBinder = new MyBinder();
@Override
public void onCreate() {
super.onCreate();
MainActivity.showlog("onCreate()");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MainActivity.showlog("onStartCommand()");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
MainActivity.showlog("onDestroy()");
}
//Service自定义方法
public void doSomethingInService(){
MainActivity.showlog("doSomethingInService()");
}
//复写onBind方法,并且返回IBinder的实现类
@Override
public IBinder onBind(Intent intent) {
MainActivity.showlog("onBind()");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
MainActivity.showlog("onUnbind()");
return super.onUnbind(intent);
}
//内部类,扩展自Binder类
class MyBinder extends Binder {
//MyBinder自定义方法
public void doSomethingInBinder() {
MainActivity.showlog("doSomethingInBinder()");
}
public MyBindService getService(){
return MyBindService.this;
}
}
}
这里我们新增了一个MyBinder类继承自Binder类,然后在MyBinder中添加了一个doSomethingInBinder()方法用于在后台执行任
务,而且在Service中还写了一个doSomethingInService()方法,同样可以执行后台任务,其实这里只是打印了一行日志。
接下来再修改MainActivity中的代码,让MainActivity和MyBindService之间建立关联,代码如下所示:
public class MainActivity extends Activity implements OnClickListener {
private Button startService;
private Button stopService;
private Button startIntentService;
private Button stopIntentService;
private Button bindService;
private Button unbindService;
private MyBindService.MyBinder myBinder;
private boolean isConnected = false;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
showlog("onServiceDisconnected");
isConnected = false;
}
@Override
public void onServiceConnected(ComponentName name, IBinder iBinder) {
showlog("onServiceConnected");
myBinder = (MyBindService.MyBinder) iBinder;
myBinder.doSomethingInBinder();
MyBindService service = myBinder.getService();
service.doSomethingInService();
isConnected = true;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService = (Button) findViewById(R.id.start_service);
stopService = (Button) findViewById(R.id.stop_service);
startIntentService = (Button) findViewById(R.id.start_intent_service);
stopIntentService = (Button) findViewById(R.id.stop_intent_service);
bindService = (Button) findViewById(R.id.bind_service);
unbindService = (Button) findViewById(R.id.unbind_service);
startService.setOnClickListener(this);
stopService.setOnClickListener(this);
startIntentService.setOnClickListener(this);
stopIntentService.setOnClickListener(this);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
showlog("click Start Service button");
Intent it1 = new Intent(this, MyService.class);
startService(it1);
break;
case R.id.stop_service:
showlog("click Stop Service button");
Intent it2 = new Intent(this, MyService.class);
stopService(it2);
break;
case R.id.start_intent_service:
showlog("click Start IntentService button");
Intent it3 = new Intent(this, MyIntentService.class);
startService(it3);
break;
case R.id.stop_intent_service:
showlog("click Stop IntentService button");
Intent it4 = new Intent(this, MyIntentService.class);
stopService(it4);
break;
case R.id.bind_service:
showlog("click Bind Service button");
Intent it5 = new Intent(this, MyBindService.class);
bindService(it5, connection, BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
showlog("click Unbind Service button");
if(isConnected){
unbindService(connection);
}
break;
default:
break;
}
}
public static void showlog(String info) {
System.out.print("Watson "+info+"\n");
}
}
可以看到,这里我们首先创建了一个ServiceConnection的匿名类,在里面重写了onServiceConnected()方法和
onServiceDisconnected()方法,这两个方法分别会在Activity与Service建立关联和解除关联的时候调用。在onServiceConnected()
方法中,我们又通过向下转型得到了MyBinder的实例,有了这个实例,Activity和Service之间的关系就变得非常紧密了。现在我们
可以在Activity中根据具体的场景来调用MyBinder中的任何public方法,即实现了Activity指挥Service干什么Service就去干什么的功
能。
当然,现在Activity和Service其实还没关联起来了呢,这个功能是在Bind Service按钮的点击事件里完成的。可以看到,这里我们
仍然是构建出了一个Intent对象,然后调用bindService()方法将Activity和Service进行绑定。bindService()方法接收三个参数,第一
个参数就是刚刚构建出的Intent对象,第二个参数是前面创建出的ServiceConnection的实例,第三个参数是一个标志位,有两个
flag, BIND_DEBUG_UNBIND 与 BIND_AUTO_CREATE,前者用于调试,后者默认使用,这里传入BIND_AUTO_CREATE表示
在Activity和Service建立关联后自动创建Service,这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不
会执行。
现在让我们重新运行一下程序吧,在MainActivity中点击一下Bind Service按钮,LogCat里的打印日志如下图所示:
由于在绑定Service的时候指定的标志位是BIND_AUTO_CREATE,说明点击Bind Service按钮的时候Service也会被创建,这时应
该怎么销毁Service呢?其实也很简单,点击一下Unbind Service按钮,将Activity和Service的关联解除就可以了。Log如下:
另外需要注意,任何一个Service在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity建立关联,还可以和任
何一个Activity建立关联,而且在建立关联时它们都可以获取到相同的MyBinder实例。
4.5、Service的销毁
stopService/unbindService
在Service的启动这一部分,我们已经简单介绍了销毁Service的方法。
startService—>stopService
bindService—>unbindService
以上这两种销毁的方式都很好理解。
但有几点需要注意一下:
(1)你应当知道在调用 bindService 绑定到Service的时候,你就应当保证在某处调用 unbindService 解除绑定(尽管 Activity 被
finish 的时候绑定会自动解除,并且Service会自动停止);
(2)你应当注意使用 startService 启动服务之后,一定要使用 stopService停止服务,不管你是否使用bindService;
(3)同时使用 startService 与 bindService 要注意到,Service 的终止,需要unbindService与stopService同时调用,才能终止
Service,不管 startService 与 bindService 的调用顺序,如果先调用 unbindService 此时服务不会自动终止,再调用 stopService
之后服务才会停止,如果先调用 stopService 此时服务也不会终止,而再调用 unbindService 或者之前调用 bindService 的
Context 不存在了(如Activity 被 finish 的时候)之后服务才会自动停止;
(4)当在旋转手机屏幕的时候,当手机屏幕在“横”“竖”变换时,此时如果你的 Activity 如果会自动旋转的话,旋转其实是 Activity
的重新创建,因此旋转之前的使用 bindService 建立的连接便会断开(Context 不存在了),对应服务的生命周期与上述相同。
(5)unbindService 解除绑定,参数为之前创建的 ServiceConnection 接口对象。另外,多次调用 unbindService 来释放相同的
连接会抛出异常,因此我创建了一个 boolean 变量来判断是否 unbindService 已经被调用过。
stopSelf
对于StartService启动的服务,Service本身还提供了另外一个方法让自己停止—>stopSelf。
若系统正在处理多个调用onStartCommand()请求,那么在启动一个请求时,你不应当在此时停止该Service。为了避免这个问题,
您可以调用stopSelf(int)方法,以确保请求停止的Service是最新的启动请求。这就是说,当调用stopSelf(int)方法时,传入的ID代表
启动请求(该ID会传递至onStartCommand()),该ID与请求停止的ID一致。则如果在调用stopSelf(int)之前,Service收到一个新
的Start请求,ID将无法匹配,Service并不会停止。具体的例子参见上面Service启动一节。
...
private void initBackThread() {
mThread = new HandlerThread("ServiceStartArguments");
mThread.start();
mHandler = new Handler(mThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
//线程操作
MainActivity.showlog("processing...msg.arg1="+msg.arg1);
try {
Thread.sleep(5000);
} catch (Exception e) {
Thread.currentThread().interrupt();
}
MainActivity.showlog("stopSelf...msg.arg1="+msg.arg1);
stopSelf(msg.arg1);
}
};
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MainActivity.showlog("onStartCommand()");
Message msg = mHandler.obtainMessage();
msg.arg1 = startId;
mHandler.sendMessage(msg);
return START_STICKY;
}
...
4.6、Service和Thread的区别
Thread我们大家都知道,是用于开启一个子线程,在这里去执行一些耗时操作就不会阻塞主线程的运行。而Service我们最初理解
的时候,总会觉得它是用来处理一些后台任务的,一些比较耗时的操作也可以放在这里运行,这就会让人产生混淆了。但是,如果
我告诉你Service其实是运行在主线程里的,你还会觉得它和Thread有什么关系吗?
在MainActivity的onCreate()方法里加入一行打印当前线程id的Log:
showlog("MainActivity thread id is " + Thread.currentThread().getId());
同时在MyService的onCreate()方法里加入打印当前线程id的Log:
MainActivity.showlog("MyService thread id is " + Thread.currentThread().getId());
现在重新运行一下程序,并点击Start Service按钮,会看到如下Log信息:
可以看到,它们的线程id完全是一样的,由此证实了Service确实是运行在主线程里的,也就是说如果你在Service里编写了非常耗
时的代码,程序也会出现ANR的。
下面我详细的来解释一下:
- Thread:Thread 是程序执行的最小单元,它是分配CPU的基本单位。可以用 Thread 来执行一些异步的操作。
- Service:Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如:onCreate,onStart 这些函数在被系统调用的时候都是在主进程的 main 线程上运行的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。 因此请不要把 Service 理解成线程,它跟线程半毛钱的关系都没有!
Android的后台就是指,它的运行是完全不依赖UI的。即使Activity被销毁,或者程序被关闭,只要进程还在,Service就可以继续运行。比如说一些应用程序,始终需要与服务器之间始终保持着心跳连接,就可以使用Service来实现。你可能又会问,前面不是刚刚验证过Service是运行在主线程里的么?在这里一直执行着心跳连接,难道就不会阻塞主线程的运行吗?当然会,但是我们可以在Service中再创建一个子线程,然后在这里去处理耗时逻辑就没问题了。
既然在Service里也要创建一个子线程,那为什么不直接在Activity里创建呢?这是因为Activity很难对Thread进行控制,当Activity被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个Activity中创建的子线程,另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。因此,使用Service来处理后台任务,Activity就可以放心地finish,完全不需要担心无法对后台任务进行控制的情况。你也可以在 Service 里注册 BroadcastReceiver,在其他地方通过发送 broadcast 来控制它,当然这些都是 Thread 做不到的。
一个比较标准的Service就可以写成:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 开始执行后台任务
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
class MyBinder extends Binder {
public void startDownload() {
new Thread(new Runnable() {
@Override
public void run() {
// 执行具体的下载任务
}
}).start();
}
}
4.7、创建前台Service
Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。前台Service和普通Service最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果,只有前台Service被destroy后,状态栏显示才能消失。当然有时候你也可能不仅仅是为了防止Service被回收才使用前台Service,有些项目由于特殊的需求会要求必须使用前台Service,比如说墨迹天气,它的Service在后台更新天气数据的同时,还会在系统状态栏一直显示当前天气的信息,如下图所示:
来看一下如何才能创建一个前台Service吧,其实并不复杂,修改MyService中的代码,如下所示:
@Override
public void onCreate() {
super.onCreate();
MainActivity.showlog("onCreate()");
initBackThread();
Intent notificationIntent = new Intent(this, MainActivity.class);
/*第二个参数现在不再使用了
第四个参数描述:
FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。
FLAG_NO_CREATE:如果当前系统中存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null。
FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。
FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras*/
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setWhen(System.currentTimeMillis())
.setTicker("有通知到来")
.setContentTitle("这是通知的标题")
.setContentText("这是通知的内容")
.setOngoing(true)
.setContentIntent(pendingIntent)
.build();
/*使用startForeground,如果id为0,那么notification将不会显示*/
startForeground(1, notification);
}
这里只是修改了MyService中onCreate()方法的代码。可以看到,我们创建了一个Notification对象,然后设置了它的布局和数据,并在这里设置了点击通知后就打开MainActivity。然后调用startForeground()方法就可以让MyService变成一个前台Service,并会将通知的图片显示出来。
现在重新运行一下程序,并点击Start Service或Bind Service按钮,MyService就会以前台Service的模式启动了,并且在系统状态栏会弹出一个通栏图标,下拉状态栏后可以看到通知的详细内容,如下图所示
可以调用stopForeground(Boolean bool)来移除前台Service。该方法需传入一个boolean型变量,表示是否也一并清除状态栏上的notification。该方法并不停止Service,如果停止正在前台运行的Service,那么notification 也会一并被清除。
最后我们看一下进程的分类:
- 前台进程 Foreground process
- 当前用户操作的Activity所在进程
- 绑定了当前用户操作的Activity的Service所在进程
- 调用了startForeground()的Service 典型场景:后台播放音乐
- 可见进程 Visible process
- 处于暂停状态的Activity
- 绑定到暂停状态的Activity的Service
- 服务进程 Service process
- 通过startService()启动的Service
- 后台进程 Background process
- 处于停止状态的Activity
- 空进程 Empty process
4.8、远程Service的使用
什么是远程Service
从上面可知,Service其实是运行在主线程里的,如果直接在Service中处理一些耗时的逻辑,就会导致程序ANR。让我们来验证一下吧,修改MyService代码,在onCreate()方法中让线程睡眠60秒,如下所示:
@Override
public void onCreate() {
super.onCreate();
MainActivity.showlog("onCreate()");
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
点击一下Start Service按钮或Bind Service按钮,程序就会阻塞住无法进行任何其它操作,过一段时间后就会弹出ANR的提示框,如下图所示:
现在来看看远程Service的用法,如果将MyService转换成一个远程Service,还会不会有ANR的情况呢?让我们来动手尝试一下吧。将一个普通的Service转换成远程Service其实非常简单,只需要在注册Service的时候将它的android:process属性指定成:remote就可以了,代码如下所示:
<service android:name="com.hx.servicetest.MyService"
android:process=":remote"/>
重新运行程序,并点击一下Start Service按钮,你会看到控制台立刻打印了onCreate()的信息,而且主界面并没有阻塞住,也不会出现ANR。大概过了一分钟后,又会看到onStartCommand()打印了出来。
为什么将MyService转换成远程Service后就不会导致程序ANR了呢?这是由于,使用了远程Service后,MyService已经在另外一个进程当中运行了,所以只会阻塞该进程中的主线程,并不会影响到当前的应用程序。
那既然远程Service这么好用,干脆以后我们把所有的Service都转换成远程Service吧,还省得再开启线程了。其实不然,远程Service非但不好用,甚至可以称得上是较为难用。一般情况下如果可以不使用远程Service,就尽量不要使用它。
下面就来看一下它的弊端吧,首先将MyService的onCreate()方法中让线程睡眠的代码去除掉,然后重新运行程序,并点击一下Bind Service按钮,你会发现程序崩溃了!为什么点击Start Service按钮程序就不会崩溃,而点击Bind Service按钮就会崩溃呢?这是由于在Bind Service按钮的点击事件里面我们会让MainActivity和MyService建立关联,但是目前MyService已经是一个远程Service了,Activity和Service运行在两个不同的进程当中,这时就不能再使用传统的建立关联的方式,程序也就崩溃了。
调用远程Service
那么如何才能让Activity与一个远程Service建立关联呢?这就要使用AIDL来进行跨进程通信了(IPC)。
AIDL(Android Interface Definition Language)是Android接口定义语言的意思,它可以用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。
下面我们就来一步步地看一下AIDL的用法到底是怎样的。首先需要新建一个AIDL文件,在这个文件中定义好Activity需要与Service进行通信的方法。新建MyAIDLService.aidl文件,代码如下所示:
package com.hx.servicetest;
interface MyAIDLService {
int plus(int a, int b);
String toUpperCase(String str);
}
点击保存之后,在gen目录下通过aapt就会生成一个对应的Java文件,如下图所示:
然后我们写一个MyRemoteService,在里面实现我们刚刚定义好的MyAIDLService接口,如下所示:
public class MyRemoteService extends Service {
@Override
public void onCreate() {
super.onCreate();
MainActivity.showlog("onCreate()");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MainActivity.showlog("onStartCommand()");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
MainActivity.showlog("onDestroy()");
}
@Override
public IBinder onBind(Intent intent) {
MainActivity.showlog("onBind()");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
MainActivity.showlog("onUnbind()");
return super.onUnbind(intent);
}
MyAIDLService.Stub mBinder = new Stub() {
@Override
public String toUpperCase(String str) throws RemoteException {
if (str != null) {
return str.toUpperCase();
}
return null;
}
@Override
public int plus(int a, int b) throws RemoteException {
return a + b;
}
};
}
这里先是对MyAIDLService.Stub进行了实现,重写里了toUpperCase()和plus()这两个方法。这两个方法的作用分别是将一个字符串全部转换成大写格式,以及将两个传入的整数进行相加。然后在onBind()方法中将MyAIDLService.Stub的实现返回。这里为什么可以这样写呢?因为Stub其实就是Binder的子类,所以在onBind()方法中可以直接返回Stub的实现。
接下来修改MainActivity中的代码,如下所示:
public class MainActivity extends Activity implements OnClickListener {
private Button startService;
private Button stopService;
private Button startIntentService;
private Button stopIntentService;
private Button bindService;
private Button unbindService;
private Button bindRemoteService;
private MyAIDLService myAIDLService;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myAIDLService = MyAIDLService.Stub.asInterface(service);
try {
int result = myAIDLService.plus(7, 8);
String upperStr = myAIDLService.toUpperCase("hello watson");
showlog("result is " + result);
showlog("upperStr is " + upperStr);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService = (Button) findViewById(R.id.start_service);
stopService = (Button) findViewById(R.id.stop_service);
startIntentService = (Button) findViewById(R.id.start_intent_service);
stopIntentService = (Button) findViewById(R.id.stop_intent_service);
bindService = (Button) findViewById(R.id.bind_service);
unbindService = (Button) findViewById(R.id.unbind_service);
bindRemoteService = (Button) findViewById(R.id.bind_remote_service);
startService.setOnClickListener(this);
stopService.setOnClickListener(this);
startIntentService.setOnClickListener(this);
stopIntentService.setOnClickListener(this);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
bindRemoteService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
showlog("click Start Service button");
Intent it1 = new Intent(this, MyService.class);
startService(it1);
break;
case R.id.stop_service:
showlog("click Stop Service button");
Intent it2 = new Intent(this, MyService.class);
stopService(it2);
break;
case R.id.start_intent_service:
showlog("click Start IntentService button");
Intent it3 = new Intent(this, MyIntentService.class);
startService(it3);
break;
case R.id.stop_intent_service:
showlog("click Stop IntentService button");
Intent it4 = new Intent(this, MyIntentService.class);
stopService(it4);
break;
case R.id.bind_service:
showlog("click Bind Service button");
Intent it5 = new Intent(this, MyBindService.class);
bindService(it5, connection, BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
showlog("click Unbind Service button");
if(isConnected == true){
unbindService(connection);
}
break;
case R.id.bind_remote_service:
showlog("click Bind Remote Service button");
Intent it6 = new Intent(this, MyRemoteService.class);
bindService(it6, connection, BIND_AUTO_CREATE);
break;
default:
break;
}
}
public static void showlog(String info) {
System.out.print("Watson "+info+"\n");
}
}
我们只是修改了ServiceConnection中的代码。可以看到,这里首先使用了MyAIDLService.Stub.asInterface()方法将传入的IBinder对象传换成了MyAIDLService对象,接下来就可以调用在MyAIDLService.aidl文件中定义的所有接口了。这里我们先是调用了plus()方法,并传入了7和8作为参数,然后又调用了toUpperCase()方法,并传入hello world字符串作为参数,最后将调用方法的返回结果打印出来。
现在重新运行程序,并点击一下Bind Remote Service按钮,可以看到打印日志如下所示:
由此可见,我们确实已经成功实现跨进程通信了,在一个进程中访问到了另外一个进程中的方法。
不过你也可以看出,目前的跨进程通信其实并没有什么实质上的作用,因为这只是在一个Activity里调用了同一个应用程序的Service里的方法。而跨进程通信的真正意义是为了让一个应用程序去访问另一个应用程序中的Service,以实现共享Service的功能。那么下面我们自然要学习一下,如何才能在其它的应用程序中调用到MyRemoteService里的方法。
看看这里我们bind Service的方式:
Intent it6 = new Intent(this, MyRemoteService.class);
bindService(it6, connection, BIND_AUTO_CREATE);
这里在构建Intent的时候是使用MyRemoteService.class来指定要绑定哪一个Service的,但是在另一个应用程序中去绑定Service的时候并没有MyRemoteService这个类,这时就必须使用到隐式Intent了。现在修改AndroidManifest.xml中的代码,给MyRemoteService加上一个action,如下所示:
<service android:name="com.hx.servicetest.MyRemoteService"
android:exported="true" >
<intent-filter>
<action android:name="com.hx.servicetest.MyAIDLService"/>
</intent-filter>
</service>
这就说明,MyRemoteService可以响应带有com.hx.servicetest.MyAIDLService这个action的Intent。
现在重新运行一下程序,这样就把远程Service端的工作全部完成了。
然后创建一个新的Android项目,起名为ClientTest,我们就尝试在这个程序中远程调用MyRemoteService中的方法。
ClientTest中的Activity如果想要和MyRemoteService建立关联其实也不难,首先需要将MyAIDLService.aidl文件从ServiceTest项目中拷贝过来,注意要将原有的包路径一起拷贝过来,完成后项目的结构如下图所示:
接下来打开MainActivity,在其中加入和MyRemoteService建立关联的代码,如下所示:
public class MainActivity extends Activity implements OnClickListener {
private Button bindRemoteService;
private Button unbindRemoteService;
private boolean isConnected = false;
private MyAIDLService myAIDLService;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
isConnected = false;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
isConnected = true;
myAIDLService = MyAIDLService.Stub.asInterface(service);
try {
int result = myAIDLService.plus(13, 19);
String upperStr = myAIDLService.toUpperCase("hello aidl service");
showlog("result is " + result);
showlog("upperStr is " + upperStr);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindRemoteService = (Button) findViewById(R.id.bind_service);
unbindRemoteService = (Button) findViewById(R.id.unbind_service);
bindRemoteService.setOnClickListener(this);
unbindRemoteService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bind_service:
Intent intent = new Intent("com.hx.servicetest.MyAIDLService");
bindService(intent, connection, BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
if(isConnected){
unbindService(connection);
}
break;
default:
break;
}
}
public static void showlog(String info) {
System.out.print("Watson "+info+"\n");
}
}
这部分代码大家一定非常眼熟吧?没错,这和在ServiceTest的MainActivity中的代码几乎是完全相同的,只是在让Activity和Service建立关联的时候我们使用了隐式Intent,将Intent的action指定成了com.hx.servicetest.MyAIDLService。
在当前Activity和MyService建立关联之后,我们仍然是调用了plus()和toUpperCase()这两个方法,远程的MyService会对传入的参数进行处理并返回结果,然后将结果打印出来。
这样的话,ClientTest中的代码也就全部完成了,现在运行一下这个项目,点击Bind Remote Service按钮,此时就会去和远程的MyRemoteService建立关联,观察LogCat中的打印信息如下所示:
然后点击Unbind Remote Service按钮,此时会去和远程的MyRemoteService断开关联,MyRemoteService也会自定销毁,看Log吧:
更多推荐
所有评论(0)