Android-Learning

Android Learning

Android Studio项目目录

  • .gradle和.idea:这两个目录下放置的都是Android Studio自动生成的一些代码,我们无需关心

  • app:项目中的代码、资源等内容几乎都是放置在这个目录下的,开发工作已基本都是在这个目录下进行的

    • ​ build:主要包含了一些在编译时自动生成的文件
    • libs:使用到的第三方jar包会被放在该目录下
    • android Test:用来编写Android Test测试用例,对项目进行自动化测试
    • java:放置java代码
    • res:存放在项目中使用到的资源文件(图片、布局、字符串),有多种相同开头的文件夹似乎为了让程序能够更好地兼容各种设备
      • 所有以drawable开头的文件夹都是用存放图片的
      • 所有以mipmap开头的文件夹都是用来放应用图标的
      • 所有以values开头的文件夹都是用来存放字符串、样式、颜色等配置的
      • latout文件夹是用来放布局文件的
    • Android Manifest.xml:整个项目的配置文件,在程序中定义的四大组件都需要在这个文件中注册,还可以在这个文件中给应用程序添加权限声明
    • test:用于编写Unit Test测试用例的,对项目进行自动化测试
    • .gitignore:作用和外层的.gitignore文件类似
    • app.iml:IDEA项目自动创建的文件
    • build.gradle:这是app模块的gradle构建脚本,会指定很多项目构建相关的配置
    • proguard-rules.pro:这个文件用于制定项目代码的混淆规则
  • build:主要包含了一些在编译时自动生成的文件

  • gradle:这个目录下包含了gradle wrapper的配置文件,使用gradle wrapper的方式不需要提前下载好gradle,而是自动联网下载gradle。Android Studio默认没有启用gradle wrapper方式,可以在File->Settings->Build,Execution,Deployment->Gradle进行配置

  • gitignore:用来将指定的目录或文件排除在版本控制之外

  • build.gradle:这是项目全局的gradle构建脚本

  • gradle.properties:这个文件是全局的gradle配置文件,在这里配置的属性将会影响到项目中所有的gradle编译脚本

  • gradlew和gradlew.bat:这两个文件是用来在命令行界面中执行gradle命令的,分别对应Linux、Mac系统和Windows系统

  • Myapplication.iml:iml是所有IntelliJ IDEA项目都会自动创建的一个文件,用于表示这是一个IDEA项目

  • local.properties:指定本机中Android SDK路径

  • settings.gradle:用于指定项目中所有引入的模块

APK的运行

首先先在AndroidManifest.xml文件找到这段代码

android:name=".ClassName"对该类名的函数进行注册,没有在AndroidManifest.xml里注册的活动是不能使用的,具体来说,android:name 属性的作用就是设置一个类,当app运行前创建实例,并可以将类中的数据在运行期间给所有 Activity 来访问。其中intent-filter里的两行代码表示Mainactivity是这个项目的主活动,在手机上点集应用图标,首先启动这个活动

活动是应用程序的门面,凡是在应用中能看到的东西都是放在活动中的

可以看到MainActivity是继承自AppCompatActivity的,Activity是Android系统提供的一个活动基类,项目中所有的活动都必须继承它或者它的子类才能拥有活动的特性(AppCompatActivity是Activity的子类),而Oncreate()是MainActivity的方法,这个方法是一个活动被创建时必定要执行的方法

Android程序的设计逻辑讲究逻辑和视图分离,因此不建议在活动中直接编写界面,而应该在布局文件中编写界面,在活动中引入进来。Oncreate()方法第二行调用了setContentview方法,为当前的活动引入了一个activity_main布局布局文件都是定义在res/layout目录下的

在res/layout/activity_main.xml下可以看到

TestView是Android系统提供的一个控件,用于在布局中显示文字,而显示出来的"Hello World!"就是通过android:text="Hello World!"定义的

使用资源文件

打开res/layout/strings.xml文件可以看到

对于这个定义应用程序名的字符串,我们有两种方式来引用它

  • 在代码中通过R.string.activity_main可以获得该字符串的引用
  • 在XML中通过@string/activity_main可以获得该字符串的引用

基本的语法就是上面这两种,对于其他资源文件,只需要替换string部分即可,使用例子如下

详解build.gradle文件

Android Stuido采用Gradle来构建项目。Gradle基于Groovy的领域特定语言(DSL)来声明项目设置

项目外层的bulid.gradle文件

dependencies闭包中使用classpath声明了一个Gradle插件。由于Gradle还可以用于构建Java、C++等项目,所以需要声明插件

app目录下的build.gradle文件

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
plugins {
id 'com.android.application'
}

