Frida脚本概念

  Frida脚本就是利用Firda动态插桩框架,使用Frida导出的API和方法对内存空间里的对象方法进行监视、修改或者替换的一段代码。Frida的API是用JavaScript实现的,所以可以充分利用JavaScript的匿名函数优势以及大量的Hook(钩子函数)和回调函数的API。

阅读全文 »

活动(Activity)基本用法

上一节中的MainActivity是Android Studio自己创建的,现在我们手动创建。
首先需要关闭当前项目,点击导航栏File->Close Project。然后再新建一个Android项目,我们选No Activity,项目名字可以叫做ActivityTest,点击Finish,等待Gradle构建完成,项目就创建成功了。

手动创建活动

将项目结构手动改为Project模式

这里面app/src/main/java/com.example.activitytest目录是空的。
现在右击com.example.activitytest包->New->Activity->Empty Activity,会弹出一个创建活动的对话框,我们将活动命名为FirstActivity,不要勾选Generate Layout File和Launch Activity这两个选项,点击Finish完成创建。
创建完成后我发现这里生成的是一个Kotlin文件,而我此时还对Kotlin语言比较陌生。而且上一节里的MainActivity文件是Class文件,此时我想到两种方式,一个是接着用HelloWorld项目继续往下走,另一个是创建一个Class文件,把代码搬过来。

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

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

onCreate方法就是调用了父类的onCreate方法。

创建和加载布局

Android程序的设计讲究逻辑和视图分离,最好每一个活动都能对应一个布局,布局是用来显示界面内容的,我们来手动创建一个布局文件。
右击app/src/main/res目录->New->Directory,会弹出一个新建目录的窗口,这里创建一个名为layout的目录。然后对layout目录右键->new->Layout resource file,会弹出一个新建布局资源文件的窗口,将这个布局文件命名为first_layout,根元素默认即可点击ok完成布局创建。
然后就会出现布局编辑器,将其切换为Text,会有如下代码

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

</androidx.constraintlayout.widget.ConstraintLayout>

补充:根元素
在 Android Studio 中,XML 布局文件的根元素通常是一个视图容器或布局容器。常见的根元素之一是 ,它们是 Android 中常用的布局容器之一。这些容器允许你组织和排列屏幕上的视图。例如, 可以让你在水平或垂直方向上排列其内部的子视图,而 则允许你使用相对位置来布置子视图。
androidx.constraintlayout.widget.ConstraintLayout 是 Android 开发中的一个布局容器,用于在屏幕上放置和控制视图之间的位置关系。它是 Android Jetpack 中的一部分,被设计用于简化复杂布局的创建,并且在性能上表现良好。
使用 ConstraintLayout,你可以通过设置视图之间的水平和垂直约束来定义它们的位置。这些约束可以设置视图的边距、对齐方式、宽度和高度等。ConstraintLayout 还提供了可视化编辑器,使得在 Android Studio 中进行布局设计更加方便。

现在我们对布局稍作编辑,添加一个按钮,将Text视图切换为Design视图。
鼠标点击Button拖进显示框中。

再切换为Text视图。
增加了如下的代码。

1
2
3
4
5
6
7
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:layout_editor_absoluteX="150dp"
tools:layout_editor_absoluteY="79dp" />

这里添加了一个Button元素,并在Button元素内部增加了几个属性。
android:id是给当前的元素定义一个唯一标识符,之后可以在代码中对这个元素进行操作。
对于@+id/button,如果把+去掉,就变成了@id/button,就是再XML里引用资源的方法。
如果要在XML里引用一个id,就是用@id/id_name这种语法,而如果需要在XML中定义一个id则要使用@+id/id_name这种语法。
后面的android:layout_width指定了当前元素的宽度,这里使用wrap_content表示当前元素的高度只要能刚好包含里面的内容就行。
android:layout_height指定了当前元素的高度。
android:text指定了元素中显示的文字内容。
一个简单的布局我们编写完成,接下来在活动中加载这个布局。
回到FirstActivity,在onCreat()方法中加入代码:

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

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout); //加载布局
}
}

调用setContentView()方法来给当前活动加载一个布局,在方法中我们一般会传入布局文件的id。
项目中添加的任何资源都会在R文件中生成一个相应的资源ID,因此刚刚创建的first_layout.xml布局的id现在应该是已经添加到R文件中了。
在代码中引用布局文件,只需调用R.layout.first_layout就可以得到first_layout.xml布局的id,然后传入setContentView()方法即可。

在AndroidManifest文件中注册

所有的活动都要在AndroidManifest.xml中进行注册才能生效,而实际上FirstActivity已经在AndroidManifest.xml中注册过了,打开app/src/main/AndroidManifest.xml文件,有如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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.ActivityTest"
tools:targetApi="31">
<activity
android:name=".FirstActivity"
android:exported="false"
android:label="@string/title_activity_first"
android:theme="@style/Theme.ActivityTest" />
</application>

活动的声明放在标签里,这里是通过标签来对活动进行注册的。而现在Android Studio自动帮我们完成了对FirstActivity的注册。
标签中我们使用了android:name来指定具体注册的活动,这里的.FirstActivity就是com.example.activitytest.FirstActivity的缩写。
但是只注册了活动,程序还是不能运行的,因为还没有为程序配置主活动,也就是,程序运行起来的时候,不知道首先要启动那个活动。
配置主活动,就是在标签内部加入标签,并在这个标签里添加这两句声明即可。
还可以使用android:label指定活动中标题栏的内容,标题栏是显示在活动最顶部的,运行时可以看到。
给主活动指定的label不仅会成为标题栏中的内容,还会成为启动器(Launcher)中应用程序显示的名称。
修改后的AndroidManifest.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
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<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.ActivityTest"
tools:targetApi="31">
<activity android:name=".FirstActivity"
android:exported="true"
android:label="@string/title_activity_first"
android:theme="@style/Theme.ActivityTest">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

</manifest>

这里面有一行代码android:exported="true"原本是false,修改为true才能正常执行
android:exported 是用来指定组件是否可以被其他应用程序或组件访问的属性。
在 Android 中,应用程序由多个组件组成,如活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)和内容提供器(Content Provider)。这些组件可以是私有的(不导出)或公开的(导出)。

运行程序即可看到效果。

