Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

qxy_potato

author: DYJ (shields.io) Static Badge Static Badge

ic_launcher_round

该项目为 2022 年第四届字节跳动青训营 Android 营结营项目,项目在最终评比中获得首屈一指奖(第一名)。答辩文档原文链接:‌⁢‬⁢⁣⁡‌⁢⁡‍⁣‌‍‍‬‍⁤⁡‬‌‍⁢⁣⁤⁢⁤‌⁤⁤⁢⁡⁢青训营结业项目答辩汇报文档 -- 破忒头 组 - 飞书云文档 (feishu.cn)

一、项目介绍

项目名称:破忒头(potato)

项目功能:

项目实现了抖音的 个人中心 以及 榜单页面 。

个人中心 为APP的主页面,实现了以下内容。

  1. 个人信息的展示
  2. 端侧对数据信息的统计
  3. 点击跳转到详情页查看粉丝和关注者列表
  4. 个人作品的浏览,浏览时个人背景渐变折叠
  5. 点击作品缩略图,页面打开自动播放
  6. 播放页面实现视频流
  7. 允许从播放页面打开抖音
  8. 登录、登出
  9. 登录状态过期自判断,自动跳转提醒重新登录

榜单页面 为APP的功能页面

  1. 单页面切换查看三种类型的榜单
  2. 点击榜单时间,浏览往期榜单序号以及可以选择序号查看往期内容
  3. 点击榜单的子项,可以下弹页面,提供查看子项的详细信息

Github地址:

dyjcow/qxy_potato (github.com)

二、项目分工

团队成员 主要贡献
1. 搭建整体极简抖音使用环境,搭建网络框架以及一个易用MVP架构
2. 重构榜单页面
3. 编写登录页面以及做登录状态自识别
4. 整合修改合并的代码
1. 开发viewPager展示三个榜单,使用MVP开发整合三个Fragment
2. 开发视频详情模块,解决视频自动播放问题
81426359 (1) 1. 开发榜单整体页面背景
2. 开发粉丝列表以及关注者列表模块
1. 开发榜单影视数据 RecyclerView 及 Item
2. 开发个人主页模块
1. 开发工具类
2. 编写网络状态识别类

下文为开发期间的协助文档

字节青训营项目协作文档

三、项目实现

3.1 技术选型与相关开发文档

说明项目的技术选项,包含技术栈等信息,同时列举说明对应的技术选型在项目中的作用;开发文档主要阐述本次项目的协作模式、开发模式、如何把项目跑起来等维度信息

项目相关技术如下

开发语言: Java 、kotlin

开发工具:Android Studio 、Fiddler、Apifox

技术栈:

  • 设计模式:订阅者模式进行 网络请求 和 通知事件
  • 网络框架:使用 Rxjava+Retrofit 封装网络请求于P层
  • 事件总线:使用 EventBus 进行事件发送和订阅,且对其进行简单封装,使用索引加快其编译速度
  • 后台服务:使用 Service 加持网页的视频播放,WorkManager 实现活跃状态的Token自刷新
  • 页面编写:使用 ViewBinding 加快资源绑定速度,使用 Glide 加载网络图片
  • UI辅助 :使用 RxTool 进行崩溃捕捉以及使用其弹窗等, BRVAH 做 RecyclerView 的管理,SmartRefresh 做下拉加载和下拉刷新, UltimateBarX 做沉浸式管理,以及 Android-PickerView 做滚动选择框
  • 数据持久化:使用 MMKV 做键值对持久化存储以及对数据进行加密处理
  • 性能检测:使用 Dokit 对 APP 进行端侧的性能检测

协作开发模式:团队的开发方式时 DevOps 模式,以增量开发,以及不断使用提出新需求的形式来完成项目。团队使用的协作开发工具和平台是 Git 和 GitHub。以功能点为分支,指定组长审阅合并代码

代码提交规范:遵从阿里巴巴的Git规范

代码构建工具:Gradle

代码运行:到抖音开发平台获取对应的密钥,更改 app/src/main/res/values/Key.xml 该资源文件中的 value_client_key 以及 value_client_secret 即可运行

3.2 架构设计

整个项目采用的架构设计,可以包含流程图示例、代码示例等

项目的UML图如下,可以点击查看

Potato UML图

3.2.1 软件架构选择:MVP模式。

MVP模式将应用分为三层,并且各个对应的层的职责如下:

  • Model层,主要负责数据的提供。Model层提供业务逻辑的数据结构(比如,实体类),提供数据的获取(比如,从本地数据库或者远程网络获取数据),提供数据的存储。
  • View层,主要负责界面的显示。View层不涉及任何的业务逻辑处理,它持有Presenter层的引用,当需要进行业务逻辑处理时通知Presenter层。
  • Presenter层,对于Presenter层他是连接View层与Model层的桥梁并对业务逻辑进行处理。在MVP架构中Model与View无法直接进行交互。所以在Presenter层它会从Model层获得所需要的数据,进行一些适当的处理后交由View层进行显示。这样通过Presenter将View与Model进行隔离,使得View和Model之间不存在耦合,同时也将业务逻辑从View中抽离。