android {
compileSdk 31

defaultConfig {
applicationId "com.example.myapplication"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {

implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

第一行应用了一个插件,一般有两种值可选:com.android.application和com.android.library,分别表示这是一个应用程序模块和一个库模块

接下来是一个大的android闭包,主要用于配置项目构建的各种属性,compileSdkVersion用于指定项目构建版本的版本。android闭包中的defaultConfig闭包对项目中的更多细节进行配置,applicationId用于指定项目的包名,我们可以在这里修改包名minSdkversion指定项目最低兼容的Android系统版本targetSdkVersion指定的值表示你已经在该目标版做过充分的测试versionCode和versionName分别指定项目的版本号和版本名

buildTypes闭包用于指定生成安装文件的相关配置,通常只会有debug和release闭包

release闭包中的代码混淆

dependencies闭包

Android中的日志工具Log

Log中有五种方法

在代码中加入Log.d(“MainActivity”,“Oncreate execute”);,然后重新运行就可以在logcat窗口中看见打印的信息

由于每次传入参数Tag,我们可以在Oncreate方法外输入logt+Tab,此时就会以当前的类名作为值自动生成一个TAG常量

同时在logcat中可以添加过滤器来过滤我们不需要的信息

我们可以通过Edit Filter Configuration创建根据Tag进行过滤的过滤器

探究活动

手动创建活动

首先先手动创建一个空的项目,然后手动添加Activity,注意此时不要将该活动注册,也不要设为当前项目的主活动

项目中的任何活动都应该重写Activity中的Oncreate()方法,因为父类中也有该方法

1
2
3
4
5
6
7
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

这里的onCreate()方法就是调用了父类的onCreate()方法

创建和加载布局

Android程序设计讲究逻辑和视图分离,最后每一个活动都能对于一个布局,布局就是用来显示界面内容的

app/src/main/res->New->Directory/Android resource Directiory创建一个layout目录,然后再右键创建Layout resource file

创建完成之后AS为我们提供了可视化布局编辑器,下图红色框中的三个按钮可以在文本模式和可视化布局下切换

接下来我们添加一个Button元素

如果我们需要在XML中引用一个id,就是用@id/id_name这种语法,如果是定义一个id,就使用@+id/id_name这种语法,下面三个属性分别是宽高和元素中显示的文字内容,创建之后我们就可以通过R.id.id_name来直接加载资源文件

此时切换回布局界面就可以看见我们创建的Button了

接下来我们在活动中加载这个布局

在setContentView()方法中,我们一般会传入一个布局文件的id。项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们创建的first_layout.xml布局的id现在应该是已经添加到R文件中了,接着只需要调用R.layout.first_layout就可以得到first_layout.xml布局的id,然后将这个值传入setContentView()方法即可

在AndroidManifest文件中注册

接下来我们要将活动进行注册,而AS已经自动帮我们注册了,但是此时还未指定程序的主活动。

配置主活动

只需要在标签内部加入标签,并且在这个标签里添加这两句声明即可

注意如果添加标签需要加上android:exported=“true”

然后运行即可,可以看到BUTTON1按钮已经创建好了

下面这个是标签栏

如果应用程序中没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是无法在启动器中看到或打开这个程序。这种程序一般都是作为第三方服务提供其他应用在内部进行调用的,比如支付宝的快捷支付服务

在活动中使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失

先在我们创建的按钮中添加一个弹出Toast的触发点,在Oncreate函数中添加代码

在活动中可以通过findViewById()方法获取到在布局文件中定义的元素,这里我们传入R.id.button_1来获得按钮的实例(new出来的对象),这个值就是我们在first_layout.xml中通过android:id属性指定的。findViewById()方法返回一个View对象,我们需要向下转型为Button对象,得到按钮的实例之后,我们通过setOnclickListener()方法为按钮注册一个监听器,点击按钮就会执行监听器中的onClick()方法。因此弹出Toast的功能需要在onClick()方法中编写

Toast的用法比较简单,通过makeText()创建出一个Toast对象,然后调用show()将Toast显示出来即可,makeText()方法需要三个参数,第一个参数是Context,也就是Toast要求的上下文,而活动本身就是一个Context对象,所以直接传入MainActivity.this即可,第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,可以选择Toast.LENGTH_SHORT或Toast.LENGTH_LONG

同时我们可以修改Toast的颜色、背景和字体大小,然后再调用show方法

自定义Toast

在活动中使用Menu

首先在res目录下新建一个menu文件夹,接着在这个文件夹下新建一个叫main的菜单文件

接下来再main.xml中添加如下代码

这里我们创建了两个菜单项,其中标签用来创建一个具体的菜单项,通过android:id为这个菜单指定一个唯一的标识符,通过android:title为这个菜单指定名称

接着重新回到MainAcivity中重写onCreateOptionsMenu()方法

可以通过Ctrl+O快速重写方法

getMenuInflater()方法能够得到MenuInflater对象,再调用它的inflate方法就可以给当前活动创建菜单了。第一个参数用于指定使用哪一个资源文件来创建菜单,第二个参数用于指定我们的菜单项将添加到哪一个Menu对象中去,这里直接使用onCreateOptionsMenu方法中传入的menu参数,然后返回true表示创建成功。

我们还需要定义菜单响应事件,在MainActivity中重写onOptionsMenuSelected方法

在onOptionsItemSelected方法中,通过调用item.getItemid()来判断我们点击的菜单项,然后为菜单项加入逻辑

销毁一个活动

Activity类中提供了一个finish()方法来让我们结束当前的活动

我们在MainActivity中修改一下代码,此时点击Button1活动就会结束了

使用Intent在活动之间穿梭

接下来我们会学习如何从主活动跳转到其他活动

使用显式Intent

接下来我们再创建一个活动,并且定义一个按钮,此时AS已经自动帮我们在AndroidManifest.xml文件中注册了

LinearLayout表示线性布局,在Android中有六种布局

Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据,Intent一般可被用于启动活动,启动服务以及发送广播等场景

Intent大致可以分为显式和隐式

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext,Class<?>cls)。这个构造函数接受两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数Class则是指定想要启动的目标活动接下来我们将Intent对象传入Activity类中的startActivity方法启动目标活动

我们将当前活动作为上下文,出啊如SecondActivity.class作为目标互动,此时点击Button1就会通过startActivity来执行这个Intent

如果想销毁当前的互动,只需要点击Back键即可

这种Intent的"意图"非常明显的,我们称之为显式Intent

使用隐式Intent

隐式Intent并不在明确指出我们想要启动的活动,而是指定了一系列action和category等信息,然后交由系统去分析Intent,并帮我们找出合适的活动去启动

我们可以通过在标签下配置的内容(intent过滤器),进而指定当前活动能够响应的action和category

标签中我们指明了当前活动可以响应com.example.myapplication.ACTION_START这个action,而标签则包含了一些附加信息,更精确地指定当前活动能够响应的Intent,只有Intent过滤器中的全部标签都满足时,这个活动才能响应该Intent

修改MainActivity中按钮的点击事件

这里我们传入不同的参数来使用不同的构造函数,这里我们直接将action的字符串传入,表明我们想要启动能够响应com.example.myapplication.ACTION_START这个action的活动。没有指定category是因为android.intent.category.DEFAULT是一种默认的category,在调用starActivity会自动将这个category添加到Intent中

每个Intent中只能指定一个action,但却能指定多个category,接下来我们在Intent中增加一个category,并且在活动的中添加该categoty,表示可以响应这个Intent中的category内容

此时我们可以通过点击Button1进入SecondActivity活动

这个就是隐式Intent

更多隐式Intent的用法

使用隐式Intent不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能,我们可以调用系统的浏览器来打开网页

我们创建一个隐式Intent

首先制定了Intent的action是Intent.ACTION_VIEW,这是一个Android系统的内置动作,其常量值为android.intent.VIEW,然后通过Uri.prase()方法将一个网站字符串解析为Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去,setdata()方法接受一个Uri对象,用于指定当前Intent正在操作的数据,而这些数据通常都是 以字符串的形式传入Uri.parse()方法中解析产生的

此时我们点击Button1就会启动浏览器并且打开百度

**我们还可以在标签中再配置一个标签,用于更精确地指定当前活动能够响应什么类型的数据 **

标签主要可以配置以下内容

只有标签指定的内容和Intent中携带的Data完全一致时,当前活动才能响应该Intent,不过一般在标签中不会指定过多的内容。当我们指定android:scheme为http就可以响应所有http协议的Intent了

下面是例子

创建一个ThirdActivity,然后修改layout布局文件,最后在AndroidManifest.xml中修改ThirdActivity的注册信息

此时再点击Button1就会有两个活动来响应Intent,会跳出列表(目前能够响应这个Intent的所有程序)让我们来选择

除此之外我们还可以指定其他协议

向下一个活动传递数据

Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,启动另一个活动之后,只需要把这些数据再从Intent中取出即可

比如说MainActivity中有一个字符串,现在想把这个字符串传递到SecondActivity中

MainActivity

SecondActivity

效果如下,可以在logcat中看到传入到SecondActivity中的data被打印

在MainACtivity中使用显式Intent启动SecondActivity,并且通过putExtra()方法传递了一个字符串,其中第一个参数是键,用于后面从Intent中取值,第二个参数才是真正要传递的数据

在SecondActivity中使用getIntent()方法获取到用于启动SecondActivity的Intent,然后调用getStringExtra()方法传入相应的键值来获取传递的数据。不同类型的数据接收的方法也不同,整型则使用getIntExtra()方法

返回数据给上一个活动

在Activity中还有一个startActivityForResult()方法,该方法也是用于启动活动的,并且能够在活动销毁的时候返回一个结果给上一个活动

startActivityForResult()方法接收两个参数,第一个参数是Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源(请求码只要是一个唯一值即可)

在MainActivity中以startActivityForResult方法来启动SecondActivity,请求码只要是一个唯一值就可以了,这里传入了1

其中startActivityForResult方法被划横线是因为不推荐使用

MainActivity

在SecondActivity给button2注册点击事件,并在点击事件中添加返回数据的逻辑。我们构建了一个Intent,紧接着把要传递的数据存放在Intent中,然后调用了SetResult()方法,这个方法专门用于向上一个活动返回数据的,第一个参数用于向上一个活动返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这两个值,第二个参数则是把带有数据的Intent传递回去,然后finish()销毁当前活动

SecondActivity

由于我们使用startActivityForResult方法来启动SecondActivity的,SecondActivity被销毁之后会回调上一个活动的onActivityResult()方法,所以需要在MainActivity中重写这个方法来获取返回的数据第一个参数requestCode是我们在启动活动时传入的请求码,第二个参数resultCode是我门在返回数据时的处理结果,第三个参数data即为携带数据的Intent。在活动结束后先通过检查requestCode的值来判断数据来源,然后通过resultCode来判断处理结果是否成功。最后从data中根据键取值并打印

运行结果,logcat中打印了SecondActivity返回的数据

活动的生命周期

了解活动的生命周期可以让我们写出更加连贯流畅的程序并学会如何合理管理应用资源

返回栈

Android的任务是可以层叠的,新启动的活动会覆盖在原活动之上,点击Back键会销毁最上面的活动,下一个活动就会重新显示出来

Android使用Task来管理任务,一个任务就是一组存放在栈里的活动的集合,这个栈就是返回栈。当我们调用finish()方法或者点击Back键时,栈顶的活动就会出栈,此时前一个入栈的活动即为栈顶元素,系统总是会显示栈顶的活动给用户

活动状态

每个活动在其生命周期最多可能会有4种状态

  1. 运行状态:此时活动处于返回栈栈顶
  2. 暂停状态:当一个活动不再处于栈顶位置但仍然可见时,此时活动进入暂停状态
  3. 停止状态:当一个活动不再处于栈顶位置,并且完全不可见的时候,就进去了停止状态。虽然此时系统仍会为这种活动保存相应的状态和成员变量,但是当其他地方需要内存时,处于停止状态的活动那个有可能会被系统回收
  4. 销毁活动:当一个活动从返回栈种移除后就变成了销毁状态,系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足

活动的生存期

Activity类中定义了七个回调函数,覆盖了活动生命周期的每一个环节

  • oncreate():他会在活动第一次被创建的时候调用,我们应该在这个方法中完成活动的初始化操作,比如加载布局、绑定事件等
  • onStart():这个方法在活动由不可见变为可见的时候调用
  • onResume():这个方法准备好和用户进行交互的时候调用,此时的活动一定位于返回栈的栈顶并且处于运行状态
  • onPause():这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据
  • onStop():这个方法在活动完全不可见的时候调用,它和onPause()方法的主要去别的在于:如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法不会执行
  • onDestroy():将活动的状态变为销毁状态
  • onRestart():重新启动活动

通过以上七种方法我们可以划分出活动的生命周期

活动的展示图

举个例子

我们先创建两个活动的并且创建对应的布局,在MainActivity设置两个Button分别来启动不同的活动,其中NormalActivity是正常活动,显示了一个TextView,而DialogActivity是对话框主题(只需要在AndroidManifest.xml选择活动的主题即可)

代码如下

activity_main

AndroidManifest

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.example.android_text3;

import android.content.Intent;
import android.os.Bundle;

import com.google.android.material.snackbar.Snackbar;

import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
import android.view.View;

import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.example.android_text3.databinding.ActivityMainBinding;

import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private AppBarConfiguration appBarConfiguration;
private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);//加载main的布局
Button startNormalActivity=(Button) findViewById(R.id.start_noamal_activity);
//创建按钮并且绑定
Button startDialogActivity=(Button)findViewById(R.id.start_dialog_activity);
startNormalActivity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent =new Intent(MainActivity.this,NormalLayoutActivity.class);
startActivity(intent);
}
});
startDialogActivity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this,DialogLayoutActivity.class);
startActivity(intent);
}
});

}