在活动中使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失并且不会占用任何屏幕空间。
首先定义一个弹出Toast的触发点,正好界面上有一个按钮,我们让点击这个按钮的时候弹出一个Toast。在onCreat()方法中添加代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this,"You click Button",Toast.LENGTH_SHORT).show();
}
});
}

在活动中通过findViewById()方法获取到在布局文件中定义的元素,得到实例。
findViewById()方法返回的是一个View对象,我们需要向下转型将它转成Button对象。
得到实例后通过调用setOnClickListener()方法为按钮注册一个监听器,点击按钮就会执行监听器中的onClick()方法。弹出Toast的方法就在onClick()方法中编写了。
Toast的用法很简单,通过静态方法makeText()创建出一个Toast对象,然后用show()将Toast显示出来就可以了。makeText方法需要传入3个参数。第一个参数是Context,也就是Toast要求的上下文,由于活动本身就是一个Context对象,这里直接传入FirstActivity.this即可。第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,有两个内置常量Toast.LENGTH_SHORTToast.LENGTH_LONG

在活动中使用Menu

在res目录下新建一个menu文件夹,右击res目录->New->Directory,输入文件夹名字menu,点击OK。接着在文件夹下再新建一个名叫main的菜单文件,右击menu文件夹->New->Menu resource file,文件名输入main,点击OK完成创建。创建完成后打开main.xml文件。

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

</menu>

在里面添加代码

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add"/>
<item
android:id="@+id/remove_item"
android:title="Remove"/>
</menu>

  这里我们创建了两个菜单项,其中标签是用来创建按具体的某一个菜单项,然后通过android:id给这个菜单指定一个唯一的标识符,通过android:title给这个菜单项指定一个名称。
  回到FirstActivity中来重写onCreatOptionMenu()方法,重写方法可以使用Ctrl+O快捷键。

1
2
3
4
5
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main,menu); //这一行是我们编写的
return super.onCreateOptionsMenu(menu);
}

  通过getMenuInflater()方法能够得到MenuInflater对象,再调用它的indalte()方法就可以给当前活动创建菜单了。infalte()方法接收两个参数,第一个参数用于指定我们通过哪一个资源文件,这里传入R.menu.main。第二个参数用于指定我们的菜单项将添加到哪一个Menu对象中,这里直接使用onCreateOptionsMenu()方法中传入的menu参数。给这个方法返回true,表示与允创建的菜单显示出来,如果返回flase,创建的菜单将无法显示。
  定义菜单响应事件。再FirstActivity中重写onOptionsItemSelected()方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId(); // 可能是一个非常量值
if (itemId == R.id.add_item) {
Toast.makeText(this, "You click Add", Toast.LENGTH_SHORT).show();
} else if (itemId == R.id.remove_item) {
Toast.makeText(this, "You click Remove", Toast.LENGTH_SHORT).show();
} else {
// 处理默认情况
}
return super.onOptionsItemSelected(item);
}

在onOptionsItemSelected()方法中,通过调用item.getItemId()来判断我们点击的是哪一个菜单项,然后给每个菜单项加入自己的逻辑。
重新运行程序,会发现标题栏的右侧多了一个三点的符号,这个就是菜单按钮了。

销毁一个活动

现在已经学会了手动创建活动的方法,并且在活动中创建Toast和创建菜单。现在要学习销毁一个活动。
很简单,只要按一下Back键就可以销毁当前的活动了。当然还可以通过程序中的代码来销毁活动,Activity类提供了一个finish()方法,我们在活动中调用这个方法就可以销毁当前活动了。
修改按钮监听器中的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this,"You click Button",Toast.LENGTH_SHORT).show();
finish(); //这里加上之后,当点击按钮时,会退出程序。
}
});
}

使用Intent在活动之间穿梭

使用显式Intent

  快速在ActivityTest项目中再创建一个活动。这里创建活动还是新建一个class文件,然后把代码搬过来就OK了。
  给布局文件起名为second_layout。
  编辑second_layout.xml,定义一个按钮显示Button2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button2"
tools:layout_editor_absoluteX="170dp"
tools:layout_editor_absoluteY="104dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

然后我们在SecondActivity中加载布局

1
2
3
4
5
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout); //加载布局
}

接下来是在AndroidManifest.xml中注册活动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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.ActivityTest"
tools:targetApi="31">
<activity android:name=".FirstActivity"
android:exported="true"
android:label="@string/title_activity_first"
android:theme="@style/Theme.ActivityTest">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity"></activity> //注册活动
</application>

  由于SecondActivity不是主活动,因此不需要配置标签里的内容。
  现在第二个活动创建完成,接下来就是如何启动这第二个活动,这里我们引入一个新概念:Intent。
  Intent是Android程序中各组件之间进行交互的一种重要方式,不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务以及发送广播等场景,由于服务、广播等概念暂未涉及,现在就把目光锁定在启动活动上。
  Intent大致可以分为两种:显式Intent和隐式Intent,先来看显式Intent如何使用。
  Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?>cls)。这个构造函数接收两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数class则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent的”意图”。
  我们怎么使用Intent呢?Activity类中提供了一个startActivity()方法,这个方法是专门用于启动活动的,它接受一个Intent参数,我们将构建好的Intent传入startActivity()方法就可以启动目标活动了。
  修改FirstActivity中按钮的点击事件,如下:

1
2
3
4
5
6
7
8
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this,"You click Button",Toast.LENGTH_SHORT).show();
Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});

  我们首先构建出了一个Intent,传入FirstActivity.this作为上下文,传入SecondActivity.class作为目标活动,这样我们的意图就是在FirstActivity这个活动的基础上打开SecondActivity这个活动。通过startActivity()这个方法来执行Intent。
  运行程序,在FirstActivity的界面上点击按钮,就会启动SecondActivity这个活动,如果想回到上一个活动,按下Back键就会销毁当前活动,回到上一个活动。
  这种方式Intent的“意图”非常明显,我们称之为显式Intent。

使用隐式Intent

隐式Intent很含蓄,它并不明确指出我们想要启动哪个活动,而是指定一系列更为抽象的action和categoty等信息,然后交给系统去分析则会个Intent,并找出合适的活动去启动,简单讲就是可以响应我们这个隐式Intent的活动。
  通过在标签下配置的内容,可以指定当前活动能够响应的action和category,打开AndroidManifest.xml,添加代码:

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

  在标签中指明当前活动可以响应com.example.activity.ACTION_START这个action,而标签则包含了一些附加信息,更精确指明当前活动能够响应的Intent中还可能带有的category。只有中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应该Intent。
  修改FirstActivity中按钮事件,代码如下:

1
2
3
4
5
6
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this,"You click Button",Toast.LENGTH_SHORT).show();
Intent intent=new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);
}

  这里我们使用了Intent的另一个构造函数,直接将action的字符串传了进去,表名我们想要启动能够响应com.example.activity.ACTIN_START这个action的活动。而android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法时会自动将则会个category添加到Intent中。
  重新运行程序,在FirstActivity界面点击一下按钮,同样启动了SecondActivity了。不同的是,这次使用了隐式Intent的方式来启动的,说明我们在标签下配置的action和category生效了。
  每个Intent中只能指定一个action,但却能指定多个category。我们目前的Intent中只有一个默认的category,那现在再来增加一个。
  修改FirstActivity中按钮的点击事件,代码如下:

1
2
3
4
5
6
7
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this,"You click Button",Toast.LENGTH_SHORT).show();
Intent intent=new Intent("com.example.activitytest.ACTION_START");
intent.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(intent);
}

  调用Intent中的addCategory()方法来添加一个category,这里我们指定了一个自定义的category,值为com.example.activitytest.MY_CATEGORY
  运行程序,在FirstActivity的界面点击一下按钮,会发现程序崩溃了。我们到logcat界面查看错误日志,会看到

  提醒我们没有任何一个活动可以响应我们的Intent,因为我们刚刚在Intent中新增了一个category,而SecondActivity的标签中并没有声明可以响应这个category,就出现了没有任何活动可以响应Intent的情况。我们现在在中添加category的声明,如下:

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

再次运行程序,就正常了。

更多隐式Intent的用法

  实际上隐式Intent还有很多的内容要了解。
  使用隐式Intent,不仅可以启动自己程序内的活动,还可以启动其他程序的活动。比如说在自己的应用程序中展示一个网页,这时没必要自己去实现一个浏览器,而是只需要调用系统的浏览器来打开这个网页就行了。
  修改FirstActivity中按钮点击事件的代码,如下:

1
2
3
4
5
6
7
8
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
}
});

  这里我们首先指定Intent的action是Intent.ACTION_VIEW,这是Android系统内置的动作,其常量值为android.intent.action.VIEW。然后通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。
  重新运行程序,在FirstActivity界面点击按钮就可以看到打开了系统浏览器。
  上述代码中出现了一个方法setData(),它接收一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据通常都是以字符串的形式传入到Uri.parse()方法中解析产生的。
  与此对应,我们还可以在标签中再配置一个标签,用于更精确地指定当前活动能够响应什么类型的数据。标签中主要可以配置以下内容。

  • android:scheme。用于指定数据的协议部分,如上例中的http部分。
  • android:host。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
  • android:port。用于指定数据的端口部分,一般紧随在主机名之后。
  • android:path。用于指定主机名和端口之后的部分,一般紧随在主机名之后。
  • android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

  只有标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。不过一般再标签中不会指定过多内容,上述浏览器示例中,其实只需要指定android:scheme为http,就可以响应所有的http协议的Intent。
  为了更直观理解,我们自己建立一个活动,让它也能响应网页的Intent。
右击com.example.activity包->New->Activity->Empty Activity,新建ThirdActivity,勾选Generate Layout File,给布局文件起名为third_layout,点击finish完成创建。然后编辑third_layout.xml将里面的代码替换为:

1
2
3
4
5
6
7
8
9
10
<activity
android:name=".ThirdActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
</intent-filter>
</activity>

  我们在ThirdActivity的中配置了当前活动能够响应的action时Intent.ACTION_VIEW的常量值,而category指定了默认的category值,在标签中通过android:scheme指定了数据的协议必须是http协议,这样ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent了。
  运行程序,在FirstActivity的界面点击按钮。点击了之后,系统会自动弹出一个列表(我的手机上显示的是chome和ActivityTest),显示能响应则会个Intent的所有程序。选择chome就是和之前一样打开浏览器,显示百度主页,而选择Activity,则会启动ThirdActivity。虽然我们声明了ThirdActivity可以响应网页的Intent,但实际上这个活动并没有加载并显示网页的功能。
  除了http协议外,我们还能指定许多其他协议,如geo表示地理位置,tel表示拨打电话。在程序中调用系统拨号界面:

1
2
3
4
5
6
7
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}

  首先指定了Intent的action是Intent.ACTION_DIAL,这也是Android系统的内置动作。在data部分指定了协议是tel,号码是10086。重新运行程序,在FirstActivity界面点击按钮,就会跳转到拨号界面,而且拨号的内容是10086。

向下一个活动传递数据

  Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,启动了另一个活动之后,只需把这些数据再从Intent中取出就可以了。
  FirstActivity中有一个字符串,现在想要把这个字符串传递到SecondActivity中,就可以编写:

1
2
3
4
5
6
7
8
9
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String data = "Hello SecondActivity";
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("extra_data",data);
startActivity(intent);
}
});

  这里用显式Intent的方式来启动SecondActivity,并通过putExtra()方法传递了一个字符串。这里putExtra()方法接收两个参数,第一个参数是键,用于后面从Intent中取值,第二个参数才是真正要传递的数据。
  然后我们在SecondActivity中将传递的数据取出,并打印出来,代码如下:

1
2
3
4
5
6
7
8
9
10
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
Log.d("SecondActivity",data);
}
}

  首先通过getIntent()方法获取到用于启动SecondActivity的Intent,然后调用getStringExtra()方法,传入相应的键值,就可以得到传递的数据了。这里我们传递的是字符串,所以用getStringExtra()方法来获取传递的数据。若是整型数据,则使用getIntExtra()方法;若是布尔型数据,则使用getBooleanExtra()方法。
  重新运行程序,在FirstActivity界面点击按钮会跳转到SecondActivity,查看logcat打印信息。

能看到成功从SecondActivity中得到了FirstActivity传递过来的数据。

返回数据给上一个活动

  返回上一个活动只需要按一下Back键就可以了,并没有用于启动活动Intent来传递数据。如果查阅文档的话,会发现,Activity中还有一个startActivityForResult()方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。
  startActivityForResult()方法接收两个参数,第一个参数还是Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源。
  我们来实战,修改FirstActivity中按钮的点击事件,代码如下:

1
2
3
4
5
6
7
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivityForResult(intent,1);
}
});

这里我们使用startActivityForResult()来启动SecondActivity,请求码只要是一个唯一值就可以了,这里传入了1。接下来我们在SecondActivity中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Button button2 = (Button) findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("data_return","Hello FirstActivity");
setResult(RESULT_OK,intent);
finish();
}
});
}
}

  我们创建了一个Intent,这个Intent仅仅是用于传递数据而已,没有指定任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用setResult()方法。这个方法很重要,是专门用于向上一个活动返回数据的。setResult()方法接收两个参数,第一个参数用于向上一个活动返回处理结果,,一般只使用RESULT_OK或RESULT_CANCELED这两个值,第二个参数则是把带有数据的Intent传递回去,然后调用finish()方法来销毁当前活动。
  由于我们是使用startActivityForResult()方法来启动SecondActivity,在SecondActivity被销毁之后会回调上一个活动的onActivityResult()方法,因此我们需要在FirstActivity中重写这个方法来得到 返回的数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode){
case 1:
if (resultCode == RESULT_OK){
String returnedData = data.getStringExtra("data_return");
Log.d("FirstActivity",returnedData);
}
break;
default:
}
}

  onActivityResult()方法带有三个参数,第一个参数requestCode,即在我们启动活动时传入的请求码。第二个参数resultCode,即我们在返回数据时的处理结果。第三个参数data,即携带返回数据的Intent。由于在一个活动中有可能调用startActivityForResult()方法去启动很多不同的活动,每一个活动返回的数据都会回调到onActivityResult()这个方法中,因此我们首先要做的就是通过检查requestCode的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们通过resultCode的值来判断结果处理是否成功。最后从data中取值并打印出来,这样就完成了向上一个活动返回数据的工作。
  重新运行程序,在FirstActivity的界面点击按钮会打开SecondActivity,然后在SecondActivity界面点击Button2按钮会回到FirstActivity,这时查看logcat的打印信息

  这时SecondActivity已经成功返回数据给FirstActivity了。
  如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity,这样数据就没法返回了。但是我们可以通过在SecondActivity中重写onBackPressed()方法来解决这个问题,代码如下:

1
2
3
4
5
6
7
8
@Override
public void onBackPressed() {
super.onBackPressed();
Intent intent = new Intent();
intent.putExtra("data_return","Hello FirstActivity Back");
setResult(RESULT_OK,intent);
finish();
}

这样子,按下Back键,就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。
这里我按下Back,并没有生效。暂未解决,后续解决了再来补充。

活动的声明周期

返回栈

  Android中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击Back键就会销毁最上面的活动,下面的一个活动就会重新显示出来。
  Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。栈是一种后进先出的数据结构,默认情况下,当我们启动了一个新的活动,它会返回栈中入栈,并处于栈顶的位置,每当我们按下Back键或调用finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。

活动状态

  每一个活动在其生命周期中最多会有4中状态。
1.运行状态
当一个活动位于返回栈的栈顶时,这时活动就会处于运行状态。
2.暂停状态
当一个活动不再处于栈顶位置,但仍然可见时,活动就进入了暂停状态。处于暂停状态的活动仍然存活着,只有在内存极低的情况下,系统才会去考虑回收这种活动。
3.停止状态
当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态,系统仍然会为这种活动保存相应的状态和成员变量,当其他地方需要内存时,处于暂停状态的活动有可能会被系统回收。
4.销毁状态
当一个活动从返回栈中移除后就变成了销毁状态。

活动的生存期

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

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

活动可以分为三种生存期

  • 完整生存期。活动在onCreat()方法和onDestory之间所经历的,就是完整生存期。一般情况下,一个活动会在onCreat()方法中完成各种初始化操作,而在onDestory()方法中完成释放内存的操作。
  • 可见生存期。活动在onStart()方法和onStop()方法之间经历的,就是可见生存期。在可见生存期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法,合理地管理那些对用户可见的资源。
  • 前台生存期。活动在onResume()方法和onPause()方法之间经历的就是前台生存期。在前台生存期,活动总是处于运行状态的,此时的活动是可以和用户进行交互的。

体验活动的生命周期

  通过一个实例,更加直观地体验活动的生命周期。
  关闭ActivityTest项目,新建一个ActivityLifeCycleTest项目,这次允许Android Studio为我们自动创建活动和布局,并且勾选Launcher Activity来创建的活动设置为主活动。
  主活动创建完成,我们再分别创建两个子活动NormalActivity和DialogActivity,并且为其创建布局文件normal_layout和dialog_layout。
  编辑normal_layout.xml文件,将里面的代码替换为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tihs is a normal activity" //添加了一个TextView元素
tools:layout_editor_absoluteX="168dp"
tools:layout_editor_absoluteY="85dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

  这个布局中我们就简单的使用了一个TextView,用于显示一行文字。
  然后编辑dialog_layout.xml文件,将里面的代码替换为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a dialog activity"
tools:layout_editor_absoluteX="162dp"
tools:layout_editor_absoluteY="112dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

两个布局文件的代码几乎没有区别,知识显示的文字不同而已。从名字可以看出一个是普通的活动,一个是对话框式的活动。如何体现活动设成对话框式的呢,修改AndroidManifest.xml的标签的配置:

1
2
3
4
5
6
7
8
9
<activity android:name=".NormalActivity"
android:exported="true">

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

</activity>

  这是两个活动的注册码,在DialogActivity中,我们给它添加了一个android:theme属性,用于指定主题的,Android系统内置有很多主题可以选择,我们也可以制定自己的主题,而这里@android:style/Theme.Dialog是使用对话框式的主题。
  修改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
<?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">

<Button
android:id="@+id/start_normal_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start NormalActivity"
tools:layout_editor_absoluteX="133dp"
tools:layout_editor_absoluteY="81dp" />

<Button
android:id="@+id/start_dialog_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start DialogActivity"
tools:layout_editor_absoluteX="137dp"
tools:layout_editor_absoluteY="211dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

  我们加入了两个按钮,一个用于启动NormalActivity,一个用于启动DialogActivity。然后修改MainActivity中的代码:

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
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button start_normal_activity = (Button) findViewById(R.id.start_normal_activity);
Button start_dialog_activity = (Button) findViewById(R.id.start_dialog_activity);
start_normal_activity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,NormalActivity.class);
startActivity(intent);
}
});
start_dialog_activity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,DialogActivity.class);
startActivity(intent);
}
});
}

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

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