综上可得出使用MVP架构有以下优点:

  • 模型与视图完全分离,我们可以修改视图而不影响模型;
  • 可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部;
  • 我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁;
  • 如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)。

存在问题及解决方案:

  • 若业务剧增可能会造成接口类爆炸,Presenter层的代码难以管理。鉴于榜单业务较为简单,使用MVP更容易管理各层
  • 可能会造成内存泄漏问题:当用户关闭了View层,但此时Model层仍然进行耗时操作,因为presenter持有view的引用而无法回收view,造成内存泄漏。其实可以重写view中的onDestroy方法,在view销毁时强制回收presenter或者是采用弱引用。

我们的方案

基于上述问题,我们封装了一个MVP架构,由于M层多用于网络请求,且我们可以将网络请求封装起来,所以将M层嵌入到P层中。这是符合迪米特原则的。

带你封装MVP架构(上)|青训营笔记 - 掘金 (juejin.cn)

带你封装MVP架构(下)|青训营笔记 - 掘金 (juejin.cn)

屏幕适配方案

在绘制页面的时候,我们以360dp的宽度为基准,然后对多种机型的适配,由其页面渲染的时候进行转换,使得同一个dp在不同设备中都会绘制为同一px。相关代码如下

/**
 * 设置统一的Density,使得对应的对应的dp转化为同一px。从而适配多种屏幕
 * 
 * @param activity 当前Activity
 */
public static void setCustomDensity( Activity activity ){
    final Application application = MyUtil.getApplication();

    final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

    if (sNonCompatDensity == 0){
        sNonCompatDensity = appDisplayMetrics.density;
        sNonCompatScaleDensity = appDisplayMetrics.scaledDensity;
        application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(@NonNull Configuration newConfig) {
                if (newConfig.fontScale > 0){
                    sNonCompatScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }

            @Override
            public void onLowMemory() {

            }
        });
    }

    final float targetDensity =  ((float) appDisplayMetrics.widthPixels) / 360;
    final float targetScaleDensity = targetDensity * (sNonCompatScaleDensity/sNonCompatDensity);
    final int targetDensityDpi = (int) (160*targetDensity);

    appDisplayMetrics.density = targetDensity;
    appDisplayMetrics.scaledDensity = targetScaleDensity;
    appDisplayMetrics.densityDpi = targetDensityDpi;

    final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
    activityDisplayMetrics.density = targetDensity;
    activityDisplayMetrics.scaledDensity = targetScaleDensity;
    activityDisplayMetrics.densityDpi = targetDensityDpi;
}

3.2.2 网络架构选择:Retrofit

Retrofit是基于OKHttp的RESTFUL Api请求工具。App应用程序通过Retrofit请求网络,实质上是使用Retrofit接口层封装请求参数、Header、Url等信息,之后由OkHttp来完成后续的请求工作。在服务端返回数据后,OkHttp将原始数据交给Retrofit,Retrofit根据用户需求解析。

尽管Retrofit相对于OkHttp有更大包体积及更高的使用成本,但是它在具有OkHttp的所有优点的同时,还具有支持注解配备请求、可以搭配多种Converter的数据解析等优点。

我们封装的特点:

我们对 Retrofit 的封装,是将其与Rxjava一起封装的。通过订阅者模式,让所有网络请求都加入到一个序列中,序列执行结束后使其接收到结果有序的处理,这会让网络请求十分的优雅,条理清楚。

在封装的时候,我们还配合门面模式,对应加入了拦截器、转换器、以及Cookie过期检测及自缓存策略。如此可以实现 多BaseUrl、添加全局字段、异常拦截自处理、自动更新过期Cookie 等等的功能。

3.3 项目代码介绍

3.3.1 package目录介绍(架构说明)

img

  • annotation:注解类,项目的注解存放于此
  • app:继承 Application,完成各种初始化操作
  • base:各种基类,包括Activity,Fragment,bean等,是封装MVP的关键。其使得后续只需要继承BaseActivity、BaseFragment、BaseView 即可方便的使用,让程序员可以专注于业务的开发。
  • bean:数据类,存储包括网络请求解析使用到的数据类,以及EventBus发送事件使用到的类
  • common:常量池,存储项目中的各种常量,程序开发中使用这些常量会避免人为失误出现的低级编码错误问题
  • http:网络请求框架包,这里进行了网络数据拦截、分发、修改等,是网络框架的核心
  • util:工具类模块,这个模块封装了多种工具类,包括 图片、网络、颜色、时间、Log(可统一设置开关)等等。一个健全的工具类是开发者的高效武器
  • moudule:功能模块,我们在图片中可以看到。图中分了多个包,这些包就是不同的模块,开发对应模块的成员就可以在此建立自己的包来进行开发。如下图,这我们在自己模块的模块里面的分包情况。

