【Android第一行代码学习笔记】Android架构_四大组件_权限_持久化_通知_异步_服务

【Android第一行代码学习笔记】Android架构_四大组件_权限_持久化_通知_异步_服务

刚开始学开发的时候,不懂写博客记录笔记的好处。现在深知记录的重要性。

  1. 方便复习
  2. 当时学会了,过段时间就又忘了
  3. 自己写的,自己容易理解,要用的时候,看自己以前学过的总比看网上的拿起来更快一些
    ...
    虽然自己转了后端,但阴差阳错,这家公司没Android开发,又让我写Android。。。。。。

所以,记录一下笔记吧!

Android系统架构

Android各个版本

https://developer.android.google.cn/about/versions

Android 3.0系统,这个系统版本是专门为平板计算机(简称"平板"​)设计的

Android 4.0系统,这个版本不再对手机和平板进行差异化区分,既可以应用在手机上,也可以应用在平板上。

Android 5.0系统

  • 使用ART运行环境替代了Dalvik虚拟机
  • 提出了Material Design的概念来优化应用的界面设计
  • 推出了Android Wear、Android Auto、Android TV系统,从而进军可穿戴设备、汽车、电视等全新领域

Android 6.0系统中加入了运行时权限功能。

Android 8.0系统引入了通知渠道

Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行

Android应用开发特色

四大组件

Activity、Service、BroadcastReceiver和ContentProvider

Activity是所有Android应用程序的门面,凡是在应用中你看得到的东西,都是放在Activity中的。

而Service就比较低调了,你无法看到它,但它会在后台默默地运行,即使用户退出了应用,Service仍然是可以继续运行的。

允许你的应用接收来自各处的广播消息,比如电话、短信等,当然,你的应用也可以向外发出广播消息。

ContentProvider则为应用程序之间共享数据提供了可能,比如你想要读取系统通讯录中的联系人,就需要通过ContentProvider来实现。

SQLite数据库

Android系统还自带了这种轻量级、运算速度极快的嵌入式关系型数据库。

它不仅支持标准的SQL语法,还可以通过Android封装好的API进行操作,让存储和读取数据变得非常方便

强大的多媒体

如音乐、视频、录音、拍照、定位等

minSdk 和 targetSdk

minSdkVersion用于指定项目最低兼容的Android系统版本,这里指定成21表示最低兼容到Android 5.0系统。

targetSdkVersion指定的值表示你在该目标版本上已经做过了充分的测试,系统将会为你的应用程序启用一些最新的功能和特性。

比如Android 6.0系统中引入了运行时权限这个功能,如果你将targetSdkVersion指定成23或者更高,那么系统就会为你的程序启用运行时权限功能,而如果你将targetSdkVersion指定成22,那么就说明你的程序最高只在Android 5.1系统上做过充分的测试,Android 6.0系统中引入的新功能自然就不会启用了

viewBinding

build.gradle:

bash 复制代码
android {
    //xxx
    defaultConfig {
        applicationId "wb.lp.myapplication"
        minSdk 23
        targetSdk 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildFeatures {
        viewBinding = true
    }
    //xxx
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <TextView
        android:id="@+id/mtext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
kotlin 复制代码
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import wb.lp.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.mtext.text = "xxx"
    }

}

Activity的生命周期

Activity的4种状态

Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)​。

每个Activity在其生命周期中最多可能会有4种状态。

  • 运行状态:当一个Activity位于返回栈的栈顶时,Activity就处于运行状态。系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。
  • 暂停状态:当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种Activity。
  • 停止状态:当一个Activity不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
  • 销毁状态:一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的Activity,以保证手机的内存充足。

7个回调方法