@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,"onDestory");
}

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

  在onCreate()方法中,我们分别为两个按钮注册了点击事件,点击第一个按钮会启动NormalActivity,点击第二个按钮会启动DialogActivity。

修bug

  程序运行出来之后,两个按钮的位置重叠在一起了。我起初使用的方法是将布局文件里的根元素androidx.constraintlayout.widget.ConstraintLayout改为了LinearLayout,两个按钮分开了,但是还有一个问题就是点击Start NormalActivity出现的是空白页面,然后我发现是在NormalActivity活动没有加载布局,修改:

1
2
3
4
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.normal_layout); //加载布局
}

  同时,我搜集到有关ConstraintLayout的信息是

如果将 ConstraintLayout 更改为 LinearLayout 后按钮不再重叠,这可能是因为 LinearLayout 使用了默认的排列方式,即垂直或水平排列,使得视图在布局中自动按顺序排列,而不会重叠。但是,这并不意味着问题已经完全解决,而只是绕过了布局问题。

使用 LinearLayout 可能会限制您在布局中实现复杂的 UI 结构和定位。 ConstraintLayout 提供了更灵活和强大的布局功能,允许您以更精确的方式定位和对齐视图。因此,我建议您尝试使用 ConstraintLayout 并逐步解决按钮重叠问题,以便更好地利用其功能。

关于按钮的位置应该会在学UI时得到解决。

观察logcat的打印日志

  MainActivity第一次被创建时会依次执行 onCreat()、onStart()和onResume()方法。
  启动第一个按钮,启动NormalActivity。打印信息:

  由于NormalActivity已经把mainActivity完全遮住,因此onPause()和onStop()都会得到执行。然后按下Back键返回MainActivity。打印信息:

  由于之前MainActivity已经进入停止状态,所以onRestart()方法会得到执行,之后依次执行onStart()和onResume()方法。此时onCreate方法不会执行,因为MainActicity并没有重建。
  点击第二个按钮,启动DialogActivity。打印信息:

  只有onPause()方法得到了执行,onStop()方法并没有得到执行,因为DialogActivity并没有完全遮住MainActivity,此时MainActivity只是进入了暂停状态,并没有停止。相应的,按下Back键返回MainActivity也应该只有onResume方法会得到执行:

最后在MainActicity按下Back键退出程序

  依次执行onPause()、onStop()、onDestory()方法,最终销毁MainActivity。

活动被回收了怎么办

  当一个活动进入停止状态,是有可能被系统回收的。场景:应用中有一个活动A,用户在活动A的基础上启动了活动B,活动A就进入了停止状态,这时候由于内存不足,将活动A回收掉了,然后用户按下Back键返回活动A,其实还是会显示活动A,但这时不会执行onRestart()方法,而是会执行活动A的onCreat()方法,活动A被重新创建 了。
  活动A中可能存在临时数据和状态。比如,MainActivity中有一个文本输入框,输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉了,过了一会点击Back键返回MainActivity,而刚刚输入的文字都没了,因为MainActivity被重新创建了。
  Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在活动回收之前一定会被调用,我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。
  onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列方法用于保存数据,比如putString()方法保存字符串,putInt()方法保存整型数据,类推。每个保存方法需要传入两个参数,第一个是键,用于从后面的Bundle中取值,第二个参数是真正要保存的内容。
在MainActivity中添加代码就可以将临时数据进行保存。

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

  在onCreate()方法中也有一个Bundle类型的参数,这个参数在一般情况下都是null,但是如果活动被系统回收之前通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所有保存的全部数据,我们通过相应的取值方法将数据取出即可。
  修改MainActicity的onCreat()方法,如下所示:

1
2
3
4
5
6
7
8
9
10
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG,"onCreat");
if(savedInstanceState != null){
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG,tempData);
}
}

  取出值之后,再进行相应的操作即可,比如将文本内容重新赋值到文本输入框上,这里我们知识简单的打印以下。
  使用Bundle来保存和取出数据和在使用Intent传递数据时也是用的类似的方法。Intent还可以结合Bundle一起用于传递数据,首先可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象放在Intent里。到了目标活动之后再从Intent中取出Bundle,再从Bundle中一一取出数据。

活动的启动模式

启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过标签指定android:launchMode属性来选择启动模式。

standard

  standard是活动默认的启动模式,在不进行显示指定的情况下,所有活动都会自动使用这种启动模式。Android是使用返回栈来管理活动的,在standard模式下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
  关闭ActivityLifeCycleTest项目,打开Activity项目。修改FirstActivity中onCreate()方法的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity",this.toString());
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this,FirstActivity.class);
startActivity(intent);
}
});
}

  这段代码是在FirstActivity的基础上启动FirstActivity,还在onCreate()方法中添加了一行打印信息,用于打印当前活动的实例。
  现在重新运行程序,在FirstActivity界面连续点击两次按钮,可以在logcat看到:

可以看出,每点击一次按钮就会创建出一个新的FirstActivity实例。此时返回栈中会存在3个FirstActivity的实例,需要连按3次Back键才能退出程序。

singleTop

  当活动的启动模式指定为singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。
实践
修改AndroidManifest.xml中FirstActivity的启动模式:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name=".FirstActivity"
android:launchMode="singleTop" //here
android:exported="true"
android:label="@string/title_activity_first"
android:theme="@style/Theme.ActivityTest">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

重新运行程序,查看logcat已经创建了一个FirstActivity:

  之后不管点击多少次按钮都不会再有新的打印信息出现,因为目前FirstActivity已经处于返回栈的栈顶,每当想再启动一个FirstActivity时都会直接使用栈顶的活动,FirstActivity也只会有一个实例,仅按一次Back键就可以退出程序。
  但当FirstActivity并未处于栈顶位置时,这时再启动FirstActivity还是会创建新的实例的。
实践
修改FirstActivity中onCreate()方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity",this.toString());
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this,SecondActivity.class); //here
startActivity(intent);
}
});
}

点击按钮后启动的是SecondActivity。然后修改SecondActivity中onCreate()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Button button2 = (Button) findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(SecondActivity.this,FirstActivity.class); //here
startActivity(intent);
}
});
}

  在SecondActivity中的按钮点击事件里加入了启动FirstActivity的启动代码。重新运行程序,在FirstActivity界面点击按钮进入SecondActivity,然后再SecondActivity界面点击按钮,又会进入到FirstActivity。
  查看打印信息:

可以看到创建了两个不同的FirstActivity实例。按下Back键会回到SecondActivity,再次按下Back键又会回到FirstActivity,再按一次Back键才会退出程序。

singleTask

当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。
实践
修改AndroidManifest.xml中的启动模式:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name=".FirstActivity"
android:launchMode="singleTask" //here
android:exported="true"
android:label="@string/title_activity_first"
android:theme="@style/Theme.ActivityTest">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

在FirstActivity中添加onRestart()方法,并打印日志:

1
2
3
4
5
@Override
protected void onRestart() {
super.onRestart();
Log.d("FirstActivity","onRestart");
}

在SecondActivity中添加onDestory()方法,并打印日志:

1
2
3
4
5
@Override
protected void onDestroy() {
super.onDestroy();
Log.d("SecondActivity","onDestory");
}

重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入FirstActivity。


从打印信息中看出,在SecondActivity中启动FirstActivity时,会发现栈中已经存在一个FirstActivity的实例,并且是在SecondActivity的下面,于是SecondActivity会从返回栈中出栈,而FirstActivity重新成为了栈顶活动,FirstActivity的onRestart()方法和SecondActivity的onDestory()方法会得到执行。现在返回栈中只剩下一个FirstActivity的实例,按一下Back键就可以退出程序了。

singleInstance

  singleInstance模式应该算是四种启动模式中最特殊也是最复杂的一个了。指定为singleInstance模式的活动会启用一个新的返回栈来管理这个活动(如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈。)这样做的意义就是,假设我们的程序中有一个活动是允许其他程序调用的,我们想实现其他程序和我们的程序可以共享这个活动的实例,如何实现,前3种是做不到的,因为每个程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例。而是用singleInstance模式就可以解决这个问题,这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都公用的同一个返回栈,也就解决了共享活动实例的问题。
实践
修改AndroidManifest.xml中SecondActivity的启动模式。

1
2
3
4
5
6
7
8
9
10
<activity
android:name=".SecondActivity"
android:exported="true"
android:launchMode="singleInstance"> //here
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.example.activitytest.MY_CATEGORY"/>
</intent-filter>
</activity>

先将SecondActivity的启动模式指定为singleInstance,然后修改FirstActivity中onCreate()方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity","Task id is"+getTaskId()); //here
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});
}

在onCreate()方法中打印当前返回栈的id。然后修改SecondActivity中onCreate()方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Log.d("SecondActivity","Task id is"+getTaskId()); //here
Button button2 = (Button) findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(SecondActivity.this,ThirdActivity.class); //here
startActivity(intent);
}
});
}

同样是在onCreate()方法中打印当前返回栈的id,然后修改了按钮点击事件的代码,用于启动ThirdActivity。最后来修改ThirdActivity中onCreate()方法的代码:

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("ThirdActivity","This id is"+getTaskId());
setContentView(R.layout.third_layout);
}

仍然是在onCreate()方法中打印了当前返回栈的id。
现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮进入ThirdActivity。查看logcat:



  可以看到,SecondActivity的Task id不同于FirstActivity和ThirdActivity,这说明SecondActivity确实是存放在一个单独的返回栈里的,这且这个栈中只有SecondActivity这一个活动。
  接着我们按下Back键进行返回,会发现ThirdActivity直接返回到了FirstActivity,再按下返回键又会返回到SecondActivity,再按下Back键才会退出程序。原理很简单,由于FirstActivity和SecondActivity会从返回栈中出栈,当在ThirdActivity的界面下按下Back键,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶活动显示在界面上,因此会出现从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示另一个返回栈的栈顶活动,即SecondActivity。最后按下Back键,这时所有的返回栈都已经空了,也就退出了程序。

活动的最佳实践

知晓当前是在哪一个活动

在ActivityTest项目的基础上修改,首先需要新建一个BaseActivity类。右击com.example.activity包->New->Java Class,在弹出的窗口输入BaseActivity,不同的是,我们不需要让BaseActivity在AndroidManifest中注册,然后让BaseActivity继承自AppCompatActivity,并重写onCreate()方法,如下:

1
2
3
4
5
6
7
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());
}
}

  在onCreate()方法中获取了当前实例的类名,并通过Log打印了出来。
  让BaseActivity成为ActivityTest项目中所有活动的父类。修改FirstActivity、SecondActivity、ThirdActivity的继承结构,让他们不在继承自AppCompatActivity,而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中的活动的功能并不受影响,他们仍然完全继承了Activity中的所有特性。
  重新运行程序,通过点击按钮分别进入到了FirstActivity、SecondActivity、ThirdActivity的界面,这时查看打印信息:

  每当我们进入到一个活动的界面,该活动的类名就会被打印出来,就可以时时刻刻知晓当前界面对应的是哪一个活动了。

随时随地退出程序

如果当前手机界面还停留在ThirdActivity,那么退出程序很不方便,需要连按3次Back键才行。按home键只是把程序挂起,并没有退出程序。解决思路就是用一个专门的集合类对所有的活动进行管理就可以了。新建一个ActivityCollector类作为活动管理器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ActivityCollector {
public static List<Activity> activities = new ArrayList<>();
public static void addActivity(Activity activity){
activities.add(activity);
}
public static void removeActivity(Activity activity){
activities.remove(activity);
}
public static void finishAll(){
for (Activity activity: activities){
if(!activity.isFinishing()){
activity.finish();
}
}
}
}

  在活动管理器中,我们通过一个List来暂存活动,然后提供一个addActivity()方法用于向List中添加一个活动,提供removeActivity()方法用于从List中移除活动,最后提供一个finish()方法用于将List中存储的活动全部销毁掉。
  修改BaseActivity()中的代码,如下:

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

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

  在BaseActivity的onCreate()方法中调用ActivityCollector的addActivity()方法,表名将当前正在创建的活动添加到活动管理器中。然后重写了onDestory()方法,并调用ActivityCollector的removeActivity()方法,表名将一个马上要销毁的活动从活动管理器中移除。
  接下来,不管想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了。
  如在ThirdActivity界面想通过点击按钮直接退出程序,只需为修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThirdActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("ThirdActivity","This id is "+getTaskId());
