安卓开发(试水篇)
Prerequisite
Android Studio 下载网站:https://developer.android.com/studio 【默认安装最新版】
前置知识
- SDK 全称 Software Development Kit,译为软件开发工具包
- Android Studio 运行项目途径
- 真机(建议)
- 模拟器
- IDE 自带虚拟设备(不建议)
- 采用 XML 文件进行配置工程相关参数(如布局)
- 注释用
<!-- -->
,注意只能在单元外
- 注释用
环境
【下载 SDK 组件】下载刚完成,还需要配置一些东西,但我忘了
【环境变量】打开 SDK 安装目录(C:\Users\XXX\AppData\Local\Android\Sdk
),将以下两个目录添加到环境变量
C:\Users\XXX\AppData\Local\Android\Sdk\platform-tools
C:\Users\XXX\AppData\Local\Android\Sdk\tools
【真机调试】我用的是小米 6,需要开启开发者模式,并开启USB 调试
和USB 安装
终端常用命令
# 查看设备
adb devices
# 关闭 adb
adb kill-server
# 开启 adb
adb start-server
# 进入 shell 模式
adb shell
# 查看安卓版本
adb shell getprop ro.build.version.release
# 查看 SDK 版本
adb shell getprop ro.build.version.sdk
# 获取 root 权限(确保已经获取权限)
adb root
# 安装 apk
adb install C:\Users\xxx\Desktop\Magisk.apk
快捷键(官方指南)
- 代码格式化【Ctrl + Alt + L】
入门目标
- 安卓 UI 和后台逻辑
- 网络请求
- 序列化和反序列化
- 保存 XML 文件
最终可以实现一个含有登录和跳转功能的小型 app
PS:创建项目选择 empty project
安卓 UI 和后台逻辑
切换到 Project 项目视图,其中后台逻辑和 UI 界面的功能是相对应的
- main
- java
- com.example.android_1
- MainActivity【后台逻辑】
- com.example.android_1
- res
- layout
- activity_main.xml【UI 界面】
- values
- strings.xml【字符串变量】
- layout
- java
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="150dp"
android:background="#ddd"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/txt"
android:textAlignment="center"
android:textSize="20dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingLeft="50dp"
android:paddingRight="50dp">
<TextView
android:layout_width="60dp"
android:layout_height="wrap_content"
android:gravity="right"
android:text="@string/user" />
<EditText
android:id="@+id/txt_user"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="text"></EditText>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingLeft="50dp"
android:paddingRight="50dp">
<TextView
android:layout_width="60dp"
android:layout_height="wrap_content"
android:gravity="right"
android:text="@string/pwd" />
<EditText
android:id="@+id/txt_pwd"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword"></EditText>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center">
<Button
android:id="@+id/btn_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:text="登 录"></Button>
<Button
android:id="@+id/btn_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="重 置"></Button>
</LinearLayout>
</LinearLayout>
</LinearLayout>
上面代码是实现 UI 界面,下面会详细介绍:
一、XML 基本格式
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 此时就相当于 HTML 中的空壳 <div><div> -->
<!-- LinearLayout 表示线性布局 -->
</LinearLayout>
二、各个组件的属性
// 组件类型
LinearLayout 是线性布局
TextView 是文字
EditText 是输入框
Button 是按钮
// 组件属性(android)
// LinearLayout
layout_width="match_parent" 长度匹配父组件
layout_height="200dp" 宽度200dp
layout_marginTop="150dp" 距离顶部15dp
background="#ddd" 背景颜色是灰色
orientation="vertical" 子组件方向竖着排
// TextView
text="@string/txt" 文字引用变量txt
textAlignment="center" 文字内容居中
textSize="20dp" 文字大小20dp
// EditText
id="@+id/txt_pwd" 关联绑定
inputType="textPassword" 输入内容是密码类型
// Button
layout_height="wrap_content" 高度是内容的长度
关于 dp 和 px 的区别:
- dp 是虚拟像素,在不同的像素密度的设备上会自动适配
- px 是真实像素,是固定的
- 例如当像素密度为 160,1 dp = 1 px
- 例如当像素密度为 240,1 dp = 1.5 px
三、存储变量的 XML
<resources>
<string name="app_name">Project_1</string>
<string name="txt">用户登录</string>
<string name="user">用户名:</string>
<string name="pwd">密码:</string>
</resources>
四、AndroidManifest.xml
AndroidManifest.xml
是文件是整个应用程序的信息描述文件,每当新建一个 xml
页面时,就要多加一个 activity
组件
比如下面展示我有两个页面(Home
和 MainActivity
):
<?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.INTERNET" />
<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:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android_1"
tools:targetApi="31">
<activity android:name=".Home"></activity>
<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>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
UI 界面写完了,就轮到交互环节了
package com.example.android_1;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.w3c.dom.Text;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView txtUser = findViewById(R.id.txt_user);
TextView txtPwd = findViewById(R.id.txt_pwd);
Button btnLogin = findViewById(R.id.btn_login);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("日志", "登入");
}
});
Button btnReset = findViewById(R.id.btn_reset);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("日志", "重置");
txtUser.setText("");
txtPwd.setText("");
}
});
}
}
最后点击运行即可,这就是真机上最简易的 app
网络请求
采用安卓真机模拟发送网络请求,电脑端模拟 FLASK 服务器的方式
首先安卓发送网络请求(okhttp)需要进行以下配置:
- 在
src/build.gradle
的 dependencies 中添加implementation "com.squareup.okhttp3:okhttp:4.9.1"
- 在
src/main/AndroidManifest.xml
中添加<uses-permission android:name="android.permission.INTERNET" />
- 在
src/main/res/xml
中新建network_security_config.xml
并添加以下内容(仅测试)
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--禁用掉明文流量请求的检查-->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
- 在
src/main/AndroidManifest.xml
中添加android:networkSecurityConfig="@xml/network_security_config"
(仅测试)
下面代码是完整的 MainActivity.java
代码,用于封装和发送网络请求(其他请求方式参考这篇文章)
package com.example.android_1;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class MainActivity extends AppCompatActivity {
// 各个组件类型
private TextView txtUser, txtPwd;
private Button btnLogin, btnReset;
public Context mContext;
// 启动执行的函数,相当于主函数
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
initView();
initListener();
}
// 获取对应组件
private void initView() {
txtUser = findViewById(R.id.txt_user);
txtPwd = findViewById(R.id.txt_pwd);
btnLogin = findViewById(R.id.btn_login);
btnReset = findViewById(R.id.btn_reset);
}
// 初始化按钮
private void initListener() {
// 登录按钮
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LoginForm();
}
});
// 重置按钮
btnReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("日志", "重置");
txtUser.setText("");
txtPwd.setText("");
}
});
}
// 登录按钮执行的函数
private void LoginForm() {
Log.e("日志", "登录");
// 用于存储用户信息
// 效果:{username:"123456", password:"666666"}
TreeMap<String, String> dataMap = new TreeMap<String, String>();
// 获取用户输入的用户名和密码
HashMap<String, TextView> objMap = new HashMap<String, TextView>();
objMap.put("username", txtUser);
objMap.put("password", txtPwd);
for (Map.Entry<String, TextView> entry : objMap.entrySet()) {
String key = entry.getKey();
TextView obj = entry.getValue();
String value = String.valueOf(obj.getText());
dataMap.put(key, value);
}
// 用于校验用户输入信息
// 效果:password666666username123456
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : dataMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key);
sb.append(value);
}
String dataString = sb.toString();
String signString = md5(dataString);
// dataMap = {username:"123456", password:"666666", sign:"5a231fcdb710d73268c4f44283487ba2"}
dataMap.put("sign", signString);
// 使用线程,发送 HTTP 网络请求
new Thread() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient.Builder().build();
// 发送 post 请求
FormBody form = new FormBody.Builder()
.add("user", dataMap.get("username"))
.add("pwd", dataMap.get("password"))
.add("sign", dataMap.get("sign"))
.build();
// 请求网址为测试用本地 FLASK 服务器 IP
Request req = new Request.Builder().url("http://10.10.10.111:5000/auth").post(form).build();
Call call = client.newCall(req);
try {
Response res = call.execute();
ResponseBody body = res.body();
String bodyString = body.string();
// 接收示例:{"status":true, "token":"b96efd24-e323-4efd-8813-659570619cde"}
Log.e("获取相应的内容 -->", bodyString);
} catch (IOException ex) {
Log.e("请求异常 -->", "网络错误");
}
}
}.start();
}
// MD5 加密函数
private String md5(String dataString) {
try {
MessageDigest instance = MessageDigest.getInstance("MD5");
byte[] nameBytes = instance.digest(dataString.getBytes());
// 十六进制展示
StringBuilder sb = new StringBuilder();
for (byte nameByte : nameBytes) {
int val = nameByte & 255; // 负数转换为正数
if (val < 16) {
sb.append("0");
}
sb.append(Integer.toHexString(val));
}
return sb.toString();
} catch (Exception e) {
return null;
}
}
}
接着需要获取本机 IP(局域网),方法如下:
# cmd 直接运行下面命令
python -c "import os, re; print(''.join(re.findall('无线局域网适配器 WLAN:?\n.*\n.*\n.*\n.*IPv4 地址 [\. ]+:(.*)', os.popen('ipconfig').read())[0].split()))"
下面代码是完整的 FLASK 服务器 Python 代码,用于模拟服务器接收网络请求,并返回对应内容
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
# @Author: Xiaotuan
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/auth', methods=['POST'])
def auth():
print(request.form)
# 1. 获取各个参数
user = request.form.get("user")
pwd = request.form.get("pwd")
sign = request.form.get('sign')
# 2. sign 签名的校验(略)
# 3. 根据用户名和密码去数据库校验,并返回对应内容
return jsonify({
'status': True,
'token': "b96efd24-e323-4efd-8813-659570619cde"
})
if __name__ == '__main__':
# 电脑本机 IP
app.run(host='10.10.10.111')
序列化和反序列化
首先简单介绍一下概念:
- 序列化:对象 -> 字符串
- 反序列化:字符串 -> 对象
然后进行配置(添加 Gson
组件):
- 在
src/build.gradle
的 dependencies 中添加implementation 'com.google.code.gson:gson:2.8.6'"
接着会用到反序列化(获取登录状态 + token),并创建新的 HttpResponse.java
类文件专门处理
package com.example.android_1;
class HttpResponse {
public boolean status;
public String token;
}
在 MainActivity.java
文件中添加下面代码:
// 获取登录状态 + token
HttpResponse obj = new Gson().fromJson(bodyString, HttpResponse.class);
保存 XML 文件
在安卓真机上,app 保存的 token
会存储在 XML
文件中(data/data/com.example.android_1
),可使用 adb
来查看,也可以直接在 Android Studio
右侧查看(也只能查看,毕竟没有 root
权限)
使用下面 java
代码可直接处理 XML
文件(三种方式):
- 保存
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("token", "b96efd24-e323-4efd-8813-659570619cde");
editor.commit();
- 删除
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.remove("token");
editor.commit();
- 读取
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
String token = sp.getString("token", "");
最后更新在 MainActivity.java
文件中修改的代码
try {
Response res = call.execute();
ResponseBody body = res.body();
String bodyString = body.string();
// 接收示例:{"status":true, "token":"b96efd24-e323-4efd-8813-659570619cde"}
// 1. 获取登录状态 + token
HttpResponse obj = new Gson().fromJson(bodyString, HttpResponse.class);
// 2. token 保存手机 -> 本地 XML 文件(登录凭证保存到 cookie)
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("token", obj.token);
editor.commit();
// 3. 验证成功,跳转到新页面
Intent in = new Intent(mContext, Home.class);
startActivity(in);
Log.e("获取相应的内容 -->", bodyString);
} catch (IOException ex) {
Log.e("请求异常 -->", "网络错误");
}