Docker Multi-Stage Builds

Docker 的 multi-stage 技术可以把不同的构建阶段分离开,方便修改和复用,同时还能更好地把缓存层组合在一起,避免重复劳动,还能保证最后运行时的干净纯粹,不会带入前边阶段的副产品。不过我在实际场景中遇到的问题比单纯的构建更加复杂一些——我不仅需要不同的构建阶段(build stage),还需要在不同的环境里构建同一个镜像,比如在本地和在 CI/CD 中。CI/CD 的特殊之处在于在前边的阶段中基本上已经完成了 dependency download, build, package 等一系列阶段,因此直接把 build 的结果拷贝到 Docker context 里执行 Dockerfile 的最后几步就可以了。如果复用一个 Dockerfile,那么基本上要做前边的很多重复工作,如果用两份 Dockerfile,则又会造成配置上的冗余,迟早会出幺蛾子。

为了解决这个问题,我打算使用 Docker 的 multi-stage 来做,本意是把 package 之后的几步合并到一个 stage 里,在前边的 build 阶段来区分不同的环境。就像下边的配置一样,设想中这个流程非常完美,Docker 可以根据 ENVIRONMENT 来区分不同的环境,并且在最后的 runtime 阶段中从不同的环境构建阶段中把二进制内容拷贝出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 AS runtime-base

# 当前环境
ARG ENVIRONMENT=dev

# 本地构建阶段
FROM build-base AS build-dev
RUN build
RUN package
RUN cp . /app

# CI/CD 构建阶段
FROM build-base AS build-prod
COPY . /app

# 最终阶段
FROM runtime-base AS runtime
COPY --from=build-${ENVIRONMENT} /app /app
ENTRYPOINT [ "/app/main" ]

但是这样做有个非常大的问题就是 Docker 的 build arguments 是只在单个阶段之内管用的,因此倒数第二行的 COPY 压根不认识 ENVIRONMENT 这个变量,进而这个 Dockerfile 根本无法正常构建。找了很多解决方案,最终看到虽然在 Docker 某一个阶段内不认识其他阶段的 build arguments,但是在定义阶段本身的时候是可以复用的,也就是 FROM runtime-base AS runtime 这一行,在这里是可以用 ENVIRONMENT 这个变量的。因此思路就很清晰了,可以直接改成下边这种情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 AS runtime-base

# 当前环境
ARG ENVIRONMENT=dev

# 本地构建阶段
FROM build-base AS build-dev
RUN build
RUN package
RUN cp . /app

# CI/CD 构建阶段
FROM build-base AS build-prod
COPY . /app

# 最终阶段
FROM build-${ENVIRONMENT} AS runtime
ENTRYPOINT [ "/app/main" ]

但这样有个最大的问题,就是最后的 runtime 阶段直接继承前边 build 阶段,会把这些阶段运行过程中的副产品、垃圾文件和系统包带到最后,这显然极大地违背了 multi-stage 的初衷。继承还是要继承的,关键是不能从 build 阶段继承,那么自然而然地就想到再加上一层干净的 ready 阶段来过渡,保证 ready 阶段中的内容是纯粹的二进制,每个环境都有自己对应的 ready 阶段,然后分别把自己的二进制拷贝进去就行,如下配置:

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
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 AS runtime-base

# 当前环境
ARG ENVIRONMENT=dev

# 本地构建阶段
FROM build-base AS build-dev
RUN build
RUN package
RUN cp . /app

# CI/CD 构建阶段
FROM build-base AS build-prod
COPY . /app

# 本地准备阶段
FROM runtime-base AS ready-dev
COPY --from=build-dev /app /app

# CI/CD 准备阶段
FROM runtime-base AS ready-prod
COPY --from=build-prod /app /app

# 最终阶段
FROM ready-${ENVIRONMENT} AS runtime
ENTRYPOINT [ "/app/main" ]