@Override
protected void onStart() {
super.onStart();
Log.d(TAG,"onStart");
}

@Override
protected void onPause() {
super.onPause();
Log.d(TAG,"onPause");
}

@Override
protected void onStop() {
super.onStop();
Log.d(TAG,"onStop");
}

@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
}

@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG,"onRestart");
}

@Override
protected void onResume() {
super.onResume();
Log.d(TAG,"onResume");
}
}

创建之后是这个样子的

此时logcat显示,可以看到在MainActivity第一次被创建时会依次执行onCreate、onStart、onResume方法

当我们点击NormalActivity,此时NoralActivity已经完全把MainActivity遮挡住了,onPause、onStop方法被执行

接下来按下Back键,由于MainActivity已经创建过了,所以不会执行onCreate方法

然后点击第二个按钮启动DialogActivity

可以看到这里只执行了onPause方法,这是因为DialogActivity并没有完全遮挡住MainActivity,MainActivity只进入暂停状态,此时按下Back只有onResume方法被执行

最后在MainActivity中按下Back

这三个方法依次执行,最终销毁MainActivity

活动被回收了怎么处理

当一个活动进入停止状态时,其可能被系统回收。虽然可以执行onCreate方法重新创建活动,但是原本活动的临时数据就会被回收了。而Activity中的onSaveInstanceState()回调方法可以保证在活动被回收之前一定会被调用,因此可以通过该方法来解决活动那个被回收时临时数据得不到保存的问题

onSaveInstanceState会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如putString()方法保存字符串,putInt()方法保存整型数据。每个保存方法都需要两个参数,第一个参数是键,用于后面从Bundle取值,第二个参数是真正的数据

取出数据的时候只需要使用onSaveInstanceState中对应的get方法取出即可

onCreate方法中有一个Bundle参数,当活动被系统回收之前有使用onSaveInstanceState方法保存数据的话,这个参数就会带有之前保存的所有数据

取出

Intent和Bundle的结合

1662688520457

活动的启动模式

在实际项目中我们应该根据特定的需要为每个互动指定恰当的启动模式,启动模式有standard、singleTop、singleTask和singleInstance,我们可以在AndroidManifest.xml中通过个指定android:launchMode属性来选择启动模式

standard

活动默认的启动方式

对于standard模式的的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新实例

多个相同活动的返回栈

此时需要连续点击三次Back才能退出程序,每点击一次按钮就会创建一个新的实例

singleTop

当指定启动模式为singleTop时,当我们进入按钮时并不会再次重新创建实例,而是从栈顶取出相同的活动

singleTask

使用singleTop模式可以很好地解决重复创建栈顶活动的问题,但是如果该活动没有处于栈顶位置,还是有可能会创建多个活动实例

而singleTask模式可以在启动活动时首先在栈中检查是否有该活动地实例,如果发现存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有则创建一个新的活动实例

singleInstance

指定为singleInstance模式地活动会启用一个新的返回栈来管理这个活动当我们想在本程序中和其他程序中共享一个活动的实例,但是每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈入栈时必然是创建了新的实例。而在singleInstance模式下,会有一个单独的返回栈来管理这个活动,这就实现了共享活动实例

我们指定SencondActivity为singleInstance模式,MainActivity和ThirdActivity则是默认的启动模式,在每个活动中我们都让其打印当前任务的id(同一个任务使用同一个返回栈),并且重写OnDestroy方法打印当前活动名称

运行之后发现SencondActivity和MainActivity、ThirdActivity的任务id并不相同

并且在ThirdActivity中点击Back键会直接返回到MainActivity中,再点击Back就返回到SecondActivity,这是因为MainActivity和ThirdActivity处于同一返回栈,只有当前返回栈为空,才会显示另一个返回栈的栈顶活动

原理示意图

随时随地退出程序

当我们创建多个活动而想要直接退出程序时,我们可以用一个专门的集合类来对所有的活动进行管理

只需要创建一个类,每当有一个活动被创建就加入到活动管理器中,销毁时移除,要是想在任何地方结束所有活动,直接调用ActivityCollector中的finishAll()方法即可

启动活动的最佳语法

前面介绍过两个活动之间使用Intent实现活动间数据的传递,其实更好的做法是创建一个方法来构建Intent对象,这样传入活动的参数就很明显了

在SecondActivity中写一个创建实例并传入数据的方法,最后启动活动,而MainActivity中直接调用SecondActivity活动中的这个方法即可,这样我们就能清楚看到SecondActivity活动所需要传入的参数