Activity类中定义了7个回调方法,覆盖了Activity生命周期的每一个环节

  • onCreate()。这个方法你已经看到过很多次了,我们在每个Activity中都重写了这个方法,它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局、绑定事件等。

  • onStart()。这个方法在Activity由不可见变为可见的时候调用。

  • onResume()。这个方法在Activity准备好和用户进行交互的时候调用。此时的Activity一定位于返回栈的栈顶,并且处于运行状态。

  • onPause()。这个方法在系统准备去启动或者恢复另一个Activity的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。

  • onStop()。这个方法在Activity完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行。

  • onDestroy()。这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态。

  • onRestart()。这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。

以上7个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将Activity分为以下3种生存期。

  • 完整生存期。Activity在onCreate()方法和onDestroy()方法之间所经历的就是完整生存期。一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的操作。
  • 可见生存期。Activity在onStart()方法和onStop()方法之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。
  • 前台生存期。Activity在onResume()方法和onPause()方法之间所经历的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的,我们平时看到和接触最多的就是这个状态下的Activity。

图示:

代码:

kotlin 复制代码
package wb.lp.myapplication.lifecycle

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import wb.lp.myapplication.databinding.ActivityLifecycleBinding

class LifeCycleActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLifecycleBinding
    private val tag = "LifeCycleActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLifecycleBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.startNormalActivity.setOnClickListener {
            val intent = Intent(this, NormalActivity::class.java)
            startActivity(intent)
        }
        binding.startDialogActivity.setOnClickListener {
            val intent = Intent(this, DialogActivity::class.java)
            startActivity(intent)
        }
        Log.d(tag, "onCreate")
    }

    override fun onStart() {
        super.onStart()
        Log.d(tag, "onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.d(tag, "onResume")
    }

    override fun onPause() {
        super.onPause()
        Log.d(tag, "onPause")
    }

    override fun onStop() {
        super.onStop()
        Log.d(tag, "onStop")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(tag, "onDestroy")
    }

    override fun onRestart() {
        super.onRestart()
        Log.d(tag, "onRestart")
    }

}
kotlin 复制代码
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import wb.lp.myapplication.R


/*
        <activity
            android:name=".lifecycle.DialogActivity"
            android:theme="@style/Theme.AppCompat.Dialog"
            android:exported="false" />
 */

class DialogActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_dialog)
    }
}

Activity被回收了怎么办

当一个Activity进入了停止状态,是有可能被系统回收的。

那么想象以下场景:应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次。

这样看上去好像一切正常,可是别忽略了一个重要问题:Activity A中是可能存在临时数据和状态的。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。

Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。

kotlin 复制代码
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    val tempData = "Something you just typed"
    outState.putString("data_key", tempData)
}
kotlin 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d(tag, "onCreate")
    setContentView(R.layout.activity_main)
    if (savedInstanceState != null) {
        val tempData = savedInstanceState.getString("data_key")
        Log.d(tag, "tempData is $tempData")
    }
    ...
}

当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程,因而在这种情况下,Activity中的数据也会丢失。虽然这个问题同样可以通过onSaveInstanceState()方法来解决,但是一般不太建议这么做,因为对于横竖屏旋转的情况,现在有更加优雅的解决方案

广播

Android中的广播主要可以分为两种类型:标准广播和有序广播。

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

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

注册广播

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

注册BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册。动态注册的BroadcastReceiver可以自由地控制注册与注销,在灵活性方面有很大的优势。但是它存在着一个缺点,即必须在程序启动之后才能接收广播,因为注册的逻辑是写在onCreate()方法中的。让程序在未启动的情况下也能接收广播?这就需要使用静态注册的方式了

动态注册:

kotlin 复制代码
package wb.lp.myapplication.broadcastreceiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import wb.lp.myapplication.R

class BroadcastReceiverActivity : AppCompatActivity() {

    lateinit var timeChangeReceiver: TimeChangeReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val intentFilter = IntentFilter()
        // 1分钟提示一次
        intentFilter.addAction("android.intent.action.TIME_TICK")
        timeChangeReceiver = TimeChangeReceiver()
        registerReceiver(timeChangeReceiver, intentFilter)
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(timeChangeReceiver)
    }

    class TimeChangeReceiver : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent) {
            // 1分钟提示一次
            Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
        }

    }

}

