0%

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
90
Android 提供 4 种启动模式,可以在 AndroidManifest.xml 的 <activity> 节点里配置 android:launchMode,也可以在启动时加 Intent 的 Flag 控制。

1. standard(标准模式,默认)

特性:
每次调用 startActivity() 都会新建一个 Activity 实例,压入任务栈顶部。

生命周期:
每次都是全新的实例,都会执行 onCreate()。

场景:
绝大多数页面,比如:商品详情页、设置页。

<activity
android:name=".ui.activity.CheckDeviceActivity"
android:launchMode="standard"/>

startActivity(new Intent(this, CheckDeviceActivity.class));
// 每次点都会创建新的 CheckDeviceActivity

2. singleTop(栈顶复用模式)

特性:
如果要启动的 Activity 已经在栈顶,则不会新建实例,而是复用已有实例,并回调 onNewIntent()。

生命周期:
不会走 onCreate(),而是走 onNewIntent()。

场景:
避免重复创建,比如:消息中心、搜索页。

<activity
android:name=".ui.activity.NotificationActivity"
android:launchMode="singleTop"/>

// 如果 NotificationActivity 已经在栈顶,就不会新建,只会回调 onNewIntent()
startActivity(new Intent(this, NotificationActivity.class));

3. singleTask(栈内复用模式)

特性:
系统会查找任务栈中是否已有该 Activity 实例:

如果有 → 复用,并将它上面的所有 Activity 弹出(清空)。

如果没有 → 创建新实例。

生命周期:
复用时会回调 onNewIntent()。

场景:
入口类页面(如 App 首页、主界面),保证唯一性。

<activity
android:name=".ui.activity.MainActivity"
android:launchMode="singleTask"/>

// 无论从哪里启动 MainActivity,栈里只会保留一个 MainActivity 实例
startActivity(new Intent(this, MainActivity.class));

4. singleInstance(单实例模式)

特性:
该 Activity 会独占一个新的任务栈,且全局只有一个实例。

如果别的应用拉起它,也会共用同一个实例。

生命周期:
始终复用唯一实例。

场景:
系统级别的全局页面,比如:来电界面、锁屏界面。

<activity
android:name=".ui.activity.CallActivity"
android:launchMode="singleInstance"/>

// CallActivity 始终全局唯一,独立任务栈
startActivity(new Intent(this, CallActivity.class));


✅ 实际开发建议

一般页面 → 用默认 standard 就行。

避免重复打开的页面(如消息中心) → 用 singleTop。

保证唯一入口(如 MainActivity) → 用 singleTask。

特殊全局页面(如视频通话、锁屏) → 用 singleInstance。

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
public class UploadUtil {

// 修改为你的服务端 IP 和端口
private static final String SERVER_IP = ServerConfig.SERVER_IP;
private static final int SERVER_PORT = ServerConfig.SERVER_PORT;

public static void uploadAllFilesInDirectory(File dir) {
if (dir == null || !dir.exists()) return;

if (dir.isFile()) {
try {
FileSender.sendFileFold(SERVER_IP, SERVER_PORT, dir);
} catch (Exception e) {
e.printStackTrace();
}
} else if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
uploadAllFilesInDirectory(f); // 递归上传
}
}
}
}
}


public class FileSender {
//按照单文件传输到服务器
public static void send(String host, int port, File file) throws Exception {
try (Socket socket = new Socket(host, port);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
FileInputStream fis = new FileInputStream(file)) {

byte[] nameBytes = file.getName().getBytes("UTF-8");
dos.writeInt(nameBytes.length);
dos.write(nameBytes);
dos.writeLong(file.length());

byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) > 0) {
dos.write(buffer, 0, len);
}
dos.flush();
}
}


//根据文件目录上传
public static void sendFileFold(String serverIp, int port, File file) throws Exception {
try (Socket socket = new Socket(serverIp, port);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
FileInputStream fis = new FileInputStream(file)) {

// 获取根目录(一般是 /storage/emulated/0)
String root = "/storage/emulated/0";
String absPath = file.getAbsolutePath();

// 相对路径(如 Download/abc.jpg)
String relativePath = absPath.startsWith(root)
? absPath.substring(root.length() + 1)
: file.getName();

// 发送文件名长度和路径名(UTF-8 编码)
byte[] nameBytes = relativePath.getBytes("UTF-8");
dos.writeInt(nameBytes.length);
dos.write(nameBytes);

// 发送文件大小
dos.writeLong(file.length());

// 发送文件内容
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
dos.write(buffer, 0, len);
}

dos.flush();
System.out.println("文件发送完成: " + relativePath);
}
}

}


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
发布之前,要先把代码上传到github,
1️⃣ 准备 GitHub 仓库