UI开发的点点滴滴

在每个活动的布局中

常用控件的使用方法

TextView

主要用于在界面上显示一段文本信息

创建一个TextView控件

控件的属性详解:android:id给当前控件定义了一个唯一标识符,android:layout_width和android:layout_height分别指定了控件的宽度的高度,所有的控件都有这两种属性,主要有match_parent、fill_parent、wrap_content三种可选值,match_parent和fill_parent一样,表示让当前控件的大小和父布局的一样,也就是由父布局来决定当前控件的大小,wrap_content则表示当前控件的大小刚好能包含里面的内容,也就是孔家内容决定当前控件的大小

居左上角对齐

TextView的文字默认是居左上角对齐的,想要修改对齐方式只需要使用android:gravity,可选值有top、bottom、left、right、center,可以使用|来同时指定多个值

居中对齐

此外我们还可以通过android:textSize和android:textColor属性来指定文字的大小和颜色

Button

Button的两种监听方式,可以通过android:textAllcaps来关闭和开启Button按钮的大小写

EditText

通过android:hint在未输入前提示,android:maxlines来指定最大行数,当输入的内容超过两行时,文本会自动向上滚

我们也能在程序中获取输入然后进行处理

ImageView

ProgressBar

ProgressDialog

四种布局方式

线性布局LinearLayout

分为竖直方向和水平方向的线性排列

控件在布局中的对齐方式

控件的权重占比

如上设置EditText占了屏幕的四分之三

相对布局Relativelayout

关于父布局的相对布局

关于控件的相对布局

帧布局FrameLayout

百分比布局

首先先在app/build.gradle导入

百分比布局的使用

ListView

当我们需要展示大量数据给用户的时候就需要使用这个控件,比如查看QQ聊天记录、翻阅消息等,都需要让屏幕外的数据替代屏幕内的数据

首先我们需要先把准备展示的数据提供好,数组中的数据无法直接传递给ListView,可以使用ArrayAdapter,可以通过泛型来指定要适配的数据类型,然后再Oncreate函数中将数据传入,传入适配数据之后,调用setAdapter犯法将构建好的适配器对象传递进去接口完成ListView与数据之间的关联

Android NDK开发

首先先在AS中安装CMake和NDK

JNI和NDK

JNI和NDK

JNI(Java Native Interface的缩写),译为Java本地接口,是Java与C/C沟通的一门技术,JNI的目的就是在java中调用C、C写的本地方法,android下使用JNI需要的.so文件,是通过ndk-build生成的

三者的关系

Java之所以需要调用C、C++写的本地方法是因为

JNI开发

首先先创建好项目

  1. 编写Java类,声明native方法(声明之后使用ALT+ENTER快速创建)
  2. 编写native代码
  3. 将native代码编译成so文件
  4. 在java类中引入so库,调用native方法

native命名方法

1
2
3
4
5
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_print(JNIEnv *env, jobject thiz) {
// TODO: implement print()

}

函数命名规则:Java_类路径_方法名,JNIEnv时定义任何native方法的第一个参数,表示指向JNII环境的指针,可以通过他来访问JNI提供的接口方法,jobject表示Java对象中的this,如果是静态方法则表示jclass,JNIEXPORT和JNICALL时JNI定义的宏,可以在jni.h中找到

JNI数据类型和Java数据类型的关系

Native的基本数据类型其实就是将C/C++中的基本类型用typedef重新定义了一个新的名字,在JNI中可以直接访问

1
2
3
4
5
6
7
8
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */

引用数据类型,JNI使用C语言时,所有引用类型都是以jobject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

JNI的字符串处理

native操作JVM

JNI会把Java中所有对象当作一个C指针传递到本地方法中,这个指针指向JVM的内部数据结构,而内部的数据的结构在内存中的存储方式是不可见的。只能从JNIEnv指针指向的函数表选择合适的JNI函数来操作JVM中的数据结构

比如说native访问java.lang.String时,不能像访问基本数据类型那样使用,因为它是一个Java的引用类型,所以在本地方法中只能通过类似GetStringUTFChars这样的JNI函数来访问字符串的内容

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
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.nfc.Tag;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.myapplication.databinding.ActivityMainBinding;

import org.w3c.dom.Text;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";

// Used to load the 'myapplication' library on application startup.
static {
System.loadLibrary("myapplication");//在java中引入so库
}


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv=(TextView)findViewById(R.id.sample_text);
tv.setText(stringFromJNI());//调用so方法
String result=print("helloworld");
Log.d(TAG,result);
}

