实际上 1900 年不是闰年,没有 2 月 29 日,所以很明显 这是 Excel 的一个 Bug。
我之所以会留意到这个,是因为最近在做一个绩效核对的小工具,需要用 Python 读取和处理销售交上来的 Excel。
销售交上来的东西总是稀奇古怪,比如有一列是要填日期,交上来的表格里,有的读出来是日期类型,有的读出来是字符串类型,这都还好说,日期类型直接用,字符串按格式解析成日期,就好了。但这天发现有个销售交上来的表格里,这一列读出来是数字类型。
比如 2024-02-01
,读出来对应数字 45323
。
怎么将这个数字转换成日期呢?
首先搜索了微软的官方文档,找到关于 Date systems in Excel 的说明:
Excel supports two date systems, the 1900 date system and the 1904 date system. Each date system uses a unique starting date from which all other workbook dates are calculated. All versions of Excel for Windows calculate dates based on the 1900 date system. Excel 2008 for Mac and earlier Excel for Mac versions calculate dates based on the 1904 date system. Excel 2016 for Mac and Excel for Mac 2011 use the 1900 date system, which guarantees date compatibility with Excel for Windows. … In the 1900 date system, dates are calculated by using January 1, 1900, as a starting point. When you enter a date, it is converted into a serial number that represents the number of days elapsed since January 1, 1900.
就是说,除了 Excel for Mac 的早期版本外,都是默认采用 1900 date system,以 1900-01-01
作为起点(第 1 天)。
于是根据这个信息写一个函数来将数字转换成日期,但是 翻车了……
from datetime import datetime, timedelta
def int_to_date(s):
date_zero = datetime(1900, 1, 1)
delta = timedelta(days = s - 1)
return date_zero + delta
print(int_to_date(45323))
# 输出 2024-02-02 00:00:00
Excel 表格里是 2024-02-01
,读出来是 45323
,咋按 1900-01-01
作为第一天,反算出来 45323
却是 2024-02-02
了呢?
于是一番搜索,先是找到了 OpenOffice 论坛里的讨论 Why the base date is 1899-12-30 instead of 1899-12-31?,里面有如下信息:
The earliest date Excel handles sensibly is 1900-01-01, which is “day 1” (not zero). This makes “Excel epoch” (day zero) 1899-12-31. However, Excel inherited an error from previous spreadsheet apps which assumed that 1900 was a leap year. The nonexistent 29th of February 1900 is counted in Excel time spans, just like it used to be in Lotus 123 and (IIRC) SuperCalc, and probably in most other relevant spreadsheet apps.
有意思了……于是继续在维基百科的 Microsoft Excel 词条上找到了佐证信息:
Excel的时间系统中,会认为1900年2月29日是有效日期,也就是1900年为闰年,但实际上并不是。这是源于模仿早期竞品Lotus 1-2-3上的缺陷而引入的特性,由于Lotus 1-2-3的时间纪元以1900年起始,之后的时间为差值累加,导致其时间体系一开始就认为1900年是闰年,而Excel为了兼容Lotus 1-2-3的文件格式,也保留了这个缺陷作为特性而不进行修复,即使至今最新版本已不需要兼容Lotus 1-2-3。
里面还给出了微软官方的相关解释链接:Excel incorrectly assumes that the year 1900 is a leap year,并且讲述了 Excel 的发展历史,挺有趣的,可以一读。
至此,就破案了。
我们上面提供的数字到日期的换算的方案,做个小修正就能使用了:
from datetime import datetime, timedelta
def int_to_date(s):
date_zero = datetime(1899, 12, 30)
delta = timedelta(days = s)
return date_zero + delta
print(int_to_date(45323))
# 输出 2024-02-01 00:00:00
当然这个程序并不完美,比如计算 60 以内的数字,算出来的就与 Excel 上显示的日期不一致,但 who cares……毕竟 Excel 上有 1900-02-29
这种你永远不会用到的日期 :laughing:
我的公众号里前几天发的一篇文章小火了一把,阅读量到了 5000+(看官您别笑,对于我这种没什么流量的号,这已经是顶流了)。
想着看看我的号里哪些内容最受欢迎,于是翻了一下历史群发文章的数据统计,阅读量最高的是这两篇:
都是关于空投和钱的,而且最近也陆续有一些网友加我微信,咨询如何能获得类似空投的领取资格——和家人交流了一下,猜想可能是因为经济下行,大家关心的都是如何搞钱存钱。
我算是比较幸运的,这两次空投都有领取资格,在不需要额外付出什么成本的前提下,合计入账了 RMB 5000+,在如今挣点钱并不容易的大环境下,还是很香的。
我猜想后续这样的空投应该也还会出现。我就趁机在此,就大家普遍关心的话题,把我的理解小结一下,供大家参考。
首先看一下作为一名 GitHub 用户,这两次空投领取资格 门槛 分别是怎么样的。
2024 年初的 Starknet 空投:
你在 2023-11-15 前,向整个 GitHub 上 star 数 top 5000 之一的项目至少贡献过 3 次 commit;
至少有一次 commit 发生在 2018 年及以后。
2020 年初的 Namebase 空投:
在 2019-02-04 这一天所在的那一周,你的 GitHub 账号有 15 个以上的 followers;
保留有当时的 SSH / PGP 私钥。
从中我提取到的关键词是 贡献 和 影响力。
这些机构空投给 GitHub 用户,我估计一方面是回馈开发者,特别是参与构建与它们的项目相关的基础设施和技术项目的开发者,鼓励创新,另一方面也是借此扩大它们项目的影响力,吸引更多的人关注和参与进来。
虽然说不准未来的空投会采取什么样的角度来制定领取资格门槛,但我们从现在开始着手「埋伏笔」肯定是必要的。
接下来是对在 GitHub 活动的建议,其实就是三板斧——从 GitHub 学、在 GitHub 练、向 GitHub 作贡献。
一、从 GitHub 学:
二、在 GitHub 练:
三、向 GitHub 作贡献:
看到这里,可能有的朋友会问,「如果我都有这个能力和影响力了,还在乎空投这仨瓜俩枣?」
是的,你说得对,如果你依上面的建议提升了自己的能力,构建了自己的项目和影响力,给开源社区贡献了自己的力量,你所获得的有形和无形的收益可能远超自己的想象。
面对新的空投,你将有选择的自由。
]]>网页端实现动态搜索选择框,要求:
一开始根据印象里常见的搜索选择框的样式,一直在探索如何基于 <select>
来实现。Layui 的搜索选择框并没有暴露监听输入内容的事件接口,在网上找到了两个思路,但实现得都不够完美。
一是参考 https://www.cnblogs.com/zqifa/p/layui-select-input-1.html,在 <select>
上覆盖一个 <input>
,监听 <input>
的输入内容然后触发模糊搜索,进而触发更新 <select>
的选项列表。可以基本达成需要的效果,有一个问题是选择列表展示后,必须选择一项才能关闭选项列表,而期望是点击空白区域选项列表自动关闭。
二是参考 https://gitee.com/layui/layui/issues/I6N5MZ,监听经过 Layui 渲染 <select>
后生成的 <input>
元素的事件,进而触发选项列表的刷新。这个方案的思路是挺好的,但是同样有一些小问题,比如下拉选项的展示/隐藏、输入焦点、输入内容保持等,都需要自己一一去干预。
这时在 Layui 的仓库找到 这个 Issue,贤心大大这样回应网友「能不能在选择框上加上可输入可下拉可搜索」的提问:
select 组件的定位就是只能赋值选项列表中的值,包括搜索,也只是从选项中匹配。若要支持自定义输入的值,可以借助 input + dropdown 组件来自定义实现哦。
受此启发,我又思考了一下需求里的「搜索」:
看了下 Layui 文档后发现 dropdown 有专门的 reloadData 的 API,经尝试后最终选择了基于 Layui 的 dropdown 组件来实现。
效果如下:
示例代码如下:
mockData
实现应按需替换成 ajax 请求,成功拿到数据之后再 reloadData
;data-id
,在输入监听事件里删除该属性值。<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Demo</title>
<link href="https://unpkg.com/layui@2.9.6/dist/css/layui.css" rel="stylesheet">
</head>
<body>
<div class="layui-inline layui-padding-5">
<input name="" placeholder="请搜索或选择" class="layui-input" id="ID-dropdown-demo">
</div>
<script src="https://unpkg.com/layui@2.9.6/dist/layui.js"></script>
<script>
layui.use(function(){
var dropdown = layui.dropdown;
var $ = layui.$;
var inst = dropdown.render({
elem: '#ID-dropdown-demo',
data: [],
click: function(obj){
this.elem.val(obj.title);
this.elem.attr('data-id', obj.id)
}
});
$(inst.config.elem).on('input propertychange', function() {
var elem = $(this);
var value = elem.val().trim();
elem.removeAttr('data-id');
var dataNew = mockData(value);
dropdown.reloadData(inst.config.id, {
data: dataNew
})
});
$(inst.config.elem).on('blur', function() {
var elem = $(this);
var dataId = elem.attr('data-id');
if (!dataId) {
elem.val('');
}
});
function mockData(value) {
return [
{id: 1, title: value + '1'},
{id: 2, title: value + '2'}
];
}
});
</script>
</body>
</html>
冷静地想清楚自己的需求和场景,有助于更快找到合适的组件和方案。
]]>Starknet 基金会启动了第一轮 Starknet 供应计划,将向近 130 万个地址分发超过 7 亿个 Starknet 代币(STRK),其中有 2.1% 分发给开源开发者。STRK 可以理解为一种数字货币,领取到后可以通过交易转换成现金。
可以通过 https://provisions.starknet.io 网站查询是否有资格领取 STRK。
Update 2024/02/29 ref https://www.guozaoke.com/t/107113#reply7
符合条件的 GitHub 用户可以在下面两个文件之一搜到自己的 ID:
如果发现有资格,可以继续阅读;如果没有资格,就不用浪费时间继续看了。另外,可以推荐给你在 GitHub 活跃的朋友试试。
往下翻,找到 Eligibility check only 链接,点击打开:
切换到 GitHub,输入 GitHub 用户名,点击 Go:
如果有资格,会看到如下界面:
点 See your allocation,会看到能领取的 STRK 数量:
在上面领取资格查询的最后一步点击 Disconnect,会回到第 2 步界面,点击 Claim STRK;
勾选,Confirm:
选择钱包,我这里用的 Argent X,根据提示下载安装浏览器插件,生成一个钱包地址:
选择 GitHub,Sign in:
按提示操作,看到如下界面就是领取成功了:
注册一个数字货币交易所账户,建议选择头部交易所,如币安、欧易,我使用的是 欧易(OKX);
在 OKX 的资金账户里 充币-选择 STRK-生成充币地址,选择将资金充入交易账户,复制充币地址:
打开之前安装的 Argent X 钱包浏览器插件,点击 Send,输入充币地址,填入 STRK 数量(可以点击 Max 选择最大转出数量),点击 Review send:
稍等一小会,刷新 OKX 应该就能看到 STRK 充进来了,在交易菜单里找到 币币,搜索 STRK/USDT,点击后输入数量,市价卖出:
资金划转,将 USDT 从 交易账户 转到 资金账户;
买币-快捷买币-出售,USDT to CNY,全部出售,支付宝收款,支付宝到账后确认并放币即可,我最终到账 1533.8 元人民币。
天上不会掉馅饼,但是定向投放的合法福利,我们要接住!
类似的空投,2020 年初也经历过一次,当时也有记录:GitHub 用户专属福利,实际到账 3K+,Namebase Airdrop,当时是将 HNS 转换到 BTC 然后提现的,本次操作时看了下,BTC 的价格相对当时翻了约五倍,所以……将数字货币留着不提现,等增值也是一种思路。
在 Starknet 的网站上,有提到空投给 GitHub 用户的原因:
In addition, STRK will be distributed to those who helped to develop the larger ecosystem of open-source software and infrastructure, as their work has become a public good and contributed to the emergence of a more open and inclusive web.
所以如果你符合领取资格,你必然也曾为构建有影响力的开源软件和基础设施贡献过力量,这是你应得的!
本次仍然使用与之前相同的方案,具体方案及操作过程可以参考 DIY|Filco 圣手二代机械键盘单模改三模,以及里面列举的参考链接,在此不展开,重点小结一下改装过程及使用过程中的一些新的体会。
改装过程因为有了改装上一把的经验,本次更加熟练和顺利,但也得到了两点新经验:
施工图:
改装完到现在也使用了一个多月了,结合我自己的体验以及网友在上一篇文章的留言,有个痛点是续航。
续航应该是取决于电池容量、使用强度、键盘本身和改装模块的耗电情况等,与厂家量产的原生三模还是有非常明显的差距的。比如我手上的两把:
有点电量焦虑。
所以,现在如果有人问我要不要把手上的键盘自己有线改无线,我的建议是,如果能找到手感合适价钱合适的,直接买一把新的吧 :-P
]]>在应用的 pom.xml 文件里,做如下修改:
<dependencies>
<!-- 添加以下依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 添加以下这一行 -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
在 application-dev.properties 文件里添加如下内容:
# freemarker hot reload
spring.freemarker.cache=false
spring.freemarker.settings.template_update_delay=0
禁用 FreeMarker 缓存,有更改后即时更新。
修改 IDEA 配置,开启自动编译:
编译应用运行时的 Run/Debug Configurations:
将 On ‘Update’ action: Update classes and resources 和 On frame deactivation: Update classes and resources 配置打开。
关于 spring-boot-devtools 的相关用途与说明,可以参考 Spring 官方文档:https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/using.html#using.devtools,可以看到,如果想要在开发过程中修改 Java 代码后免于手动重启,也可以借助于 spring-boot-devtools 的相关配置。
参考链接:
]]>按照阿里云的操作指南 https://help.aliyun.com/zh/icp-filing/fill-in-app-feature-information 进行操作时,在公钥与签名 SHA1 值获取这一步遇到了问题:我们证书的类型与指南中显示的不同,是 Distribution Managed 类型的,苹果开发者网站上不提供下载,自然也就无法直接拿到公钥和 SHA-1 指纹了。
到了这个时间点,这类问题我当然不会是第一个遇到和解决的,经过一番搜索,找到了可行的参考方法:https://blog.csdn.net/weixin_50340188/article/details/133023592,这里将完整的操作步骤也做个记录。
实测可行,已顺利通过审核。
我现在的键盘是一把有线单模的 Filco 圣手二代 87 键青轴,乃数年前离开帝都时好友所赠,一直用到现在,手感与品质都很好,虽说现在各种国产品牌和轴体的机械键盘层出不穷,价钱不贵评价也不错,但我还真舍不得换。所以对我来说最好的选择就是将其改成蓝牙无线键盘。
纪念下它原本的样子:
上网搜索了一下,发现有卖家出售通用的有线键盘改有线/蓝牙/无线三模的模块,按卖家的建议先拆开键盘,确定了里面有足够的改造空间后,下单购买了模块和电池。
拆得七零八落:
模块到了以后,改装过程主要参考了下面的两个视频,比较简单,在此不赘述:
用到的工具:
加装内部模块后:
装配完成以后——桌面也更加简洁清爽了:
先后拆坏过手表、电脑的手残党在操作过程中总结的心得:
整个改装的原料、工具,加上花费的时间,成本也可以购买一把不错的新键盘了,但意义不可同日而语,既保留了原来的手感和纪念意义,又增加了无线的便利性,还是很值得的。
]]>能够很方便地使用可变参数的方式输出日志;
日志能够根据级别输出到控制台和文件;
能够按照日期和文件大小进行日志文件的切割,滚动保存指定天数的日志,自动清理旧日志。
基于这个需求,我搜了一下「Android 日志框架」,大多网友推荐的是 logger、timber、xLog 等等,看着也不错。不过出于几年后端开发的经验和习惯,我进一步了解,发现熟悉的 log4j 和 logback 在 Android 上也有人做过适配,所以最终决定使用 slf4j + logback,以在前后端开发中取得一致的体验。
做过 Java 后端开发的同学,对于 slf4j + logback 的组合一般不陌生,而 Android 开发的同学则可能不一定听过它们。所以,本文将从零开始,记录如何在 Android APP 中集成 slf4j + logback 作为日志框架,并使用 Lombok 注解生成日志对象。
logback-android 项目地址:https://github.com/tony19/logback-android
一、在项目/模块的 build.gradle 文件中添加依赖:
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.7'
implementation 'com.github.tony19:logback-android:3.0.0'
}
如果是单模块项目,可以直接在 app/build.gradle 文件中添加,如果是多模块项目,可以在一个公共模块的 build.gradle 文件中添加,记得将 slf4j-api 的 implementation
改为 api
才可被其它模块引用。
二、创建日志配置文件 app/src/main/assets/logback.xml
:
<configuration debug="false"
xmlns="https://tony19.github.io/logback-android/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd"
>
<property name="LOG_DIR" value="${EXT_DIR:-${DATA_DIR}}/test/log"/>
<appender name="logcat" class="ch.qos.logback.classic.android.LogcatAppender">
<tagEncoder>
<pattern>%logger{12}</pattern>
</tagEncoder>
<encoder>
<pattern>[%-20thread] %msg</pattern>
</encoder>
</appender>
<appender name="local_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/test.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/test.%d.log</fileNamePattern>
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<root level="DEBUG">
<appender-ref ref="logcat" />
</root>
<root level="INFO">
<appender-ref ref="local_file" />
</root>
</configuration>
以上配置表示 DEBUG 及以上级别的日志输出到控制台,INFO 及以上级别的日志输出到文件,文件按照日期切割,最多保留 15 天的日志。
大家可以按需配置,比如还可以限定单个文件大小、自定义日志输出的格式等等。
在项目的 Wiki 里提到有一点是 Android 开发者比较关注的,就是日志有保存路径,既可以指定绝对路径,也可以用变量,比如:
${DATA_DIR}
表示 Context.getFilesDir()
;${EXT_DIR}
表示 Context.getExternalFilesDir(null)
;${EXT_DIR:-${DATA_DIR}}
表示当 EXT_DIR
可用时使用 EXT_DIR
,否则使用 DATA_DIR
;${PACKAGE_NAME}
表示包名;${VERSION_NAME}
表示版本名;${VERSION_CODE}
表示版本号。三、可以开始使用 slf4j 的 API 进行日志打印了:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 声明 logger
Logger log = LoggerFactory.getLogger(MainActivity.class);
// 打印日志
log.info("hello world");
log.info("number {}, boolean {}, string {}, object {}", 1, true, "string", new Object());
运行 APP,可以看到日志输出到 logcat 和对应位置的文件。
当对配置有疑问,需要调试时,可以将上面配置文件里的 debug="false"
改为 debug="true"
,这样 logback 就会输出详细的信息,方便我们定位问题。
在上一部分的第 3 步,在每一个需要使用 logger 的类里,都需要手动去声明 logger,如 Logger log = LoggerFactory.getLogger(MainActivity.class);
,不算方便。
这里我们可以使用 Lombok 注解来简化这一步骤,自动生成 logger 对象。
Lombok 官方提供了 Android 平台的集成说明:https://projectlombok.org/setup/android
基于 Android Studio 环境,要做的其实就两步。
一、安装 Lombok 插件;
Settings -> Plugins -> 搜索 Lombok -> 安装
注:Android Studio 版本 2020.3.1 - 2022.3.1,JetBrains 官方插件市场无法搜索到兼容版本的 Lombok 插件,可以参考 https://gitee.com/sgpublic/lombok-plugin-repository 解决。
二、在需要使用的模块的 build.gradle 文件里添加如下内容:
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
}
然后,就可以使用 @Slf4j
注解来自动生成 logger 对象了,现在的使用姿势简化成了这样:
@Slf4j
public class Test {
public void test() {
log.info("hello world");
}
}
好了以上就是在 Android 里集成 slf4j + logback 的记录了,至此我「统一」了 Java 后端和 Android 客户端打印日志的用法,在避免多项目维护造成「精神分裂」的路上前进了一小步。
本文所列代码示例已上传至 GitHub,地址:https://github.com/mzlogin/AndroidPractices/tree/master/android-studio/LogbackDemo
以上步骤供有类似需求的同学参考,同时强烈建议以官方文档为主。如果有更好的方案,欢迎留言讨论交流。
应用里有个自升级的功能,下载完 apk 后,通过 FileProvider 提供 Uri 进行安装。我修改了文件下载路径后,功能失效了,报错如下:
java.lang.IllegalArgumentException: Failed to find configured root that contains /data/user/0/org.mazhuang.test/cache/download/xxx.apk
at android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:738)
at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:417)
对应的 provider 的声明是:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
provider_paths 内容:
<?xml version="1.0" encoding="utf-8"?>
<paths >
<cache-path name="internal_cache_download" path="download/" />
</paths>
对照 FileProvider 官方文档:https://developer.android.com/reference/android/support/v4/content/FileProvider.html ,我再三确认了配置本身没有问题。
然后在报错堆栈的 android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile
方法处下断点调试:
@Override
public Uri getUriForFile(File file) {
// some code here
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// some code here
}
发现 SimplePathStrategy 的 mRoots 里确实没有我配置的路径。而 SimplePathStrategy 唯一的构造方法的参数是 authority,该实例的 authority 确实是 ${applicationId}.provider
无误……那么,合理猜测,是有同名的 FileProvider,这里用到的是另一个 FileProvider 的 mRoots。
为了验证该猜测,我从两方面做确认:
查看合并后的 AndroidManifest.xml 文件,是否有其它 FileProvider 的 authorities 也是 ${applicationId}.provider
?
阅读 Android Frameworks 里的相关源码,确认解析 provider 配置、取 FileProvider 实例的逻辑。
现在 Android Studio 已经提供了非常方便的查看合并后的 AndroidManifest.xml 的功能,打开 app 项目的 AndroidMenifest.xml 文件,在编辑器底部有个 Merged Manifest 选项卡,点击即可查看。
可以看到,确实有两个 FileProvider 的 authorities 都是 ${applicationId}.provider
,另一个是从一个第三方库里来的,并且,它排在前面。
首先是在 Android Studio 里进行,找到调用 SimplePathStrategy 构造方法的地方,是在 android.support.v4.content.FileProvider#parsePathStrategy
:
/**
* Parse and return {@link PathStrategy} for given authority as defined in
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
*
* @see #getPathStrategy(Context, String)
*/
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
// some code here
}
这里的 context.getPackageManager().resolveContentProvider
的实现,一路通过以下路径找到:
// android.app.ContextImpl#getPackageManager
// -->
// android.app.ActivityThread#getPackageManager
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}
到这里动用一点历史经验,可知实际实现类是 PackageManagerService,来看看 PackageManagerService#resolveContentProvider
的实现:
@Override
public ProviderInfo resolveContentProvider(String name, int flags, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, name);
final String instantAppPkgName = getInstantAppPackageName(Binder.getCallingUid());
// reader
synchronized (mPackages) {
final PackageParser.Provider provider = mProvidersByAuthority.get(name);
// some code here
}
// some code here
}
在 PackageManagerService 里继续查找写入 mProvidersByAuthority
的地方,在 PackageManagerService#commitPackageSettings
:
/**
* Adds a scanned package to the system. When this method is finished, the package will
* be available for query, resolution, etc...
*/
private void commitPackageSettings(PackageParser.Package pkg, PackageSetting pkgSetting,
UserHandle user, int scanFlags, boolean chatty) throws PackageManagerException {
// some code here
synchronized (mPackages) {
// some code here
for (i=0; i<N; i++) {
PackageParser.Provider p = pkg.providers.get(i);
p.info.processName = fixProcessName(pkg.applicationInfo.processName,
p.info.processName);
mProviders.addProvider(p);
p.syncable = p.info.isSyncable;
if (p.info.authority != null) {
String names[] = p.info.authority.split(";");
p.info.authority = null;
for (int j = 0; j < names.length; j++) {
// some code here
// 【我们要找的地方】
if (!mProvidersByAuthority.containsKey(names[j])) {
mProvidersByAuthority.put(names[j], p);
if (p.info.authority == null) {
p.info.authority = names[j];
} else {
p.info.authority = p.info.authority + ";" + names[j];
}
// some code here
从上面这段中我们可以得到两个知识点:
如果已经有同名的 authority,那么后面的 Provider 配置会被忽略掉;
authority 可以配置多个,用分号分隔。(这一点在官方文档之类的都没有找到说明,也许官方觉得配置项的名称 autorities
就说明了一切?实测可正常使用。)
接下来还有一点需要确认的,就是 pkg.providers
是否是按 AndroidManifexs.xml 里的顺序排列的。
根据上面代码里的线索,可以留意到 PackageParser
类,按如下顺序递进:
// android.content.pm.PackageParser#parseBaseApk(java.io.File, android.content.res.AssetManager, int)
private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
throws PackageParserException {
// some code here
// 下面这行里的 ANDROID_MANIFEST_FILENAME = AndroidManifest.xml
parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
final String[] outError = new String[1];
final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);
// some code here
}
// -->
// android.content.pm.PackageParser#parseBaseApk(java.lang.String, android.content.res.Resources, android.content.res.XmlResourceParser, int, java.lang.String[])
// -->
// android.content.pm.PackageParser#parseBaseApkCommon
// -->
// android.content.pm.PackageParser#parseBaseApplication
// -->
private boolean parseBaseApplication(Package owner, Resources res,
XmlResourceParser parser, int flags, String[] outError)
// some code here
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("activity")) {
// some code here
} else if (tagName.equals("provider")) {
Provider p = parseProvider(owner, res, parser, flags, outError);
if (p == null) {
mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
return false;
}
owner.providers.add(p);
// some code here
至此,我们已经可以确定 pkg.providers
是按 AndroidManifest.xml 里的顺序解析出来的了。
既然已经知道了问题的原因,那么解决方案也就呼之欲出了:
源码面前,了无秘密。——侯捷
如果遇到疑难问题,而恰好又有源码可查,那么就不要犹豫,直接去看源码吧!花一些时间和耐心,最终会找到你想要的。
]]>