静态注册:

【静态注册的BroadcastReceiver,是没有办法在onReceive()方法里弹出对话框这样的UI控件的】

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication">


        <receiver
            android:name=".broadcastreceiver.BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

        <activity
            android:name=".broadcastreceiver.BroadcastReceiverActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>
kotlin 复制代码
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class BootCompleteReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show()
    }

}

Android系统还会在亮屏熄屏、电量变化、网络变化等场景下发出广播。如果你想查看完整的系统广播列表,可以到如下的路径中去查看:

xml 复制代码
<Android SDK>/platforms/<任意android api版本>/data/broadcast_actions.txt

能够静态注册的广播:https://developer.android.google.cn/develop/background-work/background-tasks/broadcasts/broadcast-exceptions

发送广播

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
            intent.setPackage(packageName)
            sendBroadcast(intent)
        }
        ...
    }
    ...
}
kotlin 复制代码
class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
            intent.setPackage(packageName)
            sendOrderedBroadcast(intent, null)
        }
        ...
    }
    ...
}

持久化

Android系统中主要提供了3种方式用于简单地实现数据持久化功能:文件存储、SharedPreferences存储以及数据库存储

文件存储

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。

这个方法接收两个参数:

  • 第一个参数是文件名,在文件创建的时候使用,注意这里指定的文件名不可以包含路径,因为所有的文件都默认存储到/data/data/<package name>/files/目录下;
  • 第二个参数是文件的操作模式,主要有MODE_PRIVATE和MODE_APPEND两种模式可选,默认是MODE_PRIVATE,表示当指定相同文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。

写入:

kotlin 复制代码
//use函数,这是Kotlin提供的一个内置扩展函数。它会保证在Lambda表达式中的代码全部执行完之后自动将外层的流关闭


// 这样就不需要我们再编写一个finally语句,手动去关闭流了

fun save(inputText: String) {
    try {
        val output = openFileOutput("data", Context.MODE_PRIVATE)
        val writer = BufferedWriter(OutputStreamWriter(output))
        writer.use {
            it.write(inputText)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

读取:

kotlin 复制代码
fun load(): String {
    val content = StringBuilder()
    try {
        val input = openFileInput("data")
        val reader = BufferedReader(InputStreamReader(input))
        reader.use {
            reader.forEachLine {
                content.append(it)
            }
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
    return content.toString()
}

SharedPreferences存储

SharedPreferences是使用键值对的方式来存储数据的

Android中主要提供了以下两种方法用于得到SharedPreferences对象:存放在/data/data/<package name>/shared_prefs/目录下

  • Context类中的getSharedPreferences()方法:第一个参数用于指定SharedPreferences文件的名称,第二个参数目前只有默认的MODE_PRIVATE这一种模式可选,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
  • Activity类中的getPreferences()方法:只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。

主要可以分为3步实现。

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

写入:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        saveButton.setOnClickListener {
        		 // 生成data.xml文件
            val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
            editor.putString("name", "Tom")
            editor.putInt("age", 28)
            editor.putBoolean("married", false)
            editor.apply()
        }
    }

}

读取:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        restoreButton.setOnClickListener {
            val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
            val name = prefs.getString("name", "")
            val age = prefs.getInt("age", 0)
            val married = prefs.getBoolean("married", false)
            Log.d("MainActivity", "name is $name")
            Log.d("MainActivity", "age is $age")
            Log.d("MainActivity", "married is $married")
        }
    }

}

注意:

将密码以明文的形式存储在SharedPreferences文件中是非常不安全的,很容易被别人盗取,因此在正式的项目里必须结合一定的加密算法对密码进行保护才行。

SQLite数据库存储

Android系统是内置数据库的

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务

kotlin 复制代码
package wb.lp.myapplication.storage

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.widget.Toast

class MyDatabaseHelper(val context: Context, name: String, version: Int) :
    SQLiteOpenHelper(context, name, null, version) {

    private val createBook =
        "create table Book (" + " id integer primary key autoincrement," + "author text," + "price real," + "pages integer," + "name text)"

    private val createCategory =
        "create table Category (" + "id integer primary key autoincrement," + "category_name text," + "category_code integer)"

    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(createBook)
        db.execSQL(createCategory)
        Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
    }

    /*
     升级表

//        val dbHelper = MyDatabaseHelper(this, "BookStore.db", 1)
        val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
     */
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.execSQL("drop table if exists Book")
        db.execSQL("drop table if exists Category")
        onCreate(db)
    }

}
kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
//        val dbHelper = MyDatabaseHelper(this, "BookStore.db", 1)
        val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
        dbHelper.writableDatabase
    }

}

Android中事务的标准用法,首先调用SQLiteDatabase的beginTransaction()方法开启一个事务,然后在一个异常捕获的代码块中执行具体的数据库操作,当所有的操作都完成之后,调用setTransactionSuccessful()表示事务已经执行成功了,最后在finally代码块中调用endTransaction()结束事务。

运行时权限

Android 6.0系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。

Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限。

  • 普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作
  • 危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能。

到Android 10系统为止所有的危险权限:


表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名。原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权。但是请谨记,不要基于此规则来实现任何功能逻辑,因为Android系统随时有可能调整权限的分组。

内容提供器

持久化技术所保存的数据只能在当前应用程序中访问

实现跨程序数据共享的功能,推荐使用更加安全可靠的ContentProvider技术

ContentProvider的用法一般有两种:

  • 一种是使用现有的ContentProvider读取和操作相应程序中的数据
  • 另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。

对于每一个应用程序来说,如果想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。

内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。

  • authority是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。比如某个应用的包名是com.example.app,那么该应用对应的authority就可以命名为com.example.app.provider。

  • path则是用于对同一应用程序中不同的表做区分的,通常会添加到authority的后面。比如某个应用的数据库里存在两张表table1和table2,这时就可以将path分别命名为/table1和/table2

  • 然后把authority和path进行组合,内容URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。

  • 不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式如下:content://com.example.app.provider/table1

  • 在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入:val uri = Uri.parse("content://com.example.app.provider/table1")

kotlin 复制代码
val cursor = contentResolver.query(
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder)

例子:

kotlin 复制代码
package wb.lp.myapplication.storage

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.ContactsContract
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

/*
    <uses-permission android:name="android.permission.READ_CONTACTS" />
 */

class ContentProviderActivity : AppCompatActivity() {

    private val contactsList = ArrayList<String>()
    private lateinit var adapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.READ_CONTACTS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this, arrayOf(Manifest.permission.READ_CONTACTS), 1
            )
        } else {
            readContacts()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    readContacts()
                } else {
                    Toast.makeText(
                        this, "You denied the permission", Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
    }

    private fun readContacts() {
        // 查询联系人数据
        contentResolver.query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null
        )?.apply {
            while (moveToNext()) {
                // 获取联系人姓名
                val displayName = getString(
                    getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
                    )
                )
                // 获取联系人手机号
                val number = getString(
                    getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.NUMBER
                    )
                )
                contactsList.add("$displayName\n$number")
            }
            close()
        }
    }
}

通知

当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。

Android系统允许我们将某个应用程序的通知完全屏蔽,以防止它一直给我们发送垃圾信息,但是在这些信息中,也可能会有我们所关心的内容。比如说我希望收到某个我所关注的人的微博更新通知,但是却不想让微博一天到晚给我推送一些明星的花边新闻。在过去,用户是没有办法对这些信息做区分的,要么同意接受所有信息,要么屏蔽所有信息,这也是Android通知功能的痛点。于是,Android 8.0系统引入了通知渠道这个概念。

通知渠道就是每条通知都要属于一个对应的渠道。每个应用程序都可以自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知。

