Android 11 的存储升级和迁移

Android 11 正式发布至今已经有一段时间了,也有越来越多的设备升级或搭载了最新系统。Google 也要求 App 开发者将 targetSdkVersion 升级到 30,并给出了 deadline(今年下半年)。在 TargetSdkVersion 迁移过程中感知最大的改动是存储,api 30 要求必须适配 Scope Storage

Scope Storage 是什么

Scope Storage 最初在 Android 10 引入,但当时并不做强制要求,其目的是限制 App 对外部存储的随意访问。在此之前,App 为了能访问用户媒体数据会申请读写权限,用户授予权限之后 App 能做的其实不止于访问媒体数据,还可以获取外部存储的读写权限,这会导致很多问题,比如 App 可以随意的在外部存储中创建文件/目录,并且 App 在被卸载之后,这些创建在外部存储的数据依然会被保留,还可以随意读取其他 App 创建的数据,会导致很多数据安全问题等等,Android 普通用户对这些问题的抱怨由来已久,而 Scope Storge 就是 Google 给出的解决方案。

其实际是一套存储要求和访问限制的规范,在其规范里:

  • App 读写自己的文件不需要权限
  • App 读取其他 App 的媒体文件需要 READ_EXTERNAL_STORAGE 权限
  • App 只有在用户直接同意的情况下才能写入其他App的媒体文件(文件管理器/系统相册等除外,它们没有这个限制)
  • 不能访问其他 App 的外部存储数据目录

可以发现,正如其字面意思,Scope Storage 的思路就是先把 App 的读写权限限制在自己的范围,然后再看 App 通常有哪些合理的外部访问需求,并将这些需求相关的接口尽可能按最低访问限度开放。

继续阅读


Groovy 和 Jenkins pipeline

公司项目一直使用的 Jenkins 作为 java/Android 的 CI/CD 工具,之前需求比较简单,直接使用 Jenkins 提供的管理后台 UI 操作就能完成。后来有一些新的构建需求,我最初是使用 Python 处理的,近期全部转到了 gradle 脚本 + Jenkins pipeline 的实现。这两者使用的都是 groovy,使用过程中发现,其实 groovy 很多操作比 Python 都要方便。

先介绍一些 groovy 操作便捷和需要注意的地方

继续阅读


Python 爬虫的管理与定时任务

Scrapy 框架让我们很方便就能完成各种规模的爬虫项目,最简单的创建爬虫的方式:

  1. 创建一个项目
1
scrapy startproject tutorial
  1. 接着可以添加一个爬虫
1
scrapy genspider -t crawl cambridge dictionary.cambridge.org/us/dictionary/english-chinese-simplified		
  1. 运行爬虫
1
scrapy crawl cambridge

我们可以按实际的业务需求重复第二步,创建大量不同目的的爬虫,但是如何管理这些爬虫呢?

Scrapy 官方文档推荐使用 Scrapyd 来管理,但实际操作起来稍显复杂,经过一番寻找和比较,发现了 Scrapy-do 这个项目,如果爬虫管理侧重定时任务方面,那 Scrapy-do 是一个更简单易上手的选择。

继续阅读


Apk 反编译和压缩的体积问题

这段时间在做 Android 多渠道可自定义打包的工作,测试过程中发现经过反编译后再重新压缩签名打包后的 apk 文件总是要比原文件更小一些。

而且在部分游戏 apk 中反编译压缩重新出包甚至会导致游戏动画加载黑屏的情况。

继续阅读


Android 构建脚本从 Groovy 迁移到 Kotlin DSL

网上已经有不少关于 Kotlin-DSL 很好的文章,但并未能完全解答我迁移过程中遇到的疑问。通过查阅各类文档和源码,我找到了想要的答案。这篇文章记录了我的迁移过程以及对各类问题追溯到源头的答案。

为什么要迁移?

因为 Groovy 是动态语言,在用作 Android 构建脚本的时候,经常有些问题:

  • 很差的IDE支持(自动提示等)
  • 性能问题
  • 很多错误在build时才报出,而不是编译期
  • 难以debug
  • 重构很麻烦

kotlin 并非动态语言,但却兼具了 Groovy 的灵活性和静态语言的特点,是一种类型安全的 DSL,很大程度上解决了上述的问题。

所以,开始吧!!

继续阅读