整个项目已经上传到 GitHub,包括 app/ 和 mylibrary/ 模块。

确认 settings.gradle 包含 library:

include ':app', ':mylibrary'


确认 library 的 build.gradle 设置正确:

plugins {
id 'com.android.library'
id 'kotlin-android'
}

android {
namespace 'com.comm.library'
compileSdk 36

defaultConfig {
minSdk 24
consumerProguardFiles "consumer-rules.pro"
}
}

dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
api 'com.github.getActivity:XXPermissions:18.3'
}


注意不要在 library 的 build.gradle 中添加 applicationId,它是 library 而不是 App。

2️⃣ 打 Tag(版本号)

在项目根目录执行命令:

git tag v1.0.0
git push origin v1.0.0


JitPack 会根据 Tag 构建版本。

3️⃣ 在 JitPack 上测试构建

打开 https://jitpack.io

在输入框输入你的仓库地址,例如:

https://github.com/mingname/androidcommonutils


点击 Look up → 选择 Tag(比如 v1.0.0) → 点击 Get it

JitPack 会给你 Gradle 依赖方式,例如:

dependencies {
implementation 'com.github.mingname:androidcommonutils:v1.0.0'
}

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
🔹 1. 新建 Android Library

在 Android Studio 里:

File → New → New Module → Android Library

有两种方式使用这个 library:

直接依赖本地 module

在主项目的 settings.gradle 加入:

include ':permissionlib'
project(':permissionlib').projectDir = new File('../permissionlib')


在 app/build.gradle:

implementation project(":permissionlib")


打包成 .aar 分发

Build → Make Module "permissionlib"

在 permissionlib/build/outputs/aar/ 里会生成 permissionlib-release.aar

把 .aar 给其他项目用,在 libs/ 下放入并在 app/build.gradle 里:

implementation files('libs/permissionlib-release.aar')




Library 用 api 依赖 XXPermissions
dependencies {
api 'com.github.getActivity:XXPermissions:18.3'
}


这种方式 library 内可以用

app 也能直接引用 Permission 常量类

推荐这种方式,如果你的 library 需要 app 直接写权限列表

1
2
3
4
5
6
./gradlew :MyNetworkSDK:assembleRelease   会把library下打包成aar

./gradlew assembleDebug 打 Debug 版本 APK/AAR

./gradlew assembleRelease 打 Release 版本 APK/AAR

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
| 十进制 (Dec) | 二进制 (Bin, 8位) | 十六进制 (Hex) |
| --------- | ------------- | ---------- |
| 0 | 00000000 | 0x00 |
| 1 | 00000001 | 0x01 |
| 2 | 00000010 | 0x02 |
| 3 | 00000011 | 0x03 |
| 4 | 00000100 | 0x04 |
| 5 | 00000101 | 0x05 |
| 6 | 00000110 | 0x06 |
| 7 | 00000111 | 0x07 |
| 8 | 00001000 | 0x08 |
| 9 | 00001001 | 0x09 |
| 10 | 00001010 | 0x0A |
| 11 | 00001011 | 0x0B |
| 12 | 00001100 | 0x0C |
| 13 | 00001101 | 0x0D |
| 14 | 00001110 | 0x0E |
| 15 | 00001111 | 0x0F |
| 16 | 00010000 | 0x10 |
| 17 | 00010001 | 0x11 |
| 18 | 00010010 | 0x12 |
| 19 | 00010011 | 0x13 |
| 20 | 00010100 | 0x14 |
| 32 | 00100000 | 0x20 |
| 64 | 01000000 | 0x40 |
| 128 | 10000000 | 0x80 |
| 255 | 11111111 | 0xFF |

👉 规律记忆:

1 个十六进制位 (0~F) = 4 个二进制位

2 个十六进制位 (00~FF) = 1 个字节 (8 bit)

常用的 0x0A=10, 0x10=16, 0x40=64, 0x80=128, 0xFF=255

十六转十:高位乘以16+低位


1️⃣ 有符号 Byte 的表示

总共有 8 位二进制:

b7 b6 b5 b4 b3 b2 b1 b0


b7 是 符号位(最高位)

0 → 正数

1 → 负数

b6~b0 是数值位(7 位)

范围:-128 ~ 127

最小值:1000 0000 → -128

最大值:0111 1111 → 127

2️⃣ 二进制例子
十进制 二进制(8位) 说明
0 0000 0000 中性值
1 0000 0001 正数
127 0111 1111 最大正数
-1 1111 1111 负数(补码表示)
-128 1000 0000 最小负数