我们的应用程序如果想要发出通知,也必须创建自己的通知渠道才行

NotificationChannel类和createNotificationChannel()方法都是Android 8.0系统中新增的API,因此我们在使用的时候还需要进行版本判断

渠道ID可以随便定义,只要保证全局唯一性就可以。渠道名称是给用户看的,需要可以清楚地表达这个渠道的用途。通知的重要等级主要有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MIN这几种,对应的重要程度依次从高到低。不同的重要等级会决定通知的不同行为

kotlin 复制代码
 private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 创建视频邀请通知渠道
            val videoInviteChannel = NotificationChannel(
                "video_invite_channel", "视频邀请通知", NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "接收视频会议邀请通知"
                setShowBadge(true)
                enableLights(true)
                lightColor = android.graphics.Color.RED
                enableVibration(true)
                vibrationPattern = longArrayOf(0, 100, 200, 100)
            }

            val notificationManager = getSystemService(
                Context.NOTIFICATION_SERVICE
            ) as NotificationManager

            notificationManager.createNotificationChannel(videoInviteChannel)
        }
    }

通知的用法还是比较灵活的,既可以在Activity里创建,也可以在BroadcastReceiver里创建,当然还可以在后面我们即将学习的Service里创建。相比于BroadcastReceiver和Service,在Activity里创建通知的场景还是比较少的,因为一般只有当程序进入后台的时候才需要使用通知。

AndroidX库中提供了一个NotificationCompat类,使用这个类的构造器创建Notification对象,就可以保证我们的程序在所有Android系统版本上都能正常工作

kotlin 复制代码
private fun sendNewInviteNotification(invites: List<VideoInvite>) {
        if (invites.isEmpty()) return
        val notificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val latestInvite = invites.first()
        val intent = Intent(this, InviteListActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK
            putExtra("from_notification", true)
        }
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val notification = NotificationCompat.Builder(this, "video_invite_channel")
            .setSmallIcon(R.drawable.ic_video_call).setContentTitle("您有视频通话未处理,请点击查看")
            .setContentText("事故 ${latestInvite.registNo},客户 ${latestInvite.customPhone}")
            .setContentIntent(pendingIntent).setAutoCancel(true)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setCategory(NotificationCompat.CATEGORY_MESSAGE).setGroup("video_invite_group")
            .setStyle(
                NotificationCompat.BigTextStyle()
                    .bigText("事故号: ${latestInvite.registNo}\n客户: ${latestInvite.customPhone}\n时间: ${latestInvite.inviteTime}\n${if (invites.size > 1) "还有 ${invites.size - 1} 条未读邀请" else ""}")
            ).build()

//notify()方法接收两个参数:第一个参数是id,要保证为每个通知指定的id都是不同的
        notificationManager.notify(NOTIFICATION_ID + 1, notification)
        Log.d(TAG, "已发送新邀请通知: ${latestInvite.registNo}")
    }

例子:【创建通知渠道的代码只在第一次执行的时候才会创建,当下次再执行创建代码时,系统会检测到该通知渠道已经存在了,因此不会重复创建,也并不会影响运行效率。】

kotlin 复制代码
		val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
                NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT
            )
            manager.createNotificationChannel(channel)
        }
        sendNotice.setOnClickListener {
            val notification = NotificationCompat.Builder(this, "normal")
                .setContentTitle("This is content title")
                .setContentText("This is content text")
                .setSmallIcon(R.drawable.small_icon)
                .setLargeIcon(
                    BitmapFactory.decodeResource(resources,
                    R.drawable.large_icon))
                .build()
            manager.notify(1, notification)
        }

开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更,因为通知渠道一旦创建就不能再通过代码修改了。

异步

和许多其他的GUI库一样,Android的UI也是线程不安全的。也就是说,如果想要更新应用程序里的UI元素,必须在主线程中进行,否则就会出现异常。

Handler

kotlin 复制代码
package wb.lp.myapplication.handler

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import androidx.appcompat.app.AppCompatActivity
import wb.lp.myapplication.R
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity() {

    val updateText = 1

    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // 在这里可以进行UI操作
            when (msg.what) {
//                updateText -> textView.text = "Nice to meet you"
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
//        changeTextBtn.setOnClickListener {
            thread {
                val msg = Message()
                msg.what = updateText
                handler.sendMessage(msg) // 将Message对象发送出去
            }
//        }
    }

}

Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。

  • Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。除what之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。
  • HandlerHandler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法、post()方法等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。
  • MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
  • Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

首先在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage()方法中。由于Handler的构造函数中我们传入了Looper.getMainLooper(),所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。

AsyncTask

AsyncTask是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它。在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下。

  • Params。在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。

  • Progress。在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

  • Result。当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

  • 例子:把AsyncTask的第一个泛型参数指定为Unit,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Int,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。

    kotlin 复制代码
    class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
        ...
    }

重写AsyncTask中的几个方法才能完成对任务的定制。经常需要重写的方法有以下4个。

  • onPreExecute()这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
  • doInBackground(Params...)这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress (Progress...)方法来完成。
  • onProgressUpdate(Progress...)当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate (Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
  • onPostExecute(Result)当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。

一个比较完整的自定义AsyncTask就可以写成如下形式:【启动这个任务:DownloadTask().execute()】

kotlin 复制代码
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {

    override fun onPreExecute() {
        progressDialog.show() // 显示进度对话框
    }

    override fun doInBackground(vararg params: Unit?) = try {
        while (true) {
            val downloadPercent = doDownload() // 这是一个虚构的方法
            publishProgress(downloadPercent)
            if (downloadPercent >= 100) {
                break
            }
        }
        true
    } catch (e: Exception) {
        false
    }

    override fun onProgressUpdate(vararg values: Int?) {
        // 在这里更新下载进度
        progressDialog.setMessage("Downloaded ${values[0]}%")
    }

    override fun onPostExecute(result: Boolean) {
        progressDialog.dismiss()// 关闭进度对话框
        // 在这里提示下载结果
        if (result) {
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show()
        }
    }

}

服务

概念

Service是Android中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。

Service的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service仍然能够保持正常运行。

不过需要注意的是,Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行。

另外,也不要被Service的后台概念所迷惑,实际上Service并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在Service的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。

从Android 8.0系统开始,应用的后台功能被大幅削减。现在只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。之所以做这样的改动,是为了防止许多恶意的应用程序长期在后台占用手机资源,从而导致手机变得越来越卡。当然,如果你真的非常需要长期在后台执行一些任务,可以使用前台Service或者WorkManager

使用

新建服务:【onCreate()方法会在Service创建的时候调用,onStartCommand()方法会在每次Service启动的时候调用,onDestroy()方法会在Service销毁的时候调用。】

kotlin 复制代码
class MyService : Service() {
    ...
    override fun onCreate() {
        super.onCreate()
        Log.d("MyService", "onCreate executed")
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        Log.d("MyService", "onStartCommand executed")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("MyService", "onDestroy executed")
    }
}

启动和停止Service:【Service也可以自我停止运行,只需要在Service内部调用stopSelf()方法即可。】

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startServiceBtn.setOnClickListener {
            val intent = Intent(this, MyService::class.java)
            startService(intent) // 启动Service
        }
        stopServiceBtn.setOnClickListener {
            val intent = Intent(this, MyService::class.java)
            stopService(intent) // 停止Service
        }
    }

}

Activity和Service进行通信

在Activity中指挥Service去干什么,Service就去干什么。需要借助onBind()方法。

kotlin 复制代码
class MyService : Service() {

    private val mBinder = DownloadBinder()

    class DownloadBinder : Binder() {

        fun startDownload() {
            Log.d("MyService", "startDownload executed")
        }

        fun getProgress(): Int {
            Log.d("MyService", "getProgress executed")
            return 0
        }

    }