img

3.3.2 功能难点和解决思路

架构:易用解耦的MVP架构

难点:架构封装的时候思考的无法全面

描述:封装架构的时候,我们会需要思考多种可能的情况,会思考如何提高兼容性,提高性能等。但是无论思考多少,在最终写项目的时候都还是会遇到未考虑到的问题

解决:在最初做架构设计的时候,必然是会无法完全预想未来的问题的。对一个团队而言,一个好的架构就是不断根据实际改进,变得更加适合团队。例如一个以Cookie作为令牌的开发架构迁移去以Token作为令牌的项目时候,以前未有这种业务,直到业务出现改变,发现架构不合适了,才会做出改变。所以说需求才是导致变更的最大因素,一个好的架构的诞生,必然是要有业务的陪练的。just do it

功能:登录页面,榜单页面

难点:构造美观页面,且性能不受影响;Token的刷新;多次登出登录崩溃

解决:

编写页面的时候,想要让页面美观,第一点要做的就是如何算是美观。自己的感受可以知道它是否美观,但是凭空设计一个美观的页面还是有些难的;做个人的APP,我是方法是去借鉴市面上多人使用的APP的设计,一般这些经过市场验证的软件其美观度是在线的。例如我的登录页是模仿 “派对岛”,重构的榜单页的Tab吸顶是模仿购物软件的,列表子项是模仿抖音自己的。

页面美观的同时要做到性能不会缺失。我的方案是少用Activity,多用Fragment;页面布局层级尽量少,可以经常使用相对布局;网络请求次数能少尽量少;事件相关用观察者模式;UI(主线程)用的时候多思考任务是否耗时,是否会影响体验。

为减少网络的请求,我把ClientToken的请求尽量做到少。我设置了一个WorkManager的后台任务,让APP在开启状态才每隔2h刷新一次,当APP退出的话就会取消该任务


功能:工具类

工具类帮助开发者解决图片、颜色操作以及检测网络状态。

开发中的难点:

如何从url获取图片对象,如何对不同格式的图片文件进行常见的图片操作。

解决方法:

开发者先从url中获取inputstream,在工具类中实现inputstream到bitmap的转化。工具类提供不同格式图片间的转换方法,开发者可将其他格式的文件转为bitmap。工具类提供各种对bitmap进行常见图片操作的方法,这些方法主要通过bitmap类的自带方法以及canvas实现


功能:个人页面ui绘制,榜单页recyclerview绘制。

难点:在折叠和非折叠状态下,判断是否显示小头像、title,以及打开侧边栏的按钮变色。

解决方法:对折叠栏进行坐标位置监听,判断toolbar与 appbarlayout的相对位置。

难点:reycylerview的分割线绘制。

解决方法:实现ItemDecoration类,重写 getItemOffsets方法,根据不同的recyclerview的LayoutManger判断,进行对item进行不同的分割。


功能:电影等榜单背景

难点:抖音原生的榜单背景无法获取,通过截图等手段获取的图片模糊。

解决方法:使用可折叠标题栏作为背景布局,再通过Glide从url动态加载风景图。

功能:关注粉丝页面

难点:Tablayout和Viewpager的绑定,Tablayout的切换效果,RecyclerView的加载、下拉刷新和上拉加载。

解决方法:使用TabLayoutMediator绑定Tablayout和Viewpager,在TabLayoutMediator中设置文本内容。分别使用Tablayout和Viewpager的切换监听器,更改选中与未选中的文本效果。通过传入不同type,发起相应的网络请求及设置对应的adapter,从而达到复用fragment的效果。使用SmartRefreshLayout的滑动监听器,判断用户操作从而发起网络请求并更新adapter。


功能:基于MVP架构实现碎片展示榜单页,串联背景和榜单item

难点:presenter层的实现网络请求的相似代码如何复用

解决方法:创建碎片时标注碎片对应的榜单类型,网络请求时通过传入该标注,复用相同的网络 请求代码

功能:使用WebView实现抖音个人视频播放详情页

难点:webview的配置及其性能优化,打开网页自动播放

解决方法:

  • 使用腾讯x5内核提高性能,同时做冷启动优化,提前初始化内核,并在第一次打开WebView时开启一个服务异步加载WebView;
  • 开启webview的缓存功能,提升其在网络请求时的加载速度;
  • WebView的生命周期绑定activity的生命周期,activity销毁时要销毁webview并解除对webview的引用,防止内存泄漏;
  • 服务器响应的前端页面没有设置视频资源的自动播放,而且页面的加载和资源加载并不是同时完成,webview只有对页面加载完成的回调,并没有资源加载完成的回调。所以不能通过webview简单实现自动播放,要在页面加载完成的回调中,通过在java中调用JavaScript代码,获取视频对象并对视频设置自动播放属性,同时对视频设置资源加载完成的回调函数,在资源加载完成时播放

四、测试结果

建议从功能测试和性能测试两部分分析,其中功能测试补充测试用例(单元测试等),性能测试补充性能分析报告、可优化点等内容。

4.1 功能测试

由于时间不充分,未编写单元测试代码,网络请求部分的测试是通过抓包工具进行验证实现

网络抓包验证手机端的网络请求的内容及其格式,还有返回的报文是否与文档要求一致

软件进行了黑盒测试,对软件的各流程路线进行测试,并对测试过程中出现各种各种问题进行修复,经过多轮测试,软件已未出现bug

4.2 性能测试

项目使用了Dokit对项目进行性能测试。

4.2.1 帧率

img img img img

在新创建Activity的时刻会少量丢帧,最低为打开WebView的时候变为55。后面帧率会平稳在60帧。出现丢帧的原因,一是由于Activity的创建需要开启建立多项任务,本身就会较为消耗资源;二是页面层级过多,双重缓冲的渲染时间会过长导致页面丢帧。

优化思路:架构换为单Activity、多Fragment的形式,减少不必要的Activity多次创建;页面布局使用 CoordinatorLayout ,减少页面层级

4.2.2 CPU

img img

普通页面的CPU消耗占比为 0%~14%,WebView页面最高则会接近20%。处于正常区间,当占用并不算低。

4.2.3 内存占用

img img

内存占用为150~200M,内存占用较大,已出现内存泄露。

内存较大一个由于APP在开启阶段就会对WebView内核进行初始化,这是为了增强WebView的使用体验,且会同时开启其后台服务。另一个是代码编写的时候,context的获取是持有Activity的,导致其无法GC等等的代码实现问题,导致了内存泄露。

4.2.4 流量监控

img img

对所有页面进行抓包,流量的消耗较少45min内不超50k;以及页面的响应速度较快,正常状态下,基本都低于1ms

4.2.5 Crash

img img

无Crash情况

4.2.6 卡顿检测

img

无超过500ms的卡顿情况

4.2.7 大图检测

img

页面存在文件大小超过150k,内存大小超过1MB的图片。

需要服务端发送图片前进行压缩,且端侧显示图片前做好图片压缩

4.2.8 页面启动耗时

img img

对Activity的跳转进行耗时检测,新开的Activity平均在100ms左右,WebView会达到150ms,页面耗时不算过高

4.2.9 弱网测试

img img img

网络超时或是断网情况下,会有网络异常提示和请求时候会对相关异常做出判定。但会显示白屏

优化思路:对Get请求做缓存,无网时候自动显示上一次的缓存。添加空布局,当布局数据为空时候显示空布局页面

五、演示 demo (必须)

可同步粘贴录屏 + 核心功能截图

5.1 录屏

录屏文件_来自飞书

5.2 截图

img img img
img img img
img img img
img img img
img img

六、项目总结与反思

  1. 目前仍存在的问题
    • 代码冗余,MVP架构解耦带来的问题就是项目的类以及代码的数量都会升多
    • Activity数量过多,由于架构未作单Activity多Fragment的设计,导致几乎每个页面都要使用到一个Activity,这会明显降低页面打开的速度和增大功耗
    • 模块的还不够解耦,项目也未做好模块化的工作,没有把基类模块、功能模块拆解(当然,当前业务还没有这种大的需求)
  2. 已识别出的优化项
    • 事件总线使用了索引,加快EventBus的使用速率
    • 资源绑定使用ViewBinding,加快资源寻找速度
    • 键值对类型存储使用MMKV,其内存映射走的是Binder,加快存储速度
    • WebView开启多进程,防崩溃且提高APP可使用的内存
    • 多进程的预加载,减少页面的加载时间
    • WebView使用X5内核,加快其启动速度以及提高视频播放的体验
  3. 架构演进的可能性
    • 架构可以逐渐将其模块化,抽离出譬如网络请求的基本模块,减少复用的成本
    • 代码语言和架构类型改用kotlin和MVVM,可以有效提高效率
  4. 项目过程中的反思与总结
    • 开发协作会议和开发文档十分重要,可以有效提高开发效率
    • 团队协作中,规定好git的提交格式,分支推送的规则,以及合并的请求十分重要。做好这些规定,能减少代码冲突以及沟通失误导致的时间浪费

极简抖音中的优化点|青训营笔记 - 掘金 (juejin.cn)

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages