Broadcast和Receiver的变更

0x81 后台限制变更

从Android Nougat开始,Google开始逐渐收紧后台运行权限,到Oreo几乎禁掉了所有匿名广播。

广播的唤起进程确实非常拖慢设备的运行速度,甚至由于大量App被唤起占用CPU资源会让很多人觉得手机卡顿,所以Google终于开始限制各大App对这些系统资源的滥用。其实从另一方面讲,这些限制的出现也意味着很多功能成了系统软件的专属,用户软件相当于失去了对应的能力,当然Google其实也考虑到这些事情,提出了很多方案来弥补这些不足,也对一些功能放宽限制来保证App在前台可以正常使用。Lolipop引入的JobScheduler以及最新的Jetpack组件WorkManager都是不错的替代方案。

0x82 Nougat的变更

Nougat的变更算是一次试水,主要砍掉了两个广播并削弱了一个广播的能力。

ACTION_NEW_VIDEOACTION_NEW_PICTURE两个广播被完全砍掉,即使你的App的Target级别不是24+,只要运行在Nougat设备上,就无法再接收这两个广播,如果想做类似的功能可以利用JobScheduler对ContentUri创建的相应的Job,待收到相应的Uri,相应的JobService会被系统适时完成你的逻辑。

CONNECTIVITY_ACTION便是被削弱的广播,这个广播非常常用,我们通常用它来处理我们的App在Wifi和Cellar下的行为,静态注册的广播接收器将不再工作:

1
2
3
4
5
6
7
<receiver android:name=".nougat.receiver.ManifestConnectivityReceiver">
<intent-filter>
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
</intent-filter>
</receiver>

如果我们真的想接受网络变化怎么办,Google考虑到这个广播的易用性,保留了动态注册的能力,这意味着只要我们是通过上下文注册的,我们仍然能收到广播及其信息,并且这个广播是粘性的,我们可以依赖它作为当前的网络状态:

1
2
3
val intentFilter = IntentFilter()
intentFilter.addAction(ImplicitAction.CONNECTIVITY_ACTION)
registerReceiver(registerConnectivityReceiver, intentFilter)

当然,实际上Google建议我们用更加细粒度的控制,而不是真的依赖沉重的广播组件,毕竟这个广播已被弱化,并根据路线图以后很可能被直接砍掉,因此我们可以通过向ConnectivityManager注册回调的方式完整我们需求,并且他能做到的不仅仅是Wifi这种无线连接,像是蓝牙网络等等都可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private val connCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network?) {
println("Connectivity Callback Available: ${network?.toString()}")
}

override fun onLost(network: Network?) {
println("Connectivity Callback Lost: ${network?.toString()}")
}

override fun onCapabilitiesChanged(network: Network?, networkCapabilities: NetworkCapabilities?) {
println("Connectivity Callback Changed: ${network?.toString()} / ${networkCapabilities?.toString()}")
}
}
val connManger = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connManger.registerNetworkCallback(
NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(),
connCallback)

当然,如果对实时性不是那么依赖,例如我们只是想在某些情况下帮用户做一些辅助任务,提高易用性,还是建议使用JobScheduler,更加内存友好、电池友好:

1
2
3
4
5
val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val job = JobInfo.Builder(CONNECTIVITY_JOB_ID, ComponentName(this, ConnectivityJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
jobScheduler.schedule(job)

JobService相关不在这里展开,Google文档描述的非常详细,并且有很丰富的样例。

0x83 Oreo的变更

Oreo做了更加激进的变更,甚至大量变更是忽视掉Target级别的,只要运行在Oreo设备上,它就生效,当然这里关注的还是广播的问题。

首先,

  • Apps that are running in the background now have limits on how freely they can access background services.

  • Apps cannot use their manifests to register for most implicit broadcasts (that is, broadcasts that are not targeted specifically at the app).

其中第二条,清单再也不能注册大部分的匿名广播(未明确指定),这是一个相当大的变更,可以看出是紧随Nougat来的。当然他们默认是指Target级别达到26+,但是Oreo放开了用户设置,用户可以强制限制。

对于系统广播,都是匿名广播,基本上可以认为静态注册都不再工作(除了极少数和带指定信息的),我们用自定义的注册在清单中的广播接收器做个实验,一共三种情况——发送匿名Action、发送带PackageName的匿名Action和显式发送:

1
2
3
4
5
6
<receiver android:name=".oreo.receiver.ManifestStaticReceiver"
android:exported="false">
<intent-filter>
<action android:name="action.STATIC"/>
</intent-filter>
</receiver>
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
class ManifestStaticReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val msg = "Manifest Static Received" +
": ${intent?.action} " +
"/ ${intent?.`package`} " +
"/ ${intent?.component}}"
context?.showToast(msg)
println(msg)
}
}

btn_static_send.setOnClickListener {
val intent = Intent()
intent.action = Actions.STATIC
sendBroadcast(intent)
}

btn_static_send_with_pkg.setOnClickListener {
val intent = Intent()
intent.action = Actions.STATIC
intent.setPackage(packageName)
sendBroadcast(intent)
}

btn_static_send_explicit.setOnClickListener {
val intent = Intent()
intent.setClass(this@MainActivity, ManifestStaticReceiver::class.java)
sendBroadcast(intent)
}

上述三个广播发送,只有第二个和第三个有结果:

1
2
Manifest Static Received: action.STATIC / com.fionera.receiverchanges / ComponentInfo{com.fionera.receiverchanges/com.fionera.receiverchanges.oreo.receiver.ManifestStaticReceiver}}
Manifest Static Received: null / null / ComponentInfo{com.fionera.receiverchanges/com.fionera.receiverchanges.oreo.receiver.ManifestStaticReceiver}}

很明显,我们自定义的纯匿名广播在静态注册时也会失效,解决方法只有两个——指定目标和显式指定组件,而动态注册仍可以正常响应匿名广播,但是在我的测试里显式广播是失效的。

当然了,更加鲁棒的解决方案依然是JobScheduler。

由于Oreo砍掉了几乎所有能砍掉的匿名广播,所以Pie相对不这一部分没有大的变更,如今各大厂商商店开始陆续要求Target提升到26+,是该向GooglePlay看齐规范规范了。

Gradle依赖管理踩坑

0x81 诱因

春节快过去两个月了,果然如我之前所说,还是鸽了这么久,主要是年后挺忙的,公司搬家酒仙桥,自己租房折腾了一顿,工作上事情也比较多。再就是花一段时间攒了台电脑,有时间谢谢心得。

有关Gradle的笔记已经好久没写了,上一篇应该是两年前了,随着这两年Gradle 的快速迭代,其实多了很多奇技淫巧来处理一些问题,这里主要聊聊一个老问题,这个问题其实早在两年前Dependency API 变更的时候就会遇到,不是Gradle的问题,是我使用的问题。这个问题就是api/implementation带来的依赖可见性导致的编译不通过。

0x82 依赖管理的变化

从AGP3开始,Android的构建脚本开始支持最新的Gradle依赖管理API,大致分为三个部分(因为画表格麻烦我就不画了):compile|provided|apk(runtime) 分别被api/implementation|compileOnly|runtimeOnly取代,后面的没啥好说的主要是compile,分别用api 实现原本的compile的能力,而implemetation 则控制被依赖包的CompileScope。

举个简单的例子,A < B < C。如果B api C,则A可以调用C的API。如果B impl C,则只有B能用。

那问题是什么呢?假设B有静态方法B#d,B#d中有调用C的API,A调用B#d,如果你是B impl C,那么很遗憾,Lint就会告诉你这个方法是有问题的,其实很好理解,因为通常非静态方法就不会有这个问题。

还有一种情况,假设B extends CB super C没有问题,如果A extends BB impl C,那么很遗憾,只能A super B 不能 A super C,也很好理解,C对A是不可见的,但是这违背了Java的继承和多态,只有B api C是才能A super C,这就是控制Consumer的可见性带来的问题。

知道了原因,解决方法也很容易,一个简单粗暴api完事,但是作为规范遵从最小化原则,我们可以做Wrapper来灵活控制,毕竟面向接口编程是一个可读性非常差但十分灵活的方式:P

macOS Default Setting

0x80 前言

该篇文章主要用于记录macOS下defaults操作的相关指令。

0x81 命令

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
# 通用参数
-g/-globalDomain
-currentHost
-bool YES/NO

# 指针速度读写
defaults read -g com.apple.mouse.scaling
defaults write -g com.apple.mouse.scaling 0.5

# 禁用字体渲染(Mojave)
defaults write -g CGFontRenderingFontSmoothingDisabled -bool NO
defaults read -g CGFontRenderingFontSmoothingDisabled

# 字体渲染级别
defaults write -g AppleFontSmoothing 2
defaults read -g AppleFontSmoothing

# 设置语言首选项
defaults write com.apple.Safari AppleLanguages '(zh-CN)'
defaults read com.apple.Safari

# 开启AppStore调试菜单
defaults write com.apple.appstore ShowDebugMenu -bool true

# No more Finder pop up when iDevice connects
defaults write com.apple.AMPDevicesAgent dontAutomaticallySyncIPods -bool YES

# Disable iDevice management in Finder
defaults write "Apple Global Domain" ignore-devices -bool YES

# Carrier testing(Catalina)
defaults write com.apple.AMPDevicesAgent carrier-testing -bool YES

Git ignore file locally

0x80 新的环境

0x81 Project-Level ignore

Git应该是目前来讲绝大多数开发人员都在使用VCS工具,Git本身提供分布式的仓库管理,每一个人的repo都可以视为一个仓库,通常它们只在互相同步时有local和remote的差别。

在我们的日常开发中,往往有很多文件是不属于仓库本身的,比如编译产生的临时文件和开发者自己的本地配置文件。对于这些文件,我们通常会使用GitRoot目录及子目录下的.gitignore文件来忽略我们不想track且untracked的文件。各大代码托管服务在线生成repo时都会提供对应类型项目的ignore模版文件,用于忽略追踪一些常见的文件或目录。

.gitignore设计的初衷是忽略追踪文件,有点assume-unchanged的意思。一个文件如果被ignore且没有被追踪,那我们无论怎样修改这个文件,Git都会忽略掉它,但是如果我们已经将这个文件添加到Stage(通过--force参数)或已经track了这个文件,那.gitignore将不在对该文件生效。

Read More

Android 无法扫描蓝牙设备踩坑

0x81 从BluetoothLeScanner说起

早在几年前,Google开始在Android 4.3(Api 18)引入BLE支持的时候,使用的是一套支持不算完善的api,通过BluetoothAdapter的startLeScan方法,传入我们需要接受结果的Callback,这套api到目前为止(Api 28)都是可用的,虽然从Android 5.0开始引入了新的api并且将原来的api标记为废弃。通过观察源码我们可以发现原来的startLerScan已经转换成了BleScanner的api,通过改变原本的实现来保证我们的app仍然可以调用原来的方法并按预期工作。

Android 4.3甚至4.4对BLE的支持始终比较弱,默认不能作为外围设备不说,通信的稳定性也存在问题。对于4.3之前的设备,更有厂家自行实现Android平台的蓝牙协议栈以提供BLE支持,但是这就会导致最终app的适配十分麻烦且产生碎片化,就像指纹api一样,Android 5.0之后的api则提供了更全面的支持。

0x82 Android 8.1 Offscreen Pause Scanning

如果你使用BLE api搜索周围所有设备,你会发现即使你使用了新的api,在Android 8.1平台上,当你关闭屏幕LogCat会打印类似“pause scanning, need to be resumed”类似的消息,这是因为在Android 8.1这个例行小更新上,Google对BLE的行为再次做了限制,原本Android O上的后台行为限制继续保留,对于无限制无过滤条件(这里指的就是ScanFilter)的扫描,在你关闭屏幕的时候会立即停止。StackOverflow上也有不少人问及这个问题,解决办法就是根据文档对Scanner的描述,在扫描条件中添加ScanFilter,哪怕添加空的,都能规避这一问题。但是有一点要注意,这种行为属于后台行为,应该处理好使用的方式,后台长时间无休止无限制高功率的扫描本身也违背设计规范,并且也不再算是低功耗蓝牙。

Read More

利用iMazing修改iOS应用数据

0x81 iMazing

iMazing是一个第三方iOS设备管理工具,它的功能足够强大,在遵循设备通信协议的前提下提供了足够多的功能,比如单应用数据备份、同步设备数据、访问wrapped文件系统等等。利用iMazing的数据备份功能,我们可以手动修改备份后的数据,然后将修改后的数据还原到手机,从而达到某种目的,比如我们需要修改某些小游戏的数据。

0x82 备份应用数据

iOS设备如果没有越狱,是没有办法直接访问设备的文件系统,更不必说是直接修改应用数据。当然,没有拿到root权限的Android也是无法做到的,Android的data分区也是权限分明并且较新的版本上都是采用加密数据存储的方式,我们只能想一些曲线救国的方式。

iOS11.3的越狱刚出没多久,并且最近的支持直到11.4 beta1,如果越狱过的设备重启还会丢失越狱状态,需要重新操作。为了测试新iOS的特性,我早早讲手头的小8升级到了iOS12,截至文章时间最新测试版是12.0 beta9。iOS12现在想要越狱是不可能的,因此只能找找其他方法,比如先备份再修改然后还原。

要使用这种方式完成我们的需求,我们要做的第一件是就是对应用数据进行备份。Apple钦定的备份工具是iTunes,但我相信每一个是用过它的人都想吐槽,尤其是老版本,所以它于我的功能也就是同步个自定义铃声,而对于它的备份功能,确实它可以进行备份但是备份的数据是整个手机的使用数据,至少我没有发现合适的方法达到我的要求。于是iMazing出现了,iMazing颜值很高并且提供macOS客户端,简直不能再贴心了,最新版本支持iOS12,正好符合我们的要求。(PS: iMazing提供付费服务,可以完成更多的功能,当然我不太需要,至少免费版目前对我而言够用,当然这里指的是备份还原)

iMazing Home

左边Panel上方是我当前连接的手机,下方是之前的备份,这个old backup是连接macOS时itunes备份的,为了方便使用,我又用iMazing备份了一遍,有别于iTunes的备份目录~/Library/Application Support/MobileSync/Backup,iMazing备份在~/Library/Application Support/iMazing/Backups这个目录,弄这么一个备份有个好处,就是备份应用数据时可以选择从旧的备份中导出。以凉屋的Soul Knight为例,我们选择设备的Manage Apps,右键选择Backup App Data

Backup Data

备份导出后而我们便开始修改。

Read More

在macOS Mojave上使用VirtualBox

0x81 Mojave黑名单

Mojave对于kext(内和扩展)的加载有黑名单机制,该黑名单记录在一个kext文件中/System/Library/Extensions/AppleKextExcludeList.kext/Contents/Info.plist中,在该plist文件中有一段记录了阻止系统加载的配置:

1
2
3
4
5
<key>OSKextExcludeList</key>
<dict>
<key>org.virtualbox.kext.VBoxDrv</key>
<string>LT 5.2.14</string>
</dict>

可以看出在最新版本的Mojave中,系统阻止了低于5.2.14版本的vboxdrv.kext的加载,而这段配置在第一个Mojave的beta版本中是LT 5.3,意味着VirtualBox低于5.3版本的都不能正常使用,而当时VirtualBox的最新版本是5.2.12。在DB发生问题后不久,Oracle放出了5.2.13/14的测试版解决了导致kernel panic的问题,但是系统仍然认为VirtualBox不兼容当前操作系统,所以我们需要做一些修改以让Mojave允许加载。

0x82 修改VirtualBox

修改的方式无非两种——修改系统Kext、修改VirtualBox。本着vanilla system的原则,我们不去修改系统文件,因为修改后在系统升级后可能会被覆盖,而软件我们则可以更灵活的控制。VBox的内核扩展有四个:

四个扩展

其中VboxDrv.kext是能否启动虚拟机实例的核心,我们通过sed命令修改4个kext的plist:

1
2
3
4
5
6
7
8
9
sudo sed -i '' 's/5\.2/5\.3/g' '/Library/Application Support/VirtualBox/VBoxDrv.kext/Contents/Info.plist'
sudo sed -i '' 's/5\.2/5\.3/g' '/Library/Application Support/VirtualBox/VBoxNetAdp.kext/Contents/Info.plist'
sudo sed -i '' 's/5\.2/5\.3/g' '/Library/Application Support/VirtualBox/VBoxNetFit.kext/Contents/Info.plist'
sudo sed -i '' 's/5\.2/5\.3/g' '/Library/Application Support/VirtualBox/VBoxUSB.kext/Contents/Info.plist'

sudo kextload '/Library/Application Support/VirtualBox/VBoxDrv.kext'
sudo kextload -d '/Library/Application Support/VirtualBox/VBoxNetAdp.kext'
sudo kextload -d '/Library/Application Support/VirtualBox/VBoxNetFit.kext'
sudo kextload -d '/Library/Application Support/VirtualBox/VBoxUSB.kext'

命令很简单,我们把4个kext的版本从5.2替换为5.3,然后手动load这几个扩展文件,这样虚拟机实例就可以启动了。

如果你真的遭遇了VirtualBox不能使用这种情况,你还会发现LaunchPad里的VirtualBox有禁行符号,我们是无法启动VirtualBox Client的,原因很明显,macOS打算杜绝你的相关操作。如果你想启动,我们可以通过/Applications/VirtualBox.app/Contents/MacOS/VirtualBox直接启动VirtualBox的binary文件,他会跳过app文件的检查直接执行。当然还有一个一劳永逸的方法,像修改kext一样修改app的Info.plist:

1
sudo sed -i '' 's/5\.2/5\.3/g' '/Applications/VirtualBox.app/Contents/Info.plist'

这样操作之后,你会发现原本的禁行图标不见了,我们可以像往常一样使用VirtualBox。当然,如文章最开始所显示的那样,新测试版本的Mojave已经将版本限制到5.2.14,也就是说你只要安装最新的VirtualBox(截至文时最新版本5.2.16),并且使用比较新的Mojave就不会有这个问题。

macOS误删CoreType

0x81 事故背景——完整卸载Xcode

自High Sierra升级到Mojave后,Xcode也相对应的推出了beta版,一方面因为我不是依赖Xcode的开发者,另一方面Xcode占用的空间是真的大。

基于以上原因我决定彻底删除Xcode9,只使用Xcode10beta。

为了给予Xcode10一个相对干净的工作环境,我分别删除了~/Library/Developer/Library/Developer以及/Application/Xcode.app/下的内容,其中第一个目录是用户使用Xcode产生的辅助文件,第二个目录主要是macOS SDK和CLTool所以我们只删除SDK,第三个是Xcode的本体,里面包含framework和toolchain。删除完后我们使用sudo xcode-select -s %path%设置我们要用的Xcode路径,此时运行Xcode10将会提示安装相应的组件,之后便可以正常使用。

0x82 bom文件的内容要仔细判断

正常来讲,经过上述操作,我们应该可以完成Xcode版本的切换,但是事与愿违,在清理Xcode9之前打开了Xcode10并安装了相关组件,我在删除完相关文件后后(其实主要是SDK等相关文件被删除了),导致了Xcode的闪退,猜测macOS记录了Xcode安装的组件,被我直接删除的文件Xcode是不知道的,而我之前执行了组件安装操作,所以Xcode也没有再次弹出安装组件的弹窗,最尴尬的是重新下载Xcode是没有用的。那解决这个问题的办法只有一个了,就是从头模拟一遍Xcode要正常运行需要进行的操作。

依然是那一个目的,为了给予Xcode10一个相对干净的工作环境,我决定去Receipts下找到经过pkginstaller安装的相关Xcode文件并删除,然后手动安装Xcode10里提供的pkg文件,bom文件里的内容非常多,但是主要集中在/Library/Developer/System/Library/PrivateFrameworks下。

Read More

macOS使用seedutil选择不同的Beta版本

0x81 获取macOS的Beta更新

之前我写过一片获取macOS Developer Beta更新的笔记,获取macOS的DeveloperBeta更新简单剖析了seedutil是如何控制更新的。

出于一些特殊的需求,我又深入研究一下为什么seedutil的命令执行后会起到这种效果,最终发现根源还在Seeding.framework上。

0x82 Seeding.framenwork

如之前说的,macOS内置了Seeding.framework,它提供了seedutil工具来管理beta更新的接收,seedutil的位置:/System/Library/PrivateFrameworks/Seeding.framework/Versions/A/Resources/seedutil,Apple并没有开放链接在/usr/bin下并且该工具执行需要root权限,我们可以使用sudo来使用这个工具。

我们再看一次seed状态:

1
2
3
4
5
6
7
8
9
⇒  sudo /System/Library/PrivateFrameworks/Seeding.framework/Versions/A/Resources/seedutil current
Password:
Currently enrolled in: PublicSeed

Program: 3
Build is seed: YES
CatalogURL: https://swscan.apple.com/content/catalogs/others/index-10.14beta-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz
NSShowFeedbackMenu: YES
DisableSeedOptOut: NO

仔细看其实CatalogURL与之前10.13的时候不一样了,没错,这是Mojave配置的地址,其实更新获取的参照就是这个链接,用浏览器打开链接我们会发现它的内容和plist差不多,定义了需要增量更新的标识。

Read More

KabyLake DVMT Fix

0x81 Clover

Hackintosh,这应该是这个系列的第二篇,第一篇参考利用Hotpatch禁用DGPU主要记录了如何利用Clover禁用部分笔记本未提供BIOS选项的独显。

Clover,一个强大的引导工具,基于rEFInd魔改而来,提供强大的驱动注入和二进制patch的功能,@RehabMan等大牛的参与更是为Clover带来更加强大的诸如AutoMerge等功能,让Clover可以为黑苹果工作的更好。

0x82 DVMT

DVMT,全称Dynamic video memory technology,意为动态显存技术,这个显存还不是传统意义上我们在使用显卡的显存,它更像一种预申请用于做缓冲区初始化的存储空间。对于大部分PC厂商来说,BIOS通常会提供一个用于设置大小的选项,而对于很多笔记本来说,BIOS本身提供的选项设置就很少,更别说提供DVMT的设置。

在比较新的硬件平台上(Broadwell+),比如我现在使用的KabyLake,也就是7代英特尔CPU,在安装macOS时如果不进行相应的patch,就会导致对应的Framerbuffer程序crash从而导致kernel panic,这是个很致命的问题,因为这个过程发生在你刚引导家在进入系统的时候。究其原因,是因为macOS对于新硬件平台申请了至少64m的prealloc空间,而大部分笔记本设备厂商出厂都设置在32m,当系统想要申请比其大的空间时必然失败,就跟我们平时编程遇到的allocation memory failed是类似的情况,知道问题就知道对应的解决方案了。

Read More