/**
* A native method that is implemented by the 'myapplication' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();//声明native方法
public native String print(String str);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java_com_example_myapplication_MainActivity_print(JNIEnv *env, jobject thiz, jstring str) {
// TODO: implement print()
//从Java的内存中把字符串拷贝出来,在native中使用
const char*strFromJava=(char*)env->GetStringUTFChars(str,NULL);
if(strFromJava==NULL){//如果从内存中拷贝的字符串为空,直接返回
return NULL;
}
//字符串拷贝
char buff[128]={0};
strcpy(buff,strFromJava);
strcat(buff," My Friend!!");

//释放资源
env->ReleaseStringUTFChars(str,strFromJava);

//返回数据,并且自动转为Unicode
return env->NewStringUTF(buff);
}

在上面的代码中,print()函数接收一个jstring,而jstring是指向JVM内部的一个字符串,不能直接使用。首先需要将jstring转为C语言的字符串类型char*后才能使用,这里必须使用合适的JNI函数来访问JVM内部的字符串数据类型

java中可以定义某个函数为native类型,对于native函数只需要声明即可,因为该函数的实现是native层的,即由相应的C去实现,java编译器遇到native函数时不会关心该函数的具体实现,因此编译上不会出现任何错误

Java中默认使用Unicode编码,C/C++默认使用UTF编码,所以在native层和java层进行字符串交流的时候需要进行编码转换,GetStringUTFChars可以把jstring指针的Unicode字符串转为UTF-8格式的C字符串

ReleaseStringUTFChars方法释放内存空间,native需要我们手动释放申请的内存空间,GetStringUTFChars调用时会新申请一块空间用来装拷贝出来的字符串这个字符串用来方便native代码访问和修改

最后NewStringUTF传入char*类型的字符串,构造出一个java.lang.String的字符串对象,并转为Unicode编码

数组操作

基本类型数组

基本类型数组就是JNI中的基本数据类型组成的数组,可以直接访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_myapplication_MainActivity_SumArray(JNIEnv *env, jobject thiz, jintArray arr) {
// TODO: implement SumArray()
int result=0;
//获取数组长度
jint len=env->GetArrayLength(arr);
//动态申请数组
jint*c_array=(jint*)malloc(len*sizeof(jint));
//初始化数据为0
memset(c_array,0,len);
//将java中的数据赋值给C数组
env->GetIntArrayRegion(arr,0,len,c_array);
for(int i=0;i<len;++i){
result+=c_array[i];
}
//释放掉申请的内存
free(c_array);
return result;
}
1
2
3
4
5
int arr[]={1,2,3,4,5,6};
int final1=SumArray(arr);
Log.d(TAG,result);
Log.d(TAG,"sum is "+final1);
public native int SumArray(int arr[]);

得到结果

C层能达到jint arr之后首先需要获取它的长度,然后动态申请一个数组(因为java层传递过来的数组长度是不定的,所以这里需要动态申请C层数组)。数组是jint型的,最后调用GetIntArrayRegion进行拷贝

对象数组

所谓对象数组就是存储了多个对象的数组,对象数组中的元素是一个类的实例或其他数组的引用,不能直接访问Java传递给JNI层的数组

Native调用Java方法

Native调用Java静态方法

  1. 首先调用FindClass函数传入Class描述符(Java类的全类名,在AS中输入MyJniclass会提示自动补全),找到该类并得到jclass类型
  2. 然后通过GetStaticMethodID找到该方法的id,传入方法签名,得到jmethodID类型的引用。
  3. 构建入参,然后调用CallStaticObjectMethod去调用Java类里面的静态方法,然后传入参数,返回的直接就是Java层返回的数据。其实,这里的CallStaticObjectMethod是调用的引用类型的静态方法,与之相似的还有:CallStaticVoidMethod(无返参),CallStaticIntMethod(返参是Int),CallStaticFloatMethod等。
  4. 移除局部引用

我们自己编写一个类

1
2
3
4
5
public static class MyJniclass{
public static void Print(){
Log.d(TAG,"Native Call Method From Java");
}
}

接着我们编写native函数来调用这个类中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
extern "C"
JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_CallMethodFromJava(JNIEnv *env, jobject thiz) {
// TODO: implement CallMethodFromJava()
//从class路径下搜索整个类,并返回整个类的class对象,com.example.myapplication.MainActivity活动中的MyJniclass类
jclass clazz=env->FindClass("com/example/myapplication/MainActivity$MyJniclass");
//找到clazz类中要调用的方法,第三个参数是方法的参数和返回类型
jmethodID get=env->GetStaticMethodID(clazz,"Print","()V");//最后一个方法签名需要指定正确的参数和返回值

//如果是含参数和返回值的方法还需要传入参数和返回值
env->CallStaticVoidMethod(clazz,get);
env->DeleteLocalRef(clazz);
}

这里要注意GetStaticMethodID的第三个参数,他既包含了参数类型也包含了返回值,这里的()V表示无参数且返回值为Void

对于参数为String,返回值为String的方法是这样的

参数为String和int,返回值为Void

GetStaticMethodID第三个参数

Native调用Java实例方法

JNI动态注册

和上面的差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyJNIClass {

public int age = 30;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public static String getDes(String text) {
if (text == null) {
text = "";
}
return "传入的字符串长度是 :" + text.length() + " 内容是 : " + text;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extern "C"
JNIEXPORT void JNICALL
Java_com_xzh_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) {
//从jvm中加载类
jclass clazz = env->FindClass("com/xzh/allinone/jni/MyJNIClass");
//获取构造方法的方法id
jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");
//通过实例获取getAge方法的方法id
jmethodID mid_get_age = env->GetMethodID(clazz, "getAge", "()I");
jmethodID mid_set_age = env->GetMethodID(clazz, "setAge", "(I)V");
jobject jobj = env->NewObject(clazz, mid_construct);//调用构造方法创建一个对象--实例,因为在java中,调用方法是通过p.Method()来进行的,所以需要先实例化对象

//调用方法setAge
env->CallVoidMethod(jobj, mid_set_age, 20);
//再调用方法getAge 获取返回值 打印输出
jint age = env->CallIntMethod(jobj, mid_get_age);
LOGI("获取到 age = %d", age);

//凡是使用是jobject的子类,都需要移除引用
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(jobj);
}

静态注册和动态注册

JNI函数注册

静态注册

首先在代码块中添加LoadLibrary函数加载动态链接库,这样后面的代码调用才能找到对应的原生函数(C++函数)而静态代码块的执行时机早于构造函数、Oncreate方法,其在类加载的时候就被调用

静态注册原理是根据函数名将Java代码中的native方法与so中的JNI方法一一对应,当java层调用so层的函数时,如果发现其上有JNIEXPORT和JNICALL两个宏定义时,就会将so层函数(cpp中的函数)链接到对应的native方法中。下面是JNI函数名命名规则

例如check函数需要命名为

静态注册的缺点

  1. 必须遵循某些规则
  2. JNI方法名过长
  3. 运行时需要根据函数名查找对应的JNI函数,程序运行效率不高(因为当so层存在多个JNI函数时,则需要一一比较)

动态注册

**动态注册的原理是在调用System.LoadLibrary()时会在so层调用一个名为JNI_OnLoad()的函数,我们可以提供一个函数映射表,然后在JNI_OnLoad()函数中通过JNI中提供的 RegisterNatives()方法来注册函数, 这样Java就可以通过函数映射表来调用函数,而不必通过函数名(静态注册中复杂的函数名)来查找对应函数 **

动态注册的实现步骤

  1. 首先利用结构体JNINativeMethod数组记录native方法与JNI方法的对应关系,即函数映射表
  2. 实现JNI_OnLoad方法,在加载动态库之后,执行动态注册
  3. 调用FindClass方法,获取java对象
  4. 调用RegisterNatives方法,传入java对象,JNINativeMethod数组以及注册数目完成注册

其中JNINativeMethod结构体如下所示:

1
2
3
4
5
typedef struct {
const char* name; // native方法名
const char* signature; // 方法签名,例如()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;
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
#include <jni.h>
#include <string>

jstring stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
//函数映射表
JNINativeMethod methods[]{
//直接为结构体赋值
{"stringFromJNI","()Ljava/lang/String;",(void*) stringFromJNI}
};

jint JNI_OnLoad(JavaVM*vm,void*reserverd){
JNIEnv*env=NULL;
//首先获取jvm的环境,因为加载的类都在里面了,接下来获取类需要用到
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK){
return JNI_ERR;//告知版本
}
//获取java类,方便待会获取类中的成员方法
jclass clazz=env->FindClass("com/flag/regisiternative/MainActivity");
if(clazz==NULL){
return JNI_ERR;
}
//将java中的函数和so层的函数进行注册,进而直接调用native方法即可调用JNI函数
jint result=env->RegisterNatives(clazz,methods,sizeof(methods)/sizeof(methods[0]));
if(result<0){
//注册失败返回负值
return JNI_ERR;
}
return JNI_VERSION_1_6;
}

动态注册优点

  • 通过函数映射表来查找对应的JNI方法,运行效率高
  • 不需要遵循命名规则,灵活性更好

动态注册缺点

  • 实现起来相对复杂
  • 容易搞错方法签名导致注册失败

APK发布Release

创建一个key

选择打包版本