Android 蓝牙开发 ᚼᛒ (二)

上一篇对蓝牙协议栈做了一个简要的说明,本篇回到主题,谈谈 Android 中的蓝牙开发。

发展

Android 1.0 开始就支持了蓝牙2.0,开始的时候仅支持 headset profile 和 hands-free profile,1.5(capcake)开始支持了 A2dp,2.0 支持了蓝牙 2.1,并提供了 RFCOMM 相关 API…,到 Android 4.3 终于开始支持低功耗蓝牙,但只支持BLE的中心设备模式,到Android 5.0 才支持外设模式。

虽然一直在加大对蓝牙系统的支持,但在初期的 Android 蓝牙开发中 经常受到诟病的一点是 API 不够稳定,并且还有许多标准通信类型还不支持。其原因主要和使用的蓝牙堆栈实现有关。

在 Android 的早期版本中,整个蓝牙堆栈都是基于 BlueZ 构建的,BlueZ 是 Linux 内核中使用的开源堆栈。这个堆栈非常成熟和稳定。然而在Android环境中,BlueZ 存在一些问题,主要与其附加的 GPL 许可协议有关。为了避免与其他 Apache 许可的 Android 堆栈冲突,BlueZ 需要在一个与特殊守护进程(bluetoothd)交互的单独进程中运行。这些处理带来了大量额外的开销。

为了回避这一切,在 Android 4.2中,Google 用 Bluedroid 取代了 Android 中的 BlueZ,Bluedroid 是博通开发的全新堆栈,采用的是 Apache 开源许可,可以与蓝牙堆栈的其余部分直接兼容。 Bluedroid 是一个全新的堆栈,而 对 BLE 的支持是 Android 4.3 才引入。

继续阅读


Android 蓝牙开发 ᚼᛒ (一)

接触 Android 蓝牙开发已经有一段时间了,Android 官方文档虽然对蓝牙 API 介绍很详尽,但是对蓝牙协议不太了解的开发人员来说还是不足以建立一个系统的认识,当时和硬件组的小伙伴交流的时候这点带来的问题就特别明显。网上的资料少,并且良莠不齐,于是去 SIG 官网找了协议文档(七大卷,真的厚…),查阅之后,厘清了很多概念,大致了解了整个系统轮廓。

接下来会从蓝牙协议和 Android 蓝牙开发这两部分来叙述。

这是第一篇,蓝牙协议。

什么是蓝牙

蓝牙是一种短距离的短波无线通信技术,目前主要分为基础率/增强数据率(BR / EDR)和低功耗(LE)两个版本,前者通常也称为经典版蓝牙

关于蓝牙这个名称的由来,蓝牙的发展历史及各版本的比对等基本信息在蓝牙的维基百科词条上写得很详细,就不再赘述。

蓝牙的架构

蓝牙4.0是一个综合协议规范,它除了提出了新的 LE 规范,还囊括了 BR / EDR 规范,并在实际使用中分为了单模(Single mode)和双模(Dual mode)版本,前者仅支持 LE 规范且不能和蓝牙4.0之前的版本通信,后者同时支持 LE 和 BR / EDR 规范,并且兼容旧版蓝牙。

本文关于蓝牙协议栈的分析均基于蓝牙4.1

下图是根据蓝牙 4.1 核心技术栈及部分应用协议绘制的蓝牙协议栈示意图
蓝牙协议栈
*上图仅供参考,详细架构图参见 SIG 蓝牙协议文档

继续阅读


关于 Java 多态(Polymorphism)

之前看 Thinking in Java 的时候自认为理解了 Java 中的多态性和方法动态绑定,直到看到下面这个栗子:

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
class A {
public String show(D obj) {
return ("A and D");
}

public String show(A obj) {
return ("A and A");
}
}

class B extends A {
public String show(B obj) {
return ("B and B");
}

public String show(A obj) {
return ("B and A");
}
}

class C extends B { }

class D extends B { }

public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();

System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
}

/**
* 1--A and A
* 2--A and A
* 3--A and D
* 4--B and A
* 5--B and A
* 6--A and D
*/

最后的输出中,前三条是毫无疑问的,从第四条开始好像就不太符合我们直觉了,a2的实际类型是B, a2.show(b)调用的不应该是B.show(B)这个方法吗,为什么会调用B.show(A)方法呢?

继续阅读