setContentView(R.layout.third_layout);
Button button3 = (Button) findViewById(R.id.button3);
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ActivityCollector.finishAll();
}
});
}
}

当然还可以在销毁所有活动的代码后面加上杀掉当前进程的代码,以保证程序完全退出,杀掉进程的代码如下:

1
android.os.Process.killProcess(android.os.Process.myPid());

  其中killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们通过myPid()方法来获得当前进程的id。注意:killProcess()方法只能用于杀掉当前程序的进程,不能使用这个方法杀掉其他程序。

启动活动的最佳写法

  启动活动的方法是是通过Intent构建出当前的“意图”,然后通过调用startActivity()或startActivityForResult()方法将活动启动起来,如果有数据需要从一个活动传递到另一个活动,也可以借助Intent完成。
  假设SecondActivity中需要用到两个很重要的字符串参数,在启动SecondActivity的时候必须要传递过来,那么我们可以写:

1
2
3
4
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("param1","data1");
intent.putExtra("param2","data2");
startActivity(intent);

  这样写没毛病,但是在真正的项目开发中经常会有对接的问题出现。比如SecondActivity并不是自己开发的,但现在自己负责的部分需要启动SecondActivity这个功能,却不清楚要传递哪些数据,这时就两种办法,一个是自己阅读SecondActivity中的代码,二是询问负责编写SecondActivity的同事。其实换一种写法就可以了,修改SecondActivity中的代码:

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

  我们在SecondActivity中添加了一个actionStart()方法,在这个方法中完成了Intent的构建,另外所有SecondActivity中需要的数据都是通过actionStart()方法的参数传递过来的,然后把他们存储到Intent中,最后调用startActivity方法启动SecondActivity。
  这样写的好处就是,SecondActivity所需要的数据在方法参数中全部体现出来了,还可以简化代码,现在只需要一行代码就可以启动SecondActivity,如下:

1
2
3
4
5
6
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SecondActivity.actionStart(FirstActivity.this,"data1","data2"); //here
}
});

这样写可以让启动活动变得非常简单。

Android 系统架构

安卓操作系统的体系结构可以分为四个主要层级:
1.Linux内核层:Android基于Linux内核构建,它提供了底层的硬件抽象层(Hardware Abstraction Layer,HAL),用于管理设备驱动程序、内存管理、进程管理、安全性等底层功能。
2.系统库和运行时环境层:这一层包括了Android Runtime(ART)和核心的Java类库。ART是一个虚拟机,用于执行应用程序的字节码。Android平台使用的是基于Java的编程语言,应用程序通常以Dalvik Executable(DEX)格式运行在ART上。此外,Android还提供了一系列的系统库,用于实现常见的功能,例如图形渲染、数据库访问、网络通信等。
3.应用框架层:这一层提供了开发Android应用程序所需的各种服务和API。开发人员可以利用这些API来构建应用程序,包括用户界面(UI)组件、通知系统、数据存储、多媒体支持、位置服务等。Android应用程序通过调用应用框架中的API来与底层系统进行交互。
4.应用层:最顶层是应用层,包括了用户直接与之交互的应用程序,例如浏览器、短信应用、社交媒体应用等。这些应用程序是构建在Android应用框架之上的,它们通过调用框架提供的服务来实现各种功能。

四大组件

分别是Activity、Service、BroadcastReceive和ContentProvider。
Activity是所有Android应用程序的门面,凡是在应用中看得到的东西,都是放在Activity中的。
Service比较低调,无法看到它,但它会在后台默默地运行,即使用户瑞出了应用,Service仍然是可以继续运行的。
BroadcastReceive允许应用接收来自各处的广播消息,也可以向外发出广播消息。
ContentProvider为应用程序之间共享数据提供了可能。

开发环境

要准备的工具:
JDK
Android SDK
Android Studio

创建第一个项目 HelloWorld

创建过程略

这就创建成功了
运行程序即可把app推送到手机上并运行。

分析app程序

将项目结构模式切换为Project,就能看到真是目录结构了。

Project目录结构分析

1.gradle和.idea
这两个目录下放置的都是Android Studio 自动生成的一些文件,我们无需关心和编辑。
2.app
项目中的代码、资源等内容几乎都是放置在这个目录下的,我们后面的开发工作也都是在这个目录下进行的。
3.build
这个目录也无需关心,它主要包含了一些在编译时自动生成的文件。
4.gradle
这个目录包含了gradle wrapper的配置文件,使用gradle wrapper的方式不需要提前将gradle下载好,而是会自动根据本地的缓存情况决定是否需要联网下载gradle。
Android Studio默认没有启用gradle wrapper的方式,如果需要打开,可以点击Android Studio导航栏->File->Settings->Buile,Execution,Deployment->Buile Tools->Gradle,进行配置更改。

app目录内容分析

1.build
包含编译时自动生成的文件
2.libs
如果项目中使用到了第三方jar包,就需要把这些jar包都放在libs目录下,该目录下的jar包都会被自动添加到构建路径里去。
3.androidTest
这里编写Android Test测试用例的,可以对项目进行一些自动化调试
4.java
java目录是放置我们所有java代码的地方,展开该目录,将看到刚才创建的HelloWorld文件。
5.res
这个目录下内容很多,项目中使用到的所有的图片、布局、字符串等资源都要放在这个目录下。
这个目录下还有很多子目录,图片放在drawable目录下,布局放在layout目录下,字符串放在values目录下等。
6.AndroidManifest.xml
这是整个Android项目的配置文件,在程序中定义的所有四大组件都需要在这个文件里注册,还可以在这个文件中。
7.test
用来编写UnitTest测试用例的,是对项目及逆行自动化测试的另一种方式。
8. .gitignore
这个文件用于将app模块内的指定的目录或文件排除在版本控制之外,作用和外层的.gitignore文件类似。
9.build。gradle
这是app模块的gradle构建脚本,这个文件会指定很多项目构建相关的配置。
10.proguard-rules.pro
这个文件用于指定项目代码的混淆规则,当代码开发完成后打成安装包文件,如果不希望别人破解,通常会将代码进行混淆,从而让破解者难以阅读。