这样就可以完美地解决上边的所有问题,除了多出两个几乎一样的 build stages 以外没有什么成本。只要在执行 docker build 的时候传进入对应的 ENVIRONMENT 参数就行。还要注意的是,一定要使用 Docker Buildkit 来执行,这样才会根据最后的 runtime 阶段有挑选地执行需要的阶段,否则 Docker 会傻乎乎地执行所有的阶段,这样就得不偿失了。

专业学习 | 什么是图灵完备

坐公交车时突然想到 Brainfuck 这门语言,然后想到它是图灵完备的[2],之前一直靠这个词来装逼,然而细想好像说不清楚什么是图灵完备,于是翻了翻图灵完备的词条[3]。我们说一门编程语言是图灵完备的,意思是指它具有模拟图灵机的能力。另外还有一个相近的概念叫图灵等价,即如果两台计算机可以互相模拟,那么这两台计算机就是图灵等价的。

那么什么是图灵机呢,如果在现代计算机系统中去理解的话,就是具有循环和判断的能力、并且可以操作内存的系统,以下是维基百科上更为详细一些的解释[2]

图灵机从数学上描述了一个在纸带上操作的机器。纸带上有一个个符号,机器可以读写这些符号。图灵机的操作由一个有限指令集集合定义,比如“在状态42,如果当前符号是0,那么就写入1;如果当前符号是1,那么就进入状态17,如果此时看到的符号是0,那么就写入1并且将状态转换成6” 。更精确地说,图灵机由以下部分组成:

  1. 一条被分成一个个小格子的纸带,每一个小格子都包含一个有限字符集中的符号,该字符集中还有一个特殊的空白符。这条纸带可以任意向左或者向右延长,在一些理论中,这条纸带只能向右延伸,不管怎么说,纸带总是足够使用
  2. 一个位于纸带上方的读写头,它能够读写下方小格子中的符号,每次读写头可以向左或者向右移动一个格子(有的模型中是读写头固定,纸带移动),也可以不动
  3. 一个存储图灵机当前状态的状态寄存器,在图灵机运行之前,状态寄存器中是特殊的起始状态
  4. 一个有限的指令表,假设当前图灵机的状态是$q_i$,读写头下方的符号是$a_j$,该指令表可以控制图灵机依次执行以下操作:
    1. 写入或者擦除当前的符号(用$a_{j1}$替换$a_j$)
    2. 移动读写头,向左向右或者保持不动
    3. 保持当前状态或者进入下一个状态(从状态$q_i$进入状态$q_{i1}$)

从这个角度去看,图灵完备对于一门编程语言来说并不是特别难以达到的目标。实际上,目前我们能接触到的编程语言中,只有一些DSL是非图灵完备的,如HTML、CSS、SQL等[4]。在所有的图灵完备语言中,最简单的就是Brainfuck[1],它只有8种运算符,其编译器也只有200多个字节。虽然看起来很Braing Fucking,但是理论上它确实可以完成任何一门编程语言的工作。以下是它的8种操作和对应的C语言:

操作 C语言
> ++ptr;
< --ptr;
+ ++*ptr;
- --*ptr;
. putchar(*ptr);
, *ptr = getchar();
[ while (*ptr) {
] }

如果用Brainfuck来写”Hello World”的话,代码差不多是下边的样子:

1
2
3
++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.------.--------.>+.>.

图灵机的一个特点是其无法判定停机问题[5]。停机问题定义很简单,即是否能找到一个程序,它可以判定任意一个程序是否可以在有限时间(步数)内结束运行。停机问题是一种自我指涉问题,类似的还有经典的理发师悖论,“我只给不给我刮胡子的人刮胡子,那么我给不给我自己刮胡子?”。

这个问题的证明也很简单,假设程序P对于任意输入I都可以判定其是否停机,如果停机就输出1,否则输出0,那么现在给出另外一个程序F,它在内部调用了P,并且以自身作为输入,即F(F),如果F停机,即P(F)输出1,那么F(F)就无限循环;如果F无限循环,即P(F)输出0,那么F就立刻停机。构成了矛盾的状态,因此可以判定程序是否停机的程序P是不存在的。

参考文献

[1]. Wikipedia. Brainfuck[EB/OL]. https://en.wikipedia.org/wiki/Brainfuck. 2019-03-19/2019-03-26

[2]. Wikipedia. Turing machine[EB/OL]. https://en.wikipedia.org/wiki/Turing_machine. 2019-03-05/2019-03-26

[3]. Wikipedia. Turing completeness[EB/OL]. https://en.wikipedia.org/wiki/Turing_completeness. 2019-03-16/2019-03-26

[4]. yannis. Are there mainstream general-purpose non-Turing complete languages available today?[EB/OL]. https://softwareengineering.stackexchange.com/questions/202488/are-there-mainstream-general-purpose-non-turing-complete-languages-available-tod. 2013-06-24/2019-03-26

[5]. Wikipedia. Halting problem[EB/OL]. https://en.wikipedia.org/wiki/Halting_problem. 2019-03-05/2019-03-26

在服务器上搭建 Jupyter Notebook

Jupyter Notebook

安装Jupyter

假定工作目录为/home/jupyter

1
2
3
$ virtualenv venv -p python3
$ source venv/bin/activate
$ (venv) pip install jupyter

配置Jupyter

安装Jupyter之后,在~/.jupyter下查看是否存在jupyter_notebook_config.py文件,如果没有,就使用

1
$ (venv) jupyter notebook --generate-config

命令生成配置文件,Jupyter的具体配置内容参见Jupyter Notebook的配置选项,下边的几个选项是为部署在服务器上可能要用到的(下边c.NotebookAPP.password的设置方法见Jupyter Notebook添加密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Nginx访问时会出现跨域访问,需要在这里允许
c.NotebookApp.allow_origin = '*'
# 禁止随意修改密码
c.NotebookApp.allow_password_change = False
# 是否允许远程访问
c.NotebookApp.allow_remote_access = True
# 访问URL,假定我们想通过`$HOST/python`来访问
c.NotebookApp.base_url = '/python'
# 访问之后跳转的URL(自定义),要加上base_url
c.NotebookApp.default_url = '/python/tree'
# Jupyter Notebook Server监听的IP
c.NotebookApp.ip = '127.0.0.1'
# Jupyter Notebook的工作目录,用于限制访问位置
c.NotebookApp.notebook_dir = 'data/'
# 启动Jupyter Notebook之后是否打开浏览器(服务器上此选项应该关闭)
c.NotebookApp.open_browser = False
# 客户端打开Jupyter Notebook的密码哈希值
c.NotebookApp.password = 'sha1:******'
# Jupyter Notebook Server监听的端口
c.NotebookApp.port = 8888

集成 Nginx

Jupyter Notebook使用tornado作为服务器和Web框架,如果想要获取更高的性能以及灵活性,可以使用Nginx作为代理服务器。在/etc/nginx/conf.d/jupyter.conf中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80 default_server;
server_name myjupyter.com;
charset utf-8;
client_max_body_size 75M;

location /python/ {
# 这里要和Jupyter配置中的Base Url一致
proxy_pass http://127.0.0.1:8888/python/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
# 因为用到了Websocket协议,所以下边的配置是必须的
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}

配置完成之后,启动Jupyter Notebook即可远程访问

1
2
3
4
# 直接运行,测试使用
$ (venv) jupyter notebook
# 后台运行
$ (venv) nohup jupyter notebook &

此时在浏览器输入http://myjupyter.com/python即可进入Jupyter Notebook。

MySQL 单机单表切分实践

MySQL 单机单表切分实践

描述

客户的项目使用MySQL做持久化,MySQL部署在单机服务器上,前期在数据存取上没有问题。后来加了一个爬虫项目,爬取百度地图的数据,数据很快堆到了一亿多条,所有的数据都存储在单个的MySQL数据表中,整体的数据量超过了70GB,查询时的效率极低,几分钟才能出来结果。除此之外,前期分配的磁盘空间不足,整体的数据占用量也到了95%以上。所以一方面需要迁移MySQL的存储位置,另一方面需要解决查询效率的问题。

过程

存储迁移

在解决线上问题的时候,我的宗旨一直是尽量别相信中文社区的解决方案(包括本文),不过在做数据迁移的时候图省事直接找了个CSDN照做了,过程都是泪,最后还是老老实实照着StackOverflow做,迁移MySQL存储位置的方案看这里,简要描述如下:

  1. 假设你的迁移目标目录是/data/mysql
  2. 假设你的MySQL配置文件的目录是/etc/mysql/mysql.conf.d/mysqld.conf
1
2
3
4
5
6
1. $ sudo /etc/init.d/mysql stop # 或 sudo service mysql stop
2. $ sudo cp -R -p /var/lib/mysql /data/mysql
3. 打开/etc/mysql/mysql.conf.d/mysqld.conf, 将datadir指向/data/mysql
4. 打开/etc/apparmor.d/usr.sbin.mysqld,将其中所有的/var/lib/mysql修改为/data/mysql
5. $ sudo /etc/init.d/apparmor reload
6. $ sudo /etc/init.d/mysql restart # 或 sudo service mysql start

按照上述步骤就可以顺利完成存储的迁移,如果期间确实遇到了问题,那么就删除存储目录下的ib_logfile0ib_logfile1这两个文件,重新启动MySQL。

查询优化

优化查询的第一个反应就是加索引,查询依据主要是一个varchar的列,所以最初考虑直接对这一列加索引,设置了索引之后一直等它运行完成,结果一直做了四个多小时仍然没有结束。由于这个尝试早于存储迁移,而且加索引的过程中会产生大量的临时文件,所以直接撑爆了磁盘,搞了很久才救回来。也是由于数据量很大的原因,没有做备份就直接怼了索引,现在想起来也是大胆。这个尝试之后就加了块大磁盘,先做好了存储迁移,然后开始考虑单表切分的问题。

就现在的用户量而言,主要的压力并不在服务器本身,所以仍然考虑单机切分。数据表的字段之间没有特别强的关联,而且有几个字段的内容量很大,可是客户端需要的字段比较多,如果做垂直切分最后还是要Join,因此最后做了表的水平切分。客户端在查询的时候总是会带一个地区参数,而且参数只是城市,可以根据区域做水平切分。如果按照省份做切分,理想状态下会把数据表均匀切分成30多份,按照目前的数据增长速度,估计几个月之后又会上升到现在的量级,所以干脆按照城市进行切分,并且这次直接在新表上加索引。

在准备阶段,给数据表一个统一的前缀,结尾加上城市的Canton Id,用代码批量生成Model类,然后Migrate即可(项目基于Django)。接下来就是切分过程,大致思路是按照id每次从旧表中捞出10000条数据,根据city字段判断应该插入的新表,放在临时列表中,然后批量插入整个临时列表。在做切分的过程中还是遇到了一点小坑,首先是Django的查询集缓存问题,规范可以参考官方文档,做的时候有这个意识,但是还是没有足够细心,导致一开始速度慢了很多。另外还有一个更慢的地方,是在拼装新的Model实例的时候,这个过程理论上应该一瞬间完成,可是却成了时间瓶颈,检查了很久发现是一句item.city.canton_id导致了每次都重新查询一次数据库,做了City表中id到canton_id的映射之后这个问题才得以解决。外键写起来是个好东西,可是用起来稍不注意就忘了其凶残的本质,以后尽量不设置外键而是自己维护关联关系,这样才能时刻记住自己在做什么

结果

截至目前,迁移工作仍然在进行中,做完之后再来补…

Python 'python ImportError: No module named XXX' 问题

最近做毕业设计,用Python写一个文本爬虫,我的目录结构是这样的:

1
2
3
4
5
6
7
StockTextDigger
----src
--------main.py
--------__init__.py
----parser
--------__init__.py
--------source_parser.py

当要从main.py中引用source_parser.py中的一个类时出现了“python ImportError: No module named source_parser”的错误。之前写过的Python从没出现过这样的问题,可是这次无论责骂么都解决不聊,无论是加上 __init__.py 还是sys.path.append(‘XXX’)都没有用,然后忽然意识到parser这个包很有可能已经在默认搜索路径中了,然后改名字,顺利解决。

又一个官逼同的典型例子[摊手]

软工大作业·历物语(二)

文章来源:中国软工亚洲指挥中心

共同作者:纪神,爵爷,老板,小男孩(按首字拼音排序)

责任编辑:爵爷

进度汇总

先大致说一下这两周完成的内容:

  • 登录界面
  • 注册界面
  • 新闻详情界面
  • 用户偏好算法(初版)的设计与实现
  • 通知系统
  • 私信系统
  • 部分实现了数据操作辅助类族
  • 部分实现了推送系统
  • 部分UI的优化

细节还有非常非常多,当然不是说要拘泥于细节而失去了整体意识,而是在细节的把控中了解整个项目的脉络。虽然功能远没有实现完,但是现在的项目结构已经略显臃肿了,所以下一周准备梳理一下整个项目,开始尝试自上而下地审视并重新设计一下整个结构。

本周做的工作值得细说的还是用户偏好算法的设计。用户偏好算法是我们项目的核心算法,目的是为推测出用户近期对于新闻类型的偏好,并记录在系统内,从而在之后可以让用户选择自己感兴趣的信息,并且每当发布了用户感兴趣的信息之后可以推送给用户。除此之外,最重要的一点是,系统可以根据用户的喜好向用户提供兴趣相投的好友,从而提升用户黏性,并可以隐形中推广应用。

作为一个用户偏好推测算法,可以划分到人工智能的领域,在这里我想谈一下对于算法起点的考虑。说到机器学习,普通水平的实现只要看了Coursera上吴恩达的Machine Learning就够用了(当然我是说非常简单的应用),再不然Github上一堆数千star的项目,甚至都不需要知道基本概念。但是在初版算法设计中我们并没有采用机器学习算法,这是由于以下几点考虑:

  • 我们采集的数据特征值非常少
  • 我们采集的数据量非常少
  • 手机计算资源的限制
  • 在对机器学习没有深入的研究的情况下,设计出的算法有时还不如最普通的迭代

在初版算法设计中,我们只是使用了简单的权值计算,目前先实现这版测试一下效果如何,具体设计如下:

算法目的

为推测出用户近期对于新闻类型的偏好,并记录在系统内,从而在之后可以让用户选择自己感兴趣的信息,并且每当发布了用户感兴趣的信息之后可以推送给用户。除此之外,最重要的一点是,系统可以根据用户的喜好向用户提供兴趣相投的好友,从而提升用户黏性,并可以隐形中推广应用。

算法原理

算法计算结果是用户对于某种类型的新闻的偏好程度,目的在于两部分,第一部分是推测近期趋势,第二部分是推测用户喜好。

为了体现这两部分,我们使用两个衡量指标:时间因子 $tf$ (time factor)和认真程度 $cd$ (conscientious degree)来表示。为了突出两方面的因素,我们使用高阶运算来提升权值,计算出的值为 $tpw$ (temporary preference weight)。
除此之外,由于偏好值的计算是不断迭代的,所以每次计算新的偏好值时应该考虑之前的计算结果,给定一个权值因子 $wf$ (weight factor),并设旧的用户偏好值为opw(old preference weight),则新的用户偏好值为 $npw = opw \times wf + (1 – wf) \times tpw$, 目前取 $wf$ 为0.3,并取 $opw$ 初值为0。

算法过程

把用户访问一篇新闻的时间以及在这篇新闻上的停留时间分别设为 $visitTime$ (单位:毫秒)和 $stayTime$ (单位:毫秒),考虑通过这两个因素来计算 $tf$ 和 $cd$。

  1. 对于 $tf$ 的计算,主要是体现用户的偏好有多“新”,考虑取看这篇新闻距离现在的时间间隔,即 $currentTime – visitTime$, 换算成天数设为 $dayInterval$, 那么可以取 $tf = \frac{30}{dayInterval + 1}$ (取30为了使 $tf$ 和 $cd$ 处于同一个数量级)
  2. 对于 $cd$ 的计算,取平均阅读每个字所用的时间,设新闻正文字数为 $newsLength$ ,则 $cd = \frac{stayTime}{newsLength}$
  3. 对于 $tpw$ 的计算,为了使越新、阅读越认真的新闻重要性越大且增长速度越来越快,可以取 $tf$ 和 $cd$ 和的平方,即 $tpw = (tf + cd) ^ 2$。而用户在某一种类的新闻浏览记录有 $n$ 条,设每条记录计算的 $tpw$ 值为 $tpwi$,综上所述可得 $tpwi = (\frac{30 \times 24 \times 3600 \times 1000}{currentTime – visitTime + 86400000} + \frac{stayTime}{newsLength}) ^ 2$
  4. 则 $tpw$ 最终计算结果为。最终计算所得的 $npw = wf \times opw + (1 – wf) \times tpw$。(注意:还可以考虑的一点是,如果我是信息学院计算机系的,你也是信息学院计算机系的,那么我们很可能就有共同语言,也就是同一学院、同一学系或者同一年级的用户也可能有共同的喜好,即使这一点没有体现在算法计算结果上,考虑把这一点也加在偏好算法计算中)

算法实施

我们在数据库中设置了一张用户偏好表用于记录用户偏好信息,表项有用户 $id$、新闻类型、偏好旧值和详细信息,在详细信息中记录了一个数组,数组中每一个元素都包含 $visitTime$ 和 $stayTime$ 两部分。

每当用户访问一篇新闻时,都会更新相应的表项。同时在用户表中添加一个喜好标签表项,记录用户最近最感兴趣的新闻类型,最多记录三个,太多的话匹配结果会过多,使得匹配系统没有了意义,而且用户也没有精力去关注那么多的新闻类型。当分别计算完一个用户对于各个新闻类型的喜好权值之后,将其排序,得到的前三名新闻类型放入喜好标签中。

当推送新闻时,直接根据标签过滤即可。推荐好友时,比对两人的喜好标签即可,只要有共同的标签就推荐,并且如果当前用户没有喜好标签,那么就随机从所有用户中抽取;如果当前用户有喜好标签,那么对于其他待匹配的没有喜好标签的用户而言,也采用随机抽取的办法。

关于用户喜好的计算时机,因为计算不是在服务器上完成的(用云引擎有点麻烦而且也没必要占用服务器资源),所以考虑只要用户登录了,就在后台计算用户偏好,每个登录周期偏好只计算一次。

目前为止只做了简单的试验,算法可以得到较为合理的偏好值,但是在真实的应用场景中效果如何还有待进一步的测试。

Windows Server 2008 配置

因为软件工程大作业要用网站作为手动添加内容的后台,所以向学校申请了一台服务器(WindowsServer 2008 64-bit)。因为不常用Windows的服务器,拿到手之后遇到了一系列问题,写在这里以备下次需要。

安装应用程序(WampServer2)提示系统缺失MSVCR110.dll
解决方案: 见百度经验。不吹不黑,百度就这点好,大众性的问题还是靠得住的。

WampServer图标一直是黄色,无法正常启动

测试了80端口和3306端口,都没有被占用,修改之后还是没有起作用。
解决方案: 想到wamp的安装是在MSVCR110.dll之前进行的,是不是有什么地方出了错误,所以重新装了一遍WampServer,问题解决。

服务器本地Locathost可以访问,但是在我电脑上通过公网IP无法访问网站,也不能ping通

不能 ping 通问题的解决方案:打开服务器管理器->Windows防火墙->入站规则->文件和打印机共享(回显请求 – ICMPv4-In),启用。

设置方法

不能通过公网IP访问网站问题

解决方案:打开服务器管理器->Windows防火墙->入站规则,新建一个端口规则,把80端口加上,随便命名,之后公网IP可以正常访问。

设置方法

软工大作业·历物语(一)

文章来源:中国软工亚洲指挥中心

共同作者:纪神,爵爷,老板,小男孩(按首字拼音排序)

责任编辑:爵爷

终于开始了正式的开发工作。鉴于团队之前多少有点开发经验,很多界面写起来并没有什么阻滞,但由于我们都没有深入系统学习过Android架构和API,所以在有些细节上总是会有不到位的地方。

就拿笔者来说,虽然能照葫芦画瓢实现指定的界面和效果,但是总会在一些细微的地方卡住。如通过ViewPager实现SwipeView的解决方案中,ViewPager会时刻保留两个Fragment的View(此处存疑,只是实际操作的情况,并没有查阅过源码),其他的Fragment的view会被destroy掉。被destroyView的Fragment所有的控件都被“下架”,但是实例会被保留,那么对于EditText和RadioButton之类的控件而言,其内容是不会被保存的,除非单独设置变量保存或者放在savedInstanceState中。笔者在这里就卡了很久,又复习了一遍Activity和Fragment的生命周期,并且简单查看了一下ViewPager的源码,才解决了相关的问题。

在实现新闻列表的时候,由于需要上拂加载更多的效果,考虑现有开源方案太过庞大,所以笔者就手写了一个实现。因为新API强迫症,使用了RecyclerView而不是ListView。RecyclerView效率更高,功能更强大,操作也更灵活,但是少了诸多限制也就少了一些方便。如RecyclerView没有OnItemClickListener,笔者就往Adapter里扔了个回调,监听每个条目的点击事件。又如RecyclerView没有默认分隔线,这是可以理解的,因为要同时实现ListView、GridView以及瀑布流的效果。关于添加分割线的方案,鸿洋大大给出了一篇非常精彩的博文Android RecyclerView 使用完全解析 体验艺术般的控件 ,但是由于代码还是过多,所以笔者自己用代码模拟.9图片实现了分隔线效果,就过程而言要简洁的多(当然功能不够强大,具体见Android使用RecyclerView分隔线问题 )。

类似的问题还有很多,虽然都不算是大坑,但是有些地方还是挺绊脚的。现在尽量克制不去过于关注细节,先把大框架做出来,再进行优化工作。

贴出下一周的任务安排:

爵爷:

  • 完善登录界面、注册界面、新闻详情界面
  • 添加第一主界面新闻筛选机制(在新闻分类完成基础上)
  • 设计用户偏好计算算法(初步测试)

纪神:

  • 和小男孩讨论出新闻的种类,并制定从爬虫正式入库的方案
  • 完善好友界面,实现效果应与微信好友相似,尤其是右侧的A-Z导航(在https://github.com/Trinea/android-open-project找开源方案)
  • 完成好友详情界面,实现效果应与微信好友详情类似,完成好友申请处理界面(微信收到好友申请后好友界面顶端的效果)

小男孩:

  • 和纪神讨论出新闻的种类,并制定从爬虫正式入库的方案
  • 完善第四主界面
  • 完成修改用户信息的功能(修改的信息项根据数据库设计来,界面效果按照微信来。其中地区修改先不用做,我之前做过类似的东西,有完整的地区库)

老板:

  • 完成第三主界面
  • 继续爬取其他学院的新闻
  • 把爬取的信息按照{纪神和小男孩的方案}正式入库

软工大作业·倾物语(三)

文章来源:中国软工亚洲指挥中心

共同作者:纪神,爵爷,老板,小男孩(按首字拼音排序)

责任编辑:爵爷

本周大概把四个界面的样子做出来了(没有做细节,现在不贴图),并且老板那边的爬虫也可以跑了,虽然只是测试性入库,新闻的类别和学院学系等都还缠在一块没有分开,数据是不能用的,但效率可以满足要求,入库方案也并不难制定。

至此“试水阶段”结束,下边开始第一版最小化原型的开发(在倾物语(二)中已经提到,第一版最小化原型弱化设计,先走一遍流程知道我们在做什么,否则空谈设计很可能没有效果)。