封面画师:ke-ta 封面ID:56866134
数据存储技术
SharedPreference存储
SharedPreference是Android提供的,用来以最简单的方式对数据进行永久保存的方法。
SharedPreference存储的文件格式及路径:
文件格式:XML文件
存储路径:data > data > <包名> > shared_prefs > 文件
使用SharedPreference存储数据的步骤:
获取SharedPreferences对象,使用getSharedPreferences()
或getPreferences()
getSharedPreferences(String name, int mode):name表示SharedPreferences文件名。mode表示指定的访问权限,它有两个常用属性值,MODE_PRIVATE
被本应用读写,MODE_MULTI_PROCESS
跨应用读写。
getPreferences(int mode):mode表示指定的访问权限,它有两个常用属性值,MODE_PRIVATE
被本应用读写,MODE_MULTI_PROCESS
跨应用读写。
获得SharedPreferences.Editor对象,使用edit()方法。
向SharedPreferences.Editor对象中添加数据。可以使用格式为 put + 数据类型的方法来添加。比如:putBoolean()
、putString()
、 putInt()
提交数据,使用commit()
实现。
使用SharedPreference读取数据的步骤:
获取SharedPreferences对象,使用getSharedPreferences()
或getPreferences()
使用SharedPreferences类提供的getXxx()方法获取数据。可以使用格式为 get+ 数据类型的方法来获取。比如:getBoolean()
、getString()
、 getInt()
模拟QQ自动登录
编写布局资源文件activity_main.xml实现登录主界面布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <LinearLayout ... android:orientation ="vertical" > <EditText android:id ="@+id/username" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:hint ="QQ号/手机号/邮箱" android:layout_marginTop ="150dp" /> <EditText android:id ="@+id/password" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:hint ="密码" android:layout_marginTop ="20dp" android:inputType ="textPassword" /> <Button android:id ="@+id/login" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="登录" android:textSize ="18sp" android:background ="#13A1DA" /> </LinearLayout >
创建新布局资源文件activity_message.xml,模拟登录成功界面:
1 2 3 4 5 6 7 <RelativeLayout ... > <TextView android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="模拟登录成功界面" android:layout_centerInParent ="true" /> </RelativeLayout >
编写MainActivity.java实现主要逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class MainActivity extends AppCompatActivity { private String user = "admin" , pwd = "12345" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText usernameET = findViewById(R.id.username); final EditText passwordET = findViewById(R.id.password); Button login = findViewById(R.id.login); final SharedPreferences sp = getSharedPreferences("mypassword" ,MODE_PRIVATE); String username = sp.getString("username" ,null ); String password = sp.getString("password" ,null ); if (username != null && password != null ){ if (username.equals(user) && password.equals(pwd) ){ Intent intent = new Intent (MainActivity.this ,MessageActivity.class); startActivity(intent); } }else { login.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { String in_username = usernameET.getText().toString(); String in_password = passwordET.getText().toString(); SharedPreferences.Editor editor = sp.edit(); if (in_username.equals(user) && in_password.equals(pwd)){ editor.putString("username" ,in_username); editor.putString("password" ,in_password); editor.commit(); Intent intent = new Intent (MainActivity.this ,MessageActivity.class); startActivity(intent); Toast.makeText(MainActivity.this , "以保存账号和密码" , Toast.LENGTH_SHORT).show(); }else { Toast.makeText(MainActivity.this , "账号或密码错误" , Toast.LENGTH_SHORT).show(); } } }); } } }
测试后,我们可以在模拟器 data—data—<包名>— shared_prefs 下找到保存了账号和密码名为mypassword.xml的文件。
内部存储
Android中文件存储 :通过Java的IO流 来读取磁盘上的文件。
获取输入流:openFileInput()
获取输出流:openFileOutput()
智能手机文件的存储方式:分为内部存储和外部存储。
思考:内部存储就是和电脑自带的硬盘一样,是固定在手机里的,而外部存储就像U盘/移动硬盘。是可以移动的? 事实上并不是那样的 。
无论是内部存储还是外部存储都是在手机内部的,这与我们是否插入了可以移动的SD卡无关 。
内部存储 不是 内存!
内部存储的文件路径:
data —— data —— <包名> —— file —— 内部存储文件。
内部存储的特点
默认只能被创建它的应用访问到;
当这个应用被卸载后,内部存储中的文件也会被删除;
一旦内部存储空间耗尽,手机也会无法使用。
使用内部存储 实现一个简易备忘录
编写布局资源文件activity_main.xml对界面进行布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <RelativeLayout ... > <EditText android:id ="@+id/editText" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginLeft ="10dp" android:layout_marginRight ="10dp" android:layout_marginTop ="50dp" android:gravity ="top" /> <Button android:id ="@+id/btn_save" android:layout_width ="60dp" android:layout_height ="40dp" android:layout_alignParentRight ="true" android:text ="保存" android:textSize ="18sp" /> <Button android:id ="@+id/btn_cancel" android:layout_width ="60dp" android:layout_height ="40dp" android:layout_toLeftOf ="@+id/btn_save" android:text ="取消" android:textSize ="18sp" /> </RelativeLayout >
编写MainActivity.java实现简易备忘录逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public class MainActivity extends AppCompatActivity { byte [] buffer = null ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText editText = findViewById(R.id.editText); Button save = findViewById(R.id.btn_save); Button cancel = findViewById(R.id.btn_cancel); save.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { FileOutputStream fileOutputStream = null ; String text = editText.getText().toString(); try { fileOutputStream = openFileOutput("memo" , MODE_PRIVATE); fileOutputStream.write(text.getBytes()); fileOutputStream.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fileOutputStream != null ) { try { fileOutputStream.close(); Toast.makeText(MainActivity.this , "保存成功" , Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); } } } } }); FileInputStream fileInputStream = null ; try { fileInputStream = openFileInput("memo" ); buffer = new byte [fileInputStream.available()]; fileInputStream.read(buffer); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fileInputStream != null ) { try { fileInputStream.close(); String data = new String (buffer); editText.setText(data); } catch (IOException e) { e.printStackTrace(); } } } cancel.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { finish(); } }); } }
外部存储
外部存储 不是 可移动的SD卡! 外部存储也是存储在手机当中的。
区分内部存储和外部存储:用数据线将手机连接至电脑,能被电脑识别的部分就是外部存储。
读、写外部存储空间上的文件步骤:
获取外部存储目录:使用Environment.getExternalStorageDirectory()
读、写外部存储空间中的文件:读 FileInputStream()
、写 FileOutputStream()
使用外部存储 实现一个简易备忘录
在本案例中使用于 内部存储 相同的布局资源文件 activity_main.xml ,对MainActivity.java 进行修改,并对修改部分进行了标注:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class MainActivity extends AppCompatActivity { byte [] buffer = null ; File file; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText editText = findViewById(R.id.editText); Button save = findViewById(R.id.btn_save); Button cancel = findViewById(R.id.btn_cancel); file = new File (Environment.getExternalStorageDirectory(), "Text.text" ); save.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { FileOutputStream fileOutputStream = null ; String text = editText.getText().toString(); try { fileOutputStream = new FileOutputStream (file); fileOutputStream.write(text.getBytes()); fileOutputStream.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fileOutputStream != null ) { try { fileOutputStream.close(); Toast.makeText(MainActivity.this , "保存成功" , Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); } } } } }); FileInputStream fileInputStream = null ; try { fileInputStream = new FileInputStream (file); buffer = new byte [fileInputStream.available()]; fileInputStream.read(buffer); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fileInputStream != null ) { try { fileInputStream.close(); String data = new String (buffer); editText.setText(data); } catch (IOException e) { e.printStackTrace(); } } } cancel.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { finish(); } }); } }
在本次案例中我们实现的是将备忘录信息信息保存至SD卡上,因此我们需要获取读写SD卡的权限 。
对AndroidManifest.xml进行修改:
1 2 3 4 5 6 7 8 9 10 <manifest ... > <uses-permission android:name ="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name ="android.permission.WRITE_EXTERNAL_STORAGE" /> <application ... android:requestLegacyExternalStorage ="true" > <activity android:name =".MainActivity" > ... </activity > </application > </manifest >
同时我们还需要在模拟器内部授予本案例的应用读写储存空间的权限!
如果案例测试成功,我们可以在模拟器以下目录找到存放备忘录信息的文件:storage/emulated/0/Text.text
数据库存储
本节主要讲述sqlite3的使用。
SQLite 数据库的特点:占用资源少、运行效率高、可移植性强、安全可靠。
操作SQLite 数据库的方式:使用SDK中sqlite3工具(sqlite3.exe)或者使用Java代码。
使用命令提示符操作SQLite
使用时,先确保你的模拟器已经打开!
启动、退出SQLite
首先,我们需要以 管理员权限 打开电脑的命令提示符。
在SDK目录下有:SDK/platform-tools/,我们需要在命令提示符中进入这个目录下,然后输入:
在输入这条命令之前,必须先将你的模拟器打开 !当然,为了便于使用,建议将该目录(SDK/platform-tools/)添加至环境变量中的Path下。
然后输入:
然后打开SQLite ,输入:
除此之外,我们也可以前往SDK目录下的platform-tools目录,找到sqlite3.exe,双击运行也可以打开SQLite 数据库。
如果我们想退出SQLite 的交互模式,我们可以输入:
然后,我们就退出SQLite 的交互模式,返回至shell模式。
创建数据库目录
相当于在你的模拟器指定的目录下创建一个文件夹。
在shell模式 下,输入:
1 mkdir /data/data/com.yang/databese
com.yang是我的包名,你可以根据你自己的包名进行编写。创建成功后,你可以在你模拟器中查看是否有database这个文件夹。
创建和打开数据库
我们在创建数据库目录后,我们这可在这个目录下创建一个数据库。在SQLite数据库当中,每一个数据库保存在一个单独的文件当中。
我们需要先进入到需要创建或打开数据库的目录下,依旧是在shell模式 下,输入:
1 cd /data/data/com.yang/database
进入对应目录后,我们就可以创建或打开数据库了:
使用这条语句,可以打开一个名为db_yang的数据库,如果没有这个数据库,就创建这个数据库。
这时,我们也会发现命令提示行已经进入到 SQLite 的交互模式 。
数据表操作
在 SQLite 的交互模式下 输入以下命令以创建一个名为tb_user的数据表:
1 create table tb_user (id integer primary key autoincrement,name text not null ,pwd text);
如果我们想查看当前数据库包含哪些数据表,我们可以使用:
如果我们想向数据库插入数据:
1 insert into tb_user values (null ,'yang' ,'123' );
查询某个表中的数据:
使用代码操作SQLite
相关操作介绍:
创建数据库:openOrCreateDatabase()
或者 继承SQLiteOpenHelper类
操作数据库:insert()
插入 delete()
删除 update()
更新 query()
查询
使用SQLite实现中英文词典数据库
准备:我们需要准备一张图片按钮所用的图片资源文件,命名为translation,放置于res/drawable目录下。
编写布局资源文件activity_main.xml实现主界面布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <RelativeLayout ... > <ImageButton android:id ="@+id/search_translate" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:src ="@drawable/translation" android:layout_alignParentRight ="true" /> <EditText android:id ="@+id/search_et" android:layout_width ="match_parent" android:layout_height ="300dp" android:layout_below ="@+id/search_translate" android:gravity ="top" android:hint ="请输入你要翻译的内容" /> <ListView android:id ="@+id/result_listView" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_below ="@+id/search_et" /> <Button android:id ="@+id/btn_add" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_below ="@+id/result_listView" android:layout_alignParentRight ="true" android:background ="#0B86D6" android:text ="添加单词" android:textSize ="18sp" /> </RelativeLayout >
编写布局资源文件activity_add.xml实现单词添加界面的布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <LinearLayout ... android:orientation ="vertical" > <EditText android:id ="@+id/add_word" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:paddingTop ="50dp" android:hint ="单词" /> <EditText android:id ="@+id/add_interpret" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:hint ="解释" android:paddingTop ="20dp" /> <LinearLayout android:orientation ="horizontal" android:gravity = "right|bottom" android:layout_width ="match_parent" android:layout_height ="match_parent" > <Button android:id ="@+id/btn_save" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:background ="#0B86D6" android:text ="保存" /> <Button android:id ="@+id/btn_cancel" android:layout_marginStart ="20dp" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:background ="#0B86D6" android:text ="取消" /> </LinearLayout > </LinearLayout >
编写布局资源文件result_main.xml用于显示查询结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 <LinearLayout ... android:orientation ="vertical" > <TextView android:id ="@+id/result_word" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="单词:" /> <TextView android:id ="@+id/result_interpret" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="解释:" /> </LinearLayout >
创建DBOpenHelper.java用于创建数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class DBOpenHelper extends SQLiteOpenHelper { final String CREATE_TABLE_SQL = "create table tb_dict (_id integer primary key autoincrement,word,detail)" ; public DBOpenHelper (@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) { super (context, name, null , version); } @Override public void onCreate (SQLiteDatabase db) { db.execSQL(CREATE_TABLE_SQL); } @Override public void onUpgrade (SQLiteDatabase db, int oldVersion, int newVersion) { Log.i("词典" ,"--版本更新" +oldVersion+"-->" +newVersion); } }
编写MainActivity.java用于实现主界面逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class MainActivity extends AppCompatActivity { private DBOpenHelper dbOpenHelper; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); dbOpenHelper = new DBOpenHelper (MainActivity.this , "db_dict" , null , 1 ); final ListView listView = findViewById(R.id.result_listView); final EditText etSearch = findViewById(R.id.search_et); ImageButton btn_search = findViewById(R.id.search_translate); Button btn_add = findViewById(R.id.btn_add); btn_add.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { Intent intent = new Intent (MainActivity.this , AddActivity.class); startActivity(intent); } }); btn_search.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { String key = etSearch.getText().toString(); Cursor cursor = dbOpenHelper.getReadableDatabase().query("tb_dict" , null , "word=?" , new String []{key}, null , null , null ); ArrayList<Map<String ,String >> resultList = new ArrayList <Map<String ,String >>(); while (cursor.moveToNext()){ Map<String ,String > map = new HashMap <String, String>(); map.put("word" ,cursor.getString(1 )); map.put("interpret" ,cursor.getString(2 )); resultList.add(map); } if (resultList == null || resultList.size() == 0 ){ Toast.makeText(MainActivity.this , "很抱歉,没有先关记录" , Toast.LENGTH_SHORT).show(); }else { SimpleAdapter simpleAdapter = new SimpleAdapter (MainActivity.this ,resultList, R.layout.result_mian, new String []{"word" ,"interpret" },new int []{ R.id.result_word,R.id.result_interpret }); listView.setAdapter(simpleAdapter); } } }); } @Override protected void onDestroy () { super .onDestroy(); if (dbOpenHelper!=null ){ dbOpenHelper.close(); } } }
编写AddActivity.java用于实现单词添加界面逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class AddActivity extends AppCompatActivity { private DBOpenHelper dbOpenHelper; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_add); dbOpenHelper = new DBOpenHelper (AddActivity.this , "db_dict" , null , 1 ); final EditText etWord= findViewById(R.id.add_word); final EditText etInterpret = findViewById(R.id.add_interpret); Button btn_save = findViewById(R.id.btn_save); Button btn_cancel = findViewById(R.id.btn_cancel); btn_save.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { String word = etWord.getText().toString(); String interpret = etInterpret.getText().toString(); if (word.equals("" ) || interpret.equals("" )){ Toast.makeText(AddActivity.this , "填写的单词或解释为空" , Toast.LENGTH_SHORT).show(); }else { insertData(dbOpenHelper.getReadableDatabase(),word,interpret); Toast.makeText(AddActivity.this , "添加生词成功" , Toast.LENGTH_SHORT).show(); } } }); btn_cancel.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { Intent intent = new Intent (AddActivity.this ,MainActivity.class); startActivity(intent); } }); } private void insertData (SQLiteDatabase sqLiteDatabase,String word,String interpret) { ContentValues values = new ContentValues (); values.put("word" ,word); values.put("detail" ,interpret); sqLiteDatabase.insert("tb_dict" ,null ,values); } @Override protected void onDestroy () { super .onDestroy(); if (dbOpenHelper!=null ){ dbOpenHelper.close(); } } }
编写完成后进行测试,如果测试成功,我们可以在模拟器中data/data/<该项目包名>/databases目录下看到名为tb_dict的数据库文件。
Content Provider
用于实现不同应用程序间数据共享,同时可以保证被访问数据的安全性。
Content Provider使用基于数据模型 的简单表格 来提供其中的数据。
URI: 在电脑术语中,统一资源标识符(Uniform Resource Identifier,URI)是一个用于标识某一互联网资源名称的字符串。 该种标识允许用户对任何(包括本地和互联网)的资源通过特定的协议进行交互操作。URI由包括确定语法和相关协议的方案所定义。Web上可用的每种资源 HTML文档、图像、视频片段、程序等由一个通用资源标识符(Uniform Resource Identifier, 简称"URI")进行定位。
URL:Uniform Resource Locator,统一资源定位符。URL是URI的子集。
URL与URI的区别
创建和使用Content Provider的步骤
继承ContentProvider类
声明ContentProvider:在AndroidManifest.xml中使用<provider>
进行声明。
使用ContentProvider:
方法
含义
ContentResolver.insert()
添加
ContentResolver.delete()
删除
ContentResolver.update()
更新
ContentResolver.query()
查询
通过ContentProvider读取所有联系人
编写布局资源文件activity_main.xml实现联系人列表界面布局:
1 2 3 4 5 6 7 8 <RelativeLayout ... > <TextView android:id ="@+id/result" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:textSize ="18sp" android:layout_margin ="15dp" /> </RelativeLayout >
编写MainActivity.java用于实现主逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class MainActivity extends Activity { private String columns = ContactsContract.Contacts.DISPLAY_NAME; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = findViewById(R.id.result); textView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); textView.setText(getQueryData()); } private CharSequence getQueryData () { StringBuilder stringBuilder = new StringBuilder (); ContentResolver resolver = getContentResolver(); Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI, null ,null ,null ,null ); int displayNameIndex = cursor.getColumnIndex(columns); for (cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()){ String displayName = cursor.getString(displayNameIndex); stringBuilder.append(displayName+"\n" ); } cursor.close(); return stringBuilder.toString(); } }
由于需要获取模拟器中联系人信息,因此需要打开读取联系人信息的权限。
在AndroidManifest.xml中设置权限:
1 2 3 4 5 6 7 8 9 <manifest ... > <uses-permission android:name ="android.permission.READ_CONTACTS" /> <application ... android:requestLegacyExternalStorage ="true" > <activity android:name =".MainActivity" > ... </activity > </application > </manifest >
同时我们还需要在模拟器内部授予本案例的应用读取联系人信息的权限!
在进行测试时,模拟器内一般没有默认联系人,因此需要我们添加几个联系人 ,不然测试时就只能看到纯白界面而没有任何内容。
Handler消息处理
Handler消息传递机制
进程: 一个Android应用 就是一个进程 ,每个应用在各自的进程中运行,互不干扰。
线程: 比线程更小的独立运行 单位,一个进程可以包含多个 进程。
Android 中的线程是不安全的,不能用子线程去更改主线程。
规则:子线程不允许操作主线程中的组件。
对规则进行测试
我们可以设计案例测试一下,点击界面“按钮”,使文本内容发生改变:
编写activity_main.xml实现界面布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <RelativeLayout ... android:paddingTop ="16dp" android:paddingEnd ="16dp" android:paddingStart ="16dp" android:paddingBottom ="16dp" > <TextView android:id ="@+id/tv" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:textSize ="26sp" android:textColor ="#0086cc" android:text ="书籍是人类进步的阶梯" /> <Button android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="下一条" android:id ="@+id/button" android:layout_below ="@+id/tv" android:layout_centerHorizontal ="true" android:layout_marginTop ="34dp" /> </RelativeLayout >
编写MainActivity.java用于实现主逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView textView = findViewById(R.id.tv); Button button = findViewById(R.id.button); button.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { Thread thread = new Thread (new Runnable () { @Override public void run () { textView.setText("你今天的努力,是幸运的伏笔;当下的付出,是明日的花开" ); } }); thread.start(); } }); } }
测试: 运行案例,界面成功显示,点击“下一条”,文本内容更换,但程序停止运行并立即闪退 。这也就验证了我们上述规则的正确性。
也正是因为这个原因,因此我们**需要使用Handler(android.os.Handler )**来实现我们的案例。
那么我们应该怎么修改才能达成我们想要的目的呢?
对MainActivity.java进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView textView = findViewById(R.id.tv); Button button = findViewById(R.id.button); final Handler handler = new Handler (){ @Override public void handleMessage (@NonNull Message msg) { super .handleMessage(msg); if (msg.what == 0x123 ){ textView.setText("你今天的努力,是幸运的伏笔;当下的付出,是明日的花开" ); } } }; button.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { Thread thread = new Thread (new Runnable () { @Override public void run () { handler.sendEmptyMessage(0x123 ); } }); thread.start(); } }); } }
总结
Handler 是Android中提供的一个消息处理 的机制。
作用:
利用Handler实现倒计时进度条
在布局资源activity_main.xml中实现进度条原型:
1 2 3 4 5 6 7 8 9 <RelativeLayout ... > <ProgressBar android:id ="@+id/timer" style ="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width ="300dp" android:layout_height ="wrap_content" android:layout_centerInParent ="true" android:max ="60" /> </RelativeLayout >
编写MainActivity.java用于实现倒计时主逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class MainActivity extends AppCompatActivity { private ProgressBar timer; final int TIME = 60 ; private int mProgressStatus = 0 ; final int TIMER_MSG = 0x001 ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); timer = findViewById(R.id.timer); handler.sendEmptyMessage(TIMER_MSG); } Handler handler = new Handler (){ @Override public void handleMessage (@NonNull Message msg) { super .handleMessage(msg); if (TIME-mProgressStatus > 0 ){ mProgressStatus++; timer.setProgress(TIME-mProgressStatus); handler.sendEmptyMessageDelayed(TIMER_MSG,1000 ); }else { Toast.makeText(MainActivity.this , "还想+1s?不存在的!" , Toast.LENGTH_SHORT).show(); } } }; }
Message
在Android中Handler并不是单独工作的,与它一起的还有Looper(一个循环器,主要用于管理MessageQueue)和Message。
MessageQueue:一个消息队列,采用先进先出原则来管理消息,我们可以把它看成一个存储消息的管理器,被封装在Looper当中,所以我们并不会直接去操作它。
Message:线程之间传递的消息,可以携带少量的信息,是Handler接收和处理的对象。
一个线程对应一个Looper对象,一个Looper对象对应一个MessageQueue,一个MessageQueue中可以存放多个Message。
Message可以通过它的属性来携带一些数据。
Message的属性
arg1、arg2:存放整型数据。
obj:存放Object类型的任意对象
replayTo:指定Message发往何处
what:用户自定义的消息代码,采用Int类型表示,我们常用十六进制的整数
Message的创建
创建Message对象我们不用new Message()进行获取。
我们一般使用Message.obtain()
方法或 Handler.obtainMessage()
方法,这两个方法可以避免分配新的对象,减少内存的开销。
实现轮播图片
准备:我们需要准备4张需要轮播的图片,分别命名为image1~image4,然后把它们放置于res/drawable目录下。
编写轮播的补间动画:
slide_in_right:
1 2 3 4 5 6 <set xmlns:android ="http://schemas.android.com/apk/res/android" > <translate android:duration ="500" android:fromXDelta ="100%p" android:toXDelta ="0%p" /> </set >
slide_in_left:
1 2 3 4 5 6 <set xmlns:android ="http://schemas.android.com/apk/res/android" > <translate android:duration ="500" android:fromXDelta ="0%p" android:toXDelta ="-100%p" /> </set >
编写布局资源文件activity_main.xml设置ViewFlipper组件:
1 2 3 4 5 6 7 <RelativeLayout ... > <ViewFlipper android:id ="@+id/viewFlipper" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_marginTop ="20dp" /> </RelativeLayout >
编写MainActivity.java用于实现轮播主逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class MainActivity extends AppCompatActivity { private ViewFlipper flipper; private int [] images = new int []{R.drawable.image1,R.drawable.image2,R.drawable.image3,R.drawable.image4}; private Animation[] animations = new Animation [2 ]; final int FLAG_MSG = 0x001 ; private Message message; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); flipper = findViewById(R.id.viewFlipper); for (int index = 0 ; index < images.length; index++){ ImageView imageView = new ImageView (this ); imageView.setImageResource(images[index]); flipper.addView(imageView); } animations[0 ] = AnimationUtils.loadAnimation(this ,R.anim.slide_in_right); animations[1 ] = AnimationUtils.loadAnimation(this ,R.anim.slide_in_left); flipper.setInAnimation(animations[0 ]); flipper.setOutAnimation(animations[1 ]); message = Message.obtain(); message.what = FLAG_MSG; handler.sendMessage(message); } Handler handler = new Handler (){ @Override public void handleMessage (@NonNull Message msg) { super .handleMessage(msg); if (msg.what == FLAG_MSG){ flipper.showPrevious(); message = handler.obtainMessage(FLAG_MSG); handler.sendMessageDelayed(message,3000 ); } } }; }
注意:补间动画的持续时间与Handler延迟发送信息的时间不可太接近,否则会出现动画异常。
即:android:duration
的属性值与Handler.sendMessageDelayed(Message msg, long delayMillis)
中的delayMillis不可太接近。建议参考本代码中给出的时间。
Looper
一个循环器,主要用于管理MessageQueue,用来不断地从MessageQueue中获取Message返回给Handler。
如果我们在主线程中创建Handler,系统会自动创建Looper对象;如果我们在子线程中创建Handler,需要手动创建Looper对象。
子线程中创建Looper对象的步骤:
初始化Looper对象:prepare()
创建Handler对象:new Handler()
启动Looper:loop()
我们可以如下测试:
Looper测试
编写布局资源文件activity_main.xml添加TextView组件:
1 2 3 4 5 6 <TextView android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="到Logcat面板查看日志" android:textSize ="30sp" android:textStyle ="bold" />
创建LooperThread.java类用于实现在子线程中创建Handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class LooperThread extends Thread { private Handler handler; @Override public void run () { super .run(); handler = new Handler (){ @Override public void handleMessage (@NonNull Message msg) { super .handleMessage(msg); Log.i("Looper" ,String.valueOf(msg.what)); } }; Message message = handler.obtainMessage(); message.what = 0x001 ; handler.sendMessage(message); } }
在MainActivity.java中创建并开启线程:
1 2 3 4 5 6 7 8 9 10 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); LooperThread thread = new LooperThread (); thread.start(); } }
测试:运行案例,程序停止运行并立即闪退 。这时候我们在AS中Run面板中可以看到运行时异常:
1 java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
这个异常告诉我们:无法在尚未调用Looper.prepare()的线程创建处理程序 。这也验证了我们在“子线程中创建Handler,需要手动创建Looper对象 ”的结论。
那么我们应该怎么做呢?
修改MainActivity.java(修改部分已经标出):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class LooperThread extends Thread { private Handler handler; @Override public void run () { super .run(); Looper.prepare(); handler = new Handler (){ @Override public void handleMessage (@NonNull Message msg) { super .handleMessage(msg); Log.i("Looper" ,String.valueOf(msg.what)); } }; Message message = handler.obtainMessage(); message.what = 0x001 ; handler.sendMessage(message); Looper.loop(); } }
然后我们可以前往Logcat面板搜索与Looper有关的日志,我们可以看到如下日志:
1 2020-05-03 21:06:15.438 27574-27609/com.yang.loopertest I/Looper: 1
其中,com.yang.loopertest是我们当前项目的包,1(十进制)就是消息代码。
Service应用
什么是Service
定义:能够在后台 长时间运行,并且没有 用户界面的应用程序组件。
Activity可以比作看皮影戏时看到的画面,但是那些画面怎么动起来的我们并不知道。而在幕布后面,我们知道是由艺人操作让画面动起来的,艺人就可以比作是Service。Activity是可以看到的,而Service是看不到的。
Service的应用:后台下载文件、后台获取GPS定位信息、在后台播放音乐等。
Service的分类
Started Service:调用startService()
方法启动的Service。通过这个方法,运行应用的时候Service并没有启动,而是当应用程序组件(比如:Activity)调用startService()
通知服务启动,这时候Service才处于启动状态。一旦启动将无限期运行下去,除非停止它。
Bound Service:调用bindService()
方法启动的Service。通过这个方法,当应用运行的时候Service与Activity就绑定在一起了,一旦Activity停止,Service也会随着停止。
Service的基本用法
创建和配置Service
在Android Studio中,提供了创建与配置Service的向导 。
右击要创建Service的包 > New > Service ,然后进行选择并创建Service。有两种可以创建的Service,一种是IntentService,另一种是普通的Service。选择IntentService可以创建继承自IntentService的Service。
我们选择普通的Service,然后我们可以看到有两个复选框,一个是Exported,另一个是Enabled,我们可以用鼠标轻触这两个复选框查看它们表示什么含义(一般默认都勾选)。
我们输入创建Service的名字后并完成创建,Android Studio会自动在AndroidManifest.xml中帮我们配置Service。
我们创建名为MyService的Service,并在其中进行代码编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class MyService extends Service { public MyService () { } @Override public IBinder onBind (Intent intent) { throw new UnsupportedOperationException ("Not yet implemented" ); } @Override public void onCreate () { Log.i("Service" ,"Service已创建" ); super .onCreate(); } @Override public int onStartCommand (final Intent intent, int flags, int startId) { Log.i("Service" ,"Service已启动" ); new Thread (new Runnable () { @Override public void run () { int i = 0 ; while (isRunning()){ Log.i("Service" ,String.valueOf(++i)); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); return super .onStartCommand(intent, flags, startId); } @Override public void onDestroy () { Log.i("Service" ,"Service已停止" ); super .onDestroy(); } public boolean isRunning () { ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ArrayList<ActivityManager.RunningServiceInfo> runningService = (ArrayList<ActivityManager.RunningServiceInfo>) activityManager.getRunningServices(10 ); for (int i = 0 ;i<runningService.size();i++){ if (runningService.get(i).service.getClassName(). toString().equals("com.yang.useservice.MyService" )){ return true ; } } return false ; } }
启动和停止Service
启动Service:startService();
停止Service:Service自身调用stopSelf()
或者 其他组件调用 stopService()
编写布局资源文件activity_main.xml,设置控制Service的启动或停止的按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <RelativeLayout ... > <LinearLayout android:orientation ="horizontal" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_centerInParent ="true" > <Button android:id ="@+id/startService" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="启动Service" /> <Button android:id ="@+id/stopService" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_marginStart ="30dp" android:text ="停止Service" /> </LinearLayout > </RelativeLayout >
编写MainActivity.java,实现控制Service启动或关闭的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button start = findViewById(R.id.startService); Button stop = findViewById(R.id.stopService); final Intent intent = new Intent (MainActivity.this ,MyService.class); start.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { startService(intent); } }); stop.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { stopService(intent); } }); } }
测试: 启动程序、打开虚拟机,进入程序主入口界面,点击“启动Service”按钮,我们可以前往Logcat面板搜索与Service有关的日志,我们可以看到如下日志:
1 2 3 4 5 6 7 8 2020-05-03 22:23:03.927 28465-28465/com.yang.useservice I/Service: Service已创建 2020-05-03 22:23:03.927 28465-28465/com.yang.useservice I/Service: Service已启动 2020-05-03 22:23:03.931 28465-28501/com.yang.useservice I/Service: 1 2020-05-03 22:23:05.281 28465-28501/com.yang.useservice I/Service: 2 2020-05-03 22:23:06.284 28465-28501/com.yang.useservice I/Service: 3 2020-05-03 22:23:07.287 28465-28501/com.yang.useservice I/Service: 4 2020-05-03 22:23:08.290 28465-28501/com.yang.useservice I/Service: 5 2020-05-03 22:23:09.292 28465-28501/com.yang.useservice I/Service: 6
并且随着时间每过1秒,末尾的数字会加1,并生成新的日志。
当我们点击“停止Service”按钮,会看到以下日志:
1 2020-05-03 22:23:10.727 28465-28465/com.yang.useservice I/Service: Service已停止
同时末尾的数字不会会加1,更不会生成新的日志。
这也是Service的生命周期 。
Started Service的生命周期
控制背景音乐的播放
准备:我们需要准备两张图片按钮资源,表示音乐播放和音乐暂停,分别命名为music_play和music_stop,将它们放置于res/drawable目录下。然后我们还需要准备一段音频文件,命名为music,在res目录下新建raw目录,将音频文件放置于这个目录下。
编写activity_main.xml布局资源文件:
1 2 3 4 5 6 7 8 9 10 <RelativeLayout ... > <ImageButton android:id ="@+id/btn_play" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_alignParentEnd ="true" android:layout_marginLeft ="20dp" android:layout_marginTop ="20dp" android:src ="@drawable/music_play" /> </RelativeLayout >
创建MusicService.java,实现音乐加载、播放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class MusicService extends Service { static boolean isPlay; MediaPlayer player; public MusicService () { } @Override public IBinder onBind (Intent intent) { throw new UnsupportedOperationException ("Not yet implemented" ); } @Override public void onCreate () { player = MediaPlayer.create(this ,R.raw.music); super .onCreate(); } @Override public int onStartCommand (Intent intent, int flags, int startId) { if (!player.isPlaying()){ player.start(); isPlay = player.isPlaying(); } return super .onStartCommand(intent, flags, startId); } @Override public void onDestroy () { player.stop(); isPlay = player.isPlaying(); player.release(); super .onDestroy(); } }
编写MainActivity.java实现音乐播放、暂停的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = findViewById(R.id.tv); textView.setSelected(true ); final Intent intent = new Intent (MainActivity.this ,MusicService.class); ImageButton btn_play = findViewById(R.id.btn_play); btn_play.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { if (MusicService.isPlay == false ){ startService(intent); ((ImageButton)v).setImageDrawable(getResources(). getDrawable(R.drawable.music_play,null )); }else { stopService(intent); ((ImageButton)v).setImageDrawable(getResources(). getDrawable(R.drawable.music_stop,null )); } } }); } @Override protected void onStart () { startService(new Intent (MainActivity.this ,MusicService.class)); super .onStart(); } }
测试: 经过测试后,我们发现程序存在可以优化的地方。当我们进入应用后,音乐会开始播放,然后我们点击图片按钮,音乐播放暂停,但当我们在此点击图片按钮时,音乐会从头开始播放,这不是我们想要的,一次本案例存在可以进行优化的地方!
Bound Service
在Started Service中启动Service的组件与Service之间没有太大的联系,我们无法进行通信或交换数据;如果Service和启动它的组件需要进行方法调用或交换数据,我们就需要使用Bound Service 。
Bound Service 的生命周期:
实现Bound Service 的基本步骤:
模拟双色球随机选号
由于显示号码的文本框组件是圆形,因此我们需要创建这样的一个资源文件。
在res/drawable目录下创建名为circle_text的资源文件:
1 2 3 4 5 6 7 8 9 10 <shape xmlns:android ="http://schemas.android.com/apk/res/android" android:shape ="oval" android:useLevel ="false" > <solid android:color ="#EA0D0D" /> <stroke android:width ="0.5dip" android:color ="#000000" /> <size android:width ="15dp" android:height ="15dp" /> <corners android:radius ="180dp" /> </shape >
编写activity_main.xml布局资源文件实现主界面显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <RelativeLayout ... > <LinearLayout android:id ="@+id/linearLayout_1" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:orientation ="horizontal" android:layout_centerInParent ="true" > <TextView android:id ="@+id/text_num1" android:layout_width ="30dp" android:layout_height ="30dp" android:textSize ="18sp" android:layout_marginEnd ="10dp" android:gravity ="center" android:background ="@drawable/circle_text" /> <TextView android:id ="@+id/text_num2" android:layout_width ="30dp" android:layout_height ="30dp" android:textSize ="18sp" android:layout_marginEnd ="10dp" android:gravity ="center" android:background ="@drawable/circle_text" /> <TextView android:id ="@+id/text_num3" android:layout_width ="30dp" android:layout_height ="30dp" android:textSize ="18sp" android:layout_marginEnd ="10dp" android:gravity ="center" android:background ="@drawable/circle_text" /> <TextView android:id ="@+id/text_num4" android:layout_width ="30dp" android:layout_height ="30dp" android:layout_marginEnd ="10dp" android:textSize ="18sp" android:gravity ="center" android:background ="@drawable/circle_text" /> <TextView android:id ="@+id/text_num5" android:layout_width ="30dp" android:layout_height ="30dp" android:textSize ="18sp" android:layout_marginEnd ="10dp" android:gravity ="center" android:background ="@drawable/circle_text" /> <TextView android:id ="@+id/text_num6" android:layout_width ="30dp" android:layout_height ="30dp" android:layout_marginEnd ="10dp" android:textSize ="18sp" android:gravity ="center" android:background ="@drawable/circle_text" /> <TextView android:id ="@+id/text_num7" android:layout_width ="30dp" android:layout_height ="30dp" android:textSize ="18sp" android:gravity ="center" android:background ="@drawable/circle_text" android:backgroundTint ="#0788F8" /> </LinearLayout > <Button android:id ="@+id/btn" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="随机选号" android:textSize ="18dp" android:layout_below ="@id/linearLayout_1" android:layout_centerHorizontal ="true" android:layout_marginTop ="15dp" android:background ="#CA0505" /> </RelativeLayout >
创建名为BinderService的Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class BinderService extends Service { public BinderService () { } public class MyBinder extends Binder { public BinderService getService () { return BinderService.this ; } } @Override public IBinder onBind (Intent intent) { return new MyBinder (); } public List getRandomNum () { List resArr = new ArrayList (); int number; String strNumber = "" ; for (int i = 0 ; i < 7 ;i++){ if (i == 6 ){ number = new Random ().nextInt(16 ) +1 ; }else { number = new Random ().nextInt(33 ) +1 ; } if (number < 10 ){ strNumber = "0" +String.valueOf(number); }else { strNumber = String.valueOf(number); } resArr.add(strNumber); } return resArr; } @Override public void onDestroy () { super .onDestroy(); } }
编写MainActivity.java实现调用Service及单击响应逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class MainActivity extends AppCompatActivity { BinderService binderService; int [] tvids = {R.id.text_num1,R.id.text_num2,R.id.text_num3, R.id.text_num4,R.id.text_num5,R.id.text_num6,R.id.text_num7}; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn_random = findViewById(R.id.btn); btn_random.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { List num = binderService.getRandomNum(); for (int i = 0 ; i< num.size();i++){ TextView tv = findViewById(tvids[i]); tv.setText(num.get(i).toString()); } } }); } private ServiceConnection connection = new ServiceConnection () { @Override public void onServiceConnected (ComponentName name, IBinder service) { binderService = ((BinderService.MyBinder)service).getService(); } @Override public void onServiceDisconnected (ComponentName name) { } }; @Override protected void onStart () { super .onStart(); Intent intent = new Intent (MainActivity.this ,BinderService.class); bindService(intent,connection,BIND_AUTO_CREATE); } @Override protected void onStop () { super .onStop(); unbindService(connection); } }
Intent Service 的使用
IntentService是Service的子类。
对于普通的Service ,它不能自动开启线程 ,也不能自动停止服务 ;但是对于IntentService ,它可以自动开启线程 ,也可以自动停止服务 。
我们接下来分别对普通的Service和IntentService进行测试 ~
普通的Service
编写布局资源文件activity_main.xml用于设置启动Service或IntentService的组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <RelativeLayout ... android:gravity ="center" > <Button android:id ="@+id/service" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="启动Service" android:textSize ="18sp" android:textAllCaps ="false" /> <Button android:id ="@+id/intentService" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_toRightOf ="@+id/service" android:layout_marginStart ="20dp" android:text ="启动IntentService" android:textSize ="18sp" android:textAllCaps ="false" /> </RelativeLayout >
创建名为MyService的Service,模拟耗时任务并输出打印日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class MyService extends Service { public MyService () { } @Override public IBinder onBind (Intent intent) { throw new UnsupportedOperationException ("Not yet implemented" ); } @Override public void onCreate () { Log.i("Service" ,"Service已创建" ); super .onCreate(); } @Override public int onStartCommand (Intent intent, int flags, int startId) { Log.i("Service" ,"Service已启动" ); long endTime = System.currentTimeMillis()+10 *1000 ; while (System.currentTimeMillis()<endTime){ synchronized (this ){ try { wait(endTime-System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } return super .onStartCommand(intent, flags, startId); } @Override public void onDestroy () { Log.i("Service" ,"Service已停止" ); super .onDestroy(); } }
编写MainActivity.java用于控制按钮启动Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn_service = findViewById(R.id.service); btn_service.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { startService(new Intent (MainActivity.this ,MyService.class)); } }); } }
测试: 启动程序,打开虚拟机,点击“启动Service”,等待耗时任务完成,我们可以在Logcat面板上看到:
1 2 3 2020-05-04 12:40:44.105 30626-30626/com.yang.intentservice I/Service: Service已创建 2020-05-04 12:40:44.106 30626-30626/com.yang.intentservice I/Service: Service已启动 2020-05-04 12:40:54.108 30626-30626/com.yang.intentservice I/Choreographer: Skipped 600 frames! The application may be doing too much work on its main thread.
Service创建和启动都完成了,但是完成耗时任务后,Logcat面板打印出“The application may be doing too much work on its main thread.”,这句话表示应用在主线程中做了耗时任务,并且在模拟器上可能出现“应用无响应。要将其关闭吗?”的对话框(ANR错误)。这个对话框可以说并不是错误,如果我们点击了“等待”,过一段时间后应用会恢复正常,但这样对用户的体验是不好的,我们应该在设计的时候避免ANR错误的出现。
那么我们应该怎么做才可以呢?我们需要在模拟耗时任务时创建一个新的线程!
对MyService中的onStartCommand()方法进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public int onStartCommand (Intent intent, int flags, int startId) { Log.i("Service" ,"Service已启动" ); new Thread (new Runnable () { @Override public void run () { long endTime = System.currentTimeMillis()+10 *1000 ; while (System.currentTimeMillis()<endTime){ synchronized (this ){ try { wait(endTime-System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } } }).start(); return super .onStartCommand(intent, flags, startId); }
这时,我们在进行测试,我们发现Logcat面板中没有出现“The application may be doing too much work on its main thread.”,模拟器上也没有出现ANR错误的对话框。
1 2 2020-05-04 12:55:12.193 30763-30763/com.yang.intentservice I/Service: Service已创建 2020-05-04 12:55:12.194 30763-30763/com.yang.intentservice I/Service: Service已启动
但我们仔细观察后会发现,耗时任务结束后,Service没有关闭,难道普通的Service不能自动关闭吗?
我们再修改MyService中的onStartCommand()方法进行手动关闭Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int onStartCommand (Intent intent, int flags, int startId) { Log.i("Service" ,"Service已启动" ); new Thread (new Runnable () { @Override public void run () { long endTime = System.currentTimeMillis()+10 *1000 ; while (System.currentTimeMillis()<endTime){ synchronized (this ){ try { wait(endTime-System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } stopSelf(); } }).start(); return super .onStartCommand(intent, flags, startId); }
查看Logcat面板:
1 2 3 2020-05-04 13:00:59.248 30841-30841/com.yang.intentservice I/Service: Service已创建 2020-05-04 13:00:59.259 30841-30841/com.yang.intentservice I/Service: Service已启动 2020-05-04 13:01:09.265 30841-30841/com.yang.intentservice I/Service: Service已停止
这时我们发现打印出了Service已停止的日志,也证实了普通的Service不能自动关闭。
最后我们可以得出结论:普通的Service,它不能自动开启线程,也不能自动停止服务。
IntentService
创建名为MyIntentService的IntentService,模拟耗时任务并输出打印日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class MyIntentService extends IntentService { public MyIntentService (String name) { super (name); } public MyIntentService () { super ("MyIntentService" ); } @Override protected void onHandleIntent (@Nullable Intent intent) { Log.i("IntentService" ,"Service已启动" ); long endTime = System.currentTimeMillis()+10 *1000 ; while (System.currentTimeMillis()<endTime){ synchronized (this ){ try { wait(endTime-System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } } @Override public void onDestroy () { Log.i("IntentService" ,"Service已停止" ); super .onDestroy(); } }
在MainActivity.java中为启动IntentService的按钮编写逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn_intentService = findViewById(R.id.intentService); btn_intentService.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { startService(new Intent (MainActivity.this ,MyIntentService.class)); } }); } }
测试: 启动程序,打开虚拟机,点击“启动IntentService”,等待耗时任务完成,我们可以在Logcat面板上看到:
1 2 2020-05-04 14:48:14.806 31247-31303/com.yang.intentservice I/IntentService: Service已启动 2020-05-04 14:48:24.810 31247-31247/com.yang.intentservice I/IntentService: Service已停止
同时在模拟器上,也不会出现ANR错误。
因此我们不难得出结论:IntentService可以自动开启线程执行耗时任务,耗时任务执行完还可以自动关闭。
Android数据存储、Handler与Service完