分析HelloWorld 项目

首先打开AndroidManifest.xml文件,找到

1
2
3
4
5
6
7
8
9
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

这段代码表示对MainActivity这个活动进行注册,没有在AndroidManifest.xml里注册的活动是不能使用的。
其中intent-filer里的两行代码非常重要是这个项目的主活动,在手机上点击应用图标,首先启动的就是这个活动。
打开MainActivity,代码如下

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

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

从代码中可以看出MainActivity是继承自AppCompatActivity的,这是一种向下兼容的Activity,可以将Activity在各个系统版本中增加的特性和功能最低兼容到Android 2.1系统。
Actiity是Android系统提供的一个活动基类,我们项目中所有的活动都必须继承它或者它的子类才能拥有活动的特性(AppCompatActivity 是 Activity 的子类)。
在MainActivity里有一个onCreat()方法,这个方法是一个活动被创建时必定要执行的方法,其中有两行代码,但并没有”HelloWorld”字样。
Android程序的设计讲究逻辑和视图分离,通用做法就是在布局文件里编写界面,然后在活动中引入进来。
在onCreat()方法的第二行调用了 setContentView()方法,这个方法给当前活动引入了activity_main布局,HelloWorld就在那个文件里定义。
打开布局文件,布局文件都定义在res/layout目录下,展开layout目录会看到activity_main.xml文件。打开文件切换到Text视图,有如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

可以看到其中有TextView,这是Android系统提供的一个控件,用于在布局中显示文字的。其中有android:text:"Hello World!",在这里定义的。

详解项目中的资源

展开res目录,里面有很多文件夹。

所有以drawable开头的文件夹都是用来放图片的,所有以mipmap开头的文件夹都是用来放应用图标的,所有以values开头的文件夹都是用来放字符串、样式、颜色等配置的,layout文件夹时用来放布局文件的。
之所以有这么多mipmap开头的文件夹,主要是为了让程序更好地兼容各种设备。
drawable文件夹也是这样,我们应该自己创建以drawable开头的文件夹。
打开 res/values/strings.xml文件,有以下内容:

1
2
3
<resources>
<string name="app_name">HelloWorld</string>
</resources>

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

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

其中string部分是可以替换的,如果引用的图片资源就可以替换成drawable,如果是引用的图片资源就可以替换成drawable,如果引用的是应用图标就可以替换成mipmap,如果引用的是布局文件就可以替换成layout。
举个栗子,打开AndroidManifest.xml文件,找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
<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.HelloWorld"
...
</application>

其中,HelloWorld项目的应用图标就是通过android:icon属性来指定的,应用名称则是通过android:label属性来指定的。这里对资源的引用方式就是刚刚的XML中引用资源的语法。

详解build.gradle文件

Android Studio是采用Gradle构建项目的。Gradle是一个非常先进的项目构建工具,它使用了一种基于Groovy的领域特定语言(DSL)来声明项目设置。

掌握日志工具的使用

使用Android的日志工具Log

Android中的日志工具类是Log(android.util.Log),这个类中提供了5个方法来供我们打印日志。

  • Log.v()。用于打印那些最为繁琐、意义最小的日志信息。对应级别verbose。
  • Log.d()。用于打印一些调试信息,这些信息对调试程序和分析问题应该是有帮助的。对应级别是debug。
  • Log.i()。用于打印一些比较重要的数据,这些数据应该是可以帮助分析用户行为的数据。对应级别info。
  • Log.w()。用于打印一些警告信息,提示程序在这个地方可能会有潜在风险,最好去修复一下出现警告的地方。对应级别warn。
  • Log.e()。用于打印程序中的错误信息当有错误信息打印出来的时候,一般都代表程序出现问题了,要尽快修复。对用级别error。
    一共有5个方法,每个方法还会有不同的重载。
    我们在HelloWorld项目中试试日志工具好不好用吧。
    打开MainActivity文件,在onCreat方法中添加一行打印日志的语句
    1
    2
    3
    4
    5
    6
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.d("MainActivity","onCreat execute");
    }
    Log.d()方法里传入了两个参数:第一个参数是tag,一般传入当前的类名就好,主要用于对打印信息进行过滤;第二个参数是msg,即想要打印的具体的内容。
    重新运行HelloWorld项目,点击顶部工具栏运行按钮,或者快捷键Shift+F10,等程序运行完毕,在logcat中就可以看到打印信息了。

    其中,可以看到打印日志的内容和tag名,还有包名、打印时间以及应用程序的进程号都可以看到

为什么用Log而不用System.out

在Java中经常使用System.out.println()方法来打印日志,但在项目开发中,极度不建议使用System.out.println()。
System.out除了使用方便之外一无是处。
快捷输入,在Android Studio中,想打印一条debug级别的日志,只需输入logd,回车,即可自动代码补全。

数据模型

客户端想要去操作MYSQL数据库,首先客户端向数据库服务器发送对应的SQL语句,在服务器内部,会有一个软件叫DBMS数据库管理系统,它会去维护并且操作数据库,以及创建数据库,一个数据库里可以维护多张表,数据存储在表结构当中。

  • 关系型数据库(RDBMS)概念:建立在关系型基础上,由多张相互连接的二维表组成的数据库。
  • 非关系型数据库不通过表结构存储数据的数据库。
阅读全文 »

编程语言发展

  在晶体管计算机流行的年代(1940s-1950s),编程主要由机器语言和汇编语言来实现。1954年第一个高级语言Fortran诞生,随后1958年ALGO语言发布,称ALGO58,1960年有了ALGO60,后来很多语言基于ALGO60发展起来,其中有一个是CPL,由于CPL规模太大,不易实现,1967年英国剑桥大学对其进行了简化,推出了BCPL,随后美国贝尔实验室的肯汤姆森以BCPL为基础,又进行简化,设计出来接近硬件的语言叫B语言,后来觉得B语言太过简单了,又在B语言的基础上设计出了C语言。

什么是C语言

  c语言是一门通用的计算机编程语言,广泛用于底层开发。c和c++是编译型的语言。test.c->编译->链接->test.exe。

NexT主题启用

我选择的主题是:NexT
在博客文件夹路径下执行

1
git clone https://github.com/next-theme/hexo-theme-next themes/next
阅读全文 »