    override fun onBind(intent: Intent): IBinder {
        return mBinder
    }
    ...
}
kotlin 复制代码
class MainActivity : AppCompatActivity() {

    lateinit var downloadBinder: MyService.DownloadBinder

    private val connection = object : ServiceConnection {

        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            downloadBinder = service as MyService.DownloadBinder
            downloadBinder.startDownload()
            downloadBinder.getProgress()
        }

        override fun onServiceDisconnected(name: ComponentName) {
        }

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        bindServiceBtn.setOnClickListener {
            val intent = Intent(this, MyService::class.java)
            //第三个参数则是一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service进行绑定后自动创建Service。
            //这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行
            bindService(intent, connection, Context.BIND_AUTO_CREATE) // 绑定Service
        }
        unbindServiceBtn.setOnClickListener {
            unbindService(connection) // 解绑Service
        }
    }

}

前台Service

从Android 8.0系统开始,只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。而如果你希望Service能够一直保持运行状态,就可以考虑使用前台Service。

前台Service和普通Service最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果

由于状态栏中一直有一个正在运行的图标,相当于我们的应用以另外一种形式保持在前台可见状态,所以系统不会倾向于回收前台Service。另外,用户也可以通过下拉状态栏清楚地知道当前什么应用正在运行,因此也不存在某些恶意应用长期在后台偷偷占用手机资源的情况。

调用startForeground()方法后就会让MyService变成一个前台Service,并在系统状态栏显示出来。

Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.servicetest">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    ...
</manifest>

例1:

kotlin 复制代码
//前台服务
class MyService : Service() {
    override fun onBind(intent: Intent?): IBinder? = null
    override fun onCreate() {
        super.onCreate()
        Log.d("MyService", "onCreate executed")
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "my_service", "前台Service通知", NotificationManager.IMPORTANCE_DEFAULT
            )
            manager.createNotificationChannel(channel)
        }
        val intent = Intent(this, MainActivity::class.java)
        val pi = PendingIntent.getActivity(this, 0, intent, 0)
        val notification =
            NotificationCompat.Builder(this, "my_service").setContentTitle("This is content title")
                .setContentText("This is content text").setSmallIcon(R.drawable.small_icon)
                .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
                .setContentIntent(pi).build()
        startForeground(1, notification)
    }
}

例2:

kotlin 复制代码
startForeground(NOTIFICATION_ID, createNotification())



val channel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID, "视频邀请监听服务", NotificationManager.IMPORTANCE_LOW
            ).apply {
                description = "后台监听视频邀请消息"
                setShowBadge(false)
                lockscreenVisibility = Notification.VISIBILITY_PRIVATE
            }
 notificationManager.createNotificationChannel(channel)




  private fun createNotification(): Notification {
        return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_video_call)
            .setContentTitle(baseContext.getString(R.string.app_name) + "运行中...")/*.setContentText("后台服务运行中...")*/
            .setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).setShowWhen(false)
            .setCategory(NotificationCompat.CATEGORY_SERVICE)
            .setVisibility(NotificationCompat.VISIBILITY_PRIVATE).build()
    }
相关推荐
alexhilton8 小时前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
冬奇Lab11 小时前
InputManagerService:输入事件分发与ANR机制
android·源码阅读
张小潇14 小时前
AOSP15 Input专题InputManager源码分析
android·操作系统
RdoZam17 小时前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
奥陌陌1 天前
android 打印函数调用堆栈
android
用户985120035831 天前
Compose Navigation 3 深度解析(二):基础用法
android·android jetpack
恋猫de小郭1 天前
Android 官方正式官宣 AI 支持 AppFunctions ,Android 官方 MCP 和系统级 OpenClaw 雏形
android·前端·flutter
黄林晴1 天前
Android 17 Beta 2,隐私这把锁又拧紧了
android
Kapaseker1 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
bqliang1 天前
Compose 媒体查询 (Media Query API) 🖱️👇🕹️
android·android jetpack