“无符号”是二进制和计算机数据里一个非常重要的概念
1️⃣ 有符号数 vs 无符号数
✅ 有符号整数(Signed)

可以表示 正数、负数和零

例如 8 位(1 个字节):

范围:-128 ~ 127


第一位是 符号位:

0 = 正数

1 = 负数

✅ 无符号整数(Unsigned)

只能表示 非负数(0 或正数)

例如 8 位:

范围:0 ~ 255


所有 8 位都用来表示数值,没有符号位

因此最大值比有符号数大了一倍

2️⃣ 为什么要用无符号?

有些硬件或协议数据永远不会为负,比如:

电压值、温度传感器、PWM占空比

设备计数器、PID 输出值

使用无符号数可以 表示更大的范围,同样字节数存更多信息

3️⃣ 举例
类型 二进制 十进制
有符号 Int8 11111111 -1
无符号 UInt8 11111111 255

同样一个字节,解释方式不同,数值完全不同。

4️⃣ 在 Kotlin 里

Kotlin 没有原生 UInt16/UInt32(早期版本),所以我们用 Int 或 Long + 位运算 来表示无符号数:

val value = ((this[offset].toInt() and 0xFF) or ((this[offset+1].toInt() and 0xFF) shl 8))


and 0xFF 就是把 字节转成无符号

避免 Kotlin Byte 被当作 -128~127 的有符号数

5️⃣ 一句话总结

无符号整数 = 永远 ≥0,没有负数,最大值比同样字节数的有符号数大一倍。


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
在 Kotlin 里,object 定义的类就是 单例。

1️⃣ 基本概念
object MySingleton {
val name = "单例"
fun sayHello() = println("Hello from $name")
}


特点:

全局唯一:整个程序中只有一个实例。

懒加载:第一次访问时才初始化。

线程安全:初始化本身是线程安全的,不需要额外加锁。

调用:

fun main() {
MySingleton.sayHello()
println(MySingleton.name)
}


不能使用 MySingleton() 创建新实例

所有属性和方法都属于同一个实例

2️⃣ 使用场景

工具类(类似 Java 的静态方法集合)

全局管理类(配置、状态管理)

单例模式的对象(服务、控制器等)

3️⃣ 对比 Java 单例

Java 单例常见写法:

public class MySingleton {
private static final MySingleton instance = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() { return instance; }
}


Kotlin 直接写:

object MySingleton { ... }


✅ 语法简单,线程安全,省掉了手动写 static 和锁。

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
Kotlin 顶层函数(Top-level Function)总结
1️⃣ 定义

顶层函数就是直接写在 .kt 文件里的函数,不属于任何类或对象。

可以看作是 Java 工具类的静态方法,自动被编译成 JVM 静态方法。

// 文件名: Utils.kt
fun sayHello(name: String) {
println("Hello, $name")
}

fun add(a: Int, b: Int): Int = a + b


调用方式(Kotlin):sayHello("Alice")

调用方式(Java):UtilsKt.sayHello("Alice")

2️⃣ 顶层属性(静态字段)

顶层 val / var 也是静态的,JVM 编译后是静态字段。

val PI = 3.1415 // 常量
var counter = 0 // 可变静态变量


Kotlin 调用:println(PI) 或 counter += 1

Java 调用:UtilsKt.PI / UtilsKt.getCounter() / UtilsKt.setCounter()

3️⃣ JVM 编译规则
Kotlin 元素 JVM 编译结果
顶层函数 静态方法
顶层属性 静态字段 + getter/setter
文件名 JVM 类名 = 文件名 + Kt

例:Utils.kt → JVM 类 UtilsKt

4️⃣ 与 Java 工具类对比
Java Kotlin
static 方法 顶层函数
static 字段 顶层属性 (val/var)
工具类类名 Kotlin 文件名

Kotlin 不需要 class + static,更简洁。

5️⃣ 使用建议

顶层函数和顶层属性可以放在同一个文件里,集中管理相关工具方法。

按功能划分文件,不要把所有工具方法放在一个文件中(可维护性差)。

顶层函数也可以在 Java 中调用,非常方便。

6️⃣ 对比 companion object

如果必须在类内部写静态成员,用 companion object:

class MyClass {
companion object {
const val VERSION = "1.0"
fun greet() = println("Hello")
}
}


优点:类内部有组织

缺点:比顶层函数多了一层类包裹,调用稍麻烦

7️⃣ 总结一句话

Kotlin 顶层函数 = 静态工具方法 + 静态属性,可以直接写在文件里,不需要 class 或 static。