chenjim 发布的文章

转载自 https://github.com/getActivity/AndroidProject/blob/master/HelpDoc.md
以下内容基于项目 https://github.com/getActivity/AndroidProject

目录

为什么没有用 MVP



  • AndroidProject 舍弃 MVP 的最大一个原因,需要写各种类,各种回调,如果这个页面比较简单的话,使用 MVP 会让原本简单的代码变复杂,导致后续开发和维护成本是非常高,前期付出的代价和后期的维护不成正比关系,当然这种说法只适用于各种中小型项目,大型的项目我还没有经历过,不过我觉得,无论是 MVC、MVP、MVVM,它们出现的目的是为了解决代码多并且乱的问题,作用就是给代码做分类,但是可以跟大家分享我的心得,我并不看好 MVP,因为它让我开发和维护都很痛苦,所以我就直接将它从 AndroidProject 移除,目的也很简单,不推荐大家使用,因为 MVP 不适合大多数项目的开发和维护。我更推荐大家直接将代码写在 Activity,但是有一个前提条件需要大家遵守,大家要做好代码封装和重复代码的抽取,尽量让 Activity 成为只有业务代码的类,这样一个项目里面的大多数 Activity 代码量都能很好控制在 1000 行代码以内。但是这种看似简单的操作,但是实际要做到是一件不容易的事情,这里面不仅要解决代码带来的问题,还要解决带来的各种人性矛盾,困难重重,这种想法经过很长一段时间的思考,虽然写法在开发和维护中效率是非常高的,但是不被大多数人认可,大家更愿意相信 MVC、MVP、MVVM,而很少有人理解这三种模式的本质是什么,就是为了给代码做分类,但这三种模式都不够灵活,很生硬,像是一套套规则,而这样的代码分类,只会让大多数人的开发越来越头疼。

为什么没有用 ButterKnife

  • 随着 AndroidProject 的不断优化,ButterKnife 功能很强大,但是实际开发中,大多数人只用到了 BindView 和 OnClick 注解,在 OnClick 注解在我的项目中发现一个 Bug,就是有时候不会响应点击事件,这个问题并不是必现的。还有 BindView 注解只是在视觉上面将 View 和 ID 的关系更明显了,它其实不能为我们简化代码,因为使用 BindView 和 findViewById 的代码量是一样的。
  • ButterKnife 最大的缺点是还会自动生成 ViewBinding 类,就算在类中只使用了一个 BindView,它也会生成这个类,其实这样是不太好的。
  • 另外一个点,将 Android Studio 升级到 4.1 之后,会出现以下提示,这个是因为 Gradle 在 5.0 之后的版本,View ID 将不会以常量的形式存在,所以不能将其定义在 BindView 注解或者在 switch case 块中。
Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as annotation
Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them in switch case statements
  • 考虑到这些情况,我在新版的 AndroidProject 上面移除了 ButterKnife 框架,其实 findViewById 一直挺好,只是我们没有认真思考过而已。
  • 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:FindViewByMe

为什么没有用 ViewBinding

  • 首先 ViewBinding 和 ButterKnife 有一个相同的毛病,就是自动生成一个类,然后在这个类里面进行 findViewById,但是有一个致命的缺点,每个 Activity / Fragment / Dialog / Adapter 都需要先初始化 ViewBinding 对象,因为每次生成的类名都是不固定的,所以无法在基类中封装处理,并且每次都要写 binding.xxx 才能操作控件。
ActivityXxxxBinding.inflate binding = ActivityXxxxBinding.inflate(getLayoutInflater());
binding.tv_data_name.setText("字符串");
  • 另外一个它会根据控件 id 作为属性的名称,这样会导致一个代码不规范的问题,如果在 xml 中控件的 id 命名符合规范了,会导致在 Java 代码中的命名不规范,如果在 Java 代码中的命名规范了,又会导致 xml 的 id 不符合规范了。而代码规范关系到后续的代码维护,是一个很重要的问题,不容忽视。
  • 虽然 ViewBinding 是谷歌官方推荐的,但是我觉得并不完美,解决了 findViewById 的同时又带来了其他的问题,在关键问题上有问题和矛盾,直白点说这些问题都是硬伤。
  • 正如我上面所说的,findViewById 一直挺好,只是我们没有认真思考过而已。
  • 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:FindViewByMe

为什么没有用 DataBinding

  • DataBinding 最大的优势在于,因为它可以在 xml 直接给 View 赋值,但它的优点正是它最致命的缺点,当业务逻辑简单时,会显得格外美好,但是一旦判断条件复杂起来,由于 xml 属性不能换行的特性,会导致无法在 xml 直接赋值又或者很长的一段代码堆在布局中,间接导致 CodeReview 时异常艰难,更别说在原有的基础上继续更新迭代,这对每一个开发者来讲无疑是一个巨大的灾难。
  • 还有一个是 DataBinding 的学习成本比较高,其次成本也挺高,使用前需要做很多封装,另外每次使用时都需要添加 layoutdata 节点,然后在 Java 代码中初始化 DataBinding 对象,无法在基类中封装处理,每次都要写 binding.xxx 才能操作控件,和 ViewBinding 的问题差不多。
ActivityXxxxBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_xxxx);

为什么没有用组件化

  • 先来说说组件化的优点,能够把不同的业务代码进行隔离,达到完全解耦的效果,同时也能提升编译和打包速度。但是这两个优点只有项目业务变得大并且复杂的情况下才能产生价值,否则价值并不大,在我看来,代码解耦其实是把双刃剑,解耦的过程相对比较麻烦,这会直接加大前期开发的工作量,并且一些解耦的方式可能会导致代码重复,例如 AndroidManifest 清单文件,需要同时配置两份文件,后期改动也需要改两遍,另外一个是路由跳转,现在大多数路由框架都是通过 APT 生成一张映射表,这个需要我们每写一个 Activity 都要写一个路径的注解,这个不仅写起来麻烦,管理起来也会很麻烦,另外对每个业务模块的 SDK 初始化操作和数据存储交互上又该如何处理和解耦?这些都是组件化所存在的问题,矛和盾又该如何抉择?
  • 组件化其实是一个很好的思想,但是它并不适用于中小型项目,因为这些项目并没有那么复杂,大部分业务模块都很小,大的业务模块其实也不多,当然我个人建议可以将大点的模业务进行模块化,但是没有必要做组件化,因为一旦涉及,从组件化中得到弊会大于利。而在一些大型的项目中,大大小小的模块非常多,一次打包编译可能要半个小时甚至更久(请注意大厂的电脑基本都是高配或者顶配),相比较这种情况之下,组件化的优点就已经大于了它的缺点,同时他们也有充足的人力和过硬的技术,并且能长期投入巨大的时间和精力来做这件事。
  • AndroidProject 面对的是大众开发者,所以更倾向中小型的项目代码的设计,虽然我没有做过大型的项目,但是在我看来是差不多的,最大的不同可能是代码分类方式的不同,该做的事情不会少,该写的代码也不会少,就是业务和代码的体量上比我们大,所以他们要处理体量大所带来的的问题。

为什么没有集成界面侧滑功能

  • AndroidProject 其实有加入过这个功能,但是在 v9.0 版本 就移除了,原因是第三方侧滑框架 BGASwipeBackLayout 在 Android 9.0 上面会闪屏,并且还是 100% 必现用户体验极差,我也跟作者反馈过这个问题,但结果不了了之,所以不得不移除。但是到了 v10.0 版本,我又加上界面侧滑功能了,不过这次我换成了 SmartSwipe 来做,但是我又再一次失望了,这个框架在 Android 11 上面,如果 Activity 上有 WindowManager 正在显示,然后使用界面侧滑,那么会出现闪屏的情况,具体效果如下图:

  • 就这个情况我也联系过作者,并详细阐述了产生的原因和具体的复现步骤,但是我等了三天连个回复都没有,实属有些让我心寒,在等待的期间我看到 Github 的 issue 已经基本没有回复了,并且最后一次提交是在 13 个月前了,种种迹象都已经表明,所以经过慎重考虑,最终决定在 v12.1 版本 移除界面侧滑功能。

为什么没有用今日头条的适配方案

  • 关于屏幕适配方案,其实不能说头条的方案就是最好的,其实谷歌已经针对屏幕适配做了处理,就是 dp 和 sp ,而 dp 的计算转换是由屏幕的像素决定,系统只认 px 单位, dp 需要进行转换,比如 1dp 等于几个 px ,这个时候就需要基数进行转换,比如 1dp = 2px,这个基数就是 2。
ldpi:1dp=0.75px

mdpi:1dp=1px

hdpi:1dp=1.5px
 
xhdpi:1dp=2px

xxhdpi:1dp=3px

xxxhdpi:1dp=4px
  • 这个是谷歌对屏幕适配的一种默认方式,厂商也可以根据需要去修改默认的基数,从而达到最优的显示效果。
  • 谷歌的屏幕适配方案也不是百分之一百完美的,其实会存在一些需求不能满足的问题。谷歌的设计理念是屏幕越大显示的东西越多,这种想法并没有错,但有些 App 可能对这块会有要求,希望根据屏幕大小对控件进行百分比压缩。这个时候谷歌那套适配方案的设计已经和需求完全不一致了。
  • 那什么样的 App 才会有那样的需求呢?现在手机的屏幕大多在 5 - 6寸,而平板大多在 8 - 10 寸,也就是说我们只适配手机的话,只需要针对 5 - 6 寸的,并且它们的分辨率都差不多,其实用谷歌那种方案是最优的,如果我们需要适配平板的话,一般都会要求对控件进行百分比压缩,这个时候谷歌那套方案会把原先在手机显示的控件在平板上面变大一点,这样就会导致屏幕剩余的空间过大,导致控件显示出来的效果比较小,而如果采用百分比对控件压缩的方式,能比较好地控制 App 在不同屏幕下显示的效果。
  • 另外谈谈我的经历,我自己之前的公司主要是做平板上面的应用,所以也用过 AutoSize 框架,一年多的使用体验下来,发现这个框架 Bug 还算是比较多的,例如框架会偶尔出现机型适配失效,重写了 getResources 方法情况之后出现的情况少了一些,但是仍然还有一些奇奇怪怪的问题,这里就不一一举例了,最后总结下来就是框架还不够成熟,但是框架的思想还是很不错的。我后面换了一家公司,也是做平板应用,项目用的是用通配符的适配方案,跟 AutoSize 相对比,没有了那些奇奇怪怪的问题,但是代码的侵入性比较高。这两种方案各有优缺点,大家看着选择。

  • 在这块我也发起过群投票,相比谷歌的适配方案,大多数人更认同那种百分比适配方案,秉承着少数服从多数的理念,我在 AndroidProject v13.0 版本 加入了通配符的适配方案。虽然有一部分人不认同,但是我想跟这些人说的是:我的每一个决定都是十分谨慎的,因为这其中涉及到许多人的利益,AndroidProject 虽然是我创造的,但是它早就不是我一个人的了,而是大家的,每个重要的决定我都会考虑再三才会去做,在做决定的时候我会把大众的利益放在第一位,把自己的利益放在最后一位,所以大家唯一能做的是,相信我的选择。或许你可能觉得这样不太对,也随时欢迎你提出不同的意见给到我,我不认为自己做的决定一定都是对的,但是我会一直朝着对的方向前进。

字体大小为什么不用 dp 而用 sp

  • 首先我们先回顾一下谷歌原生的写法,将控件大小的单位定成了 dp,而字体大小的单位定成了 sp,而无论是 dp 还是 sp 作为单位,最终还是会转成 px 的单位。
  • 谷歌这样做也有一定目的,dp 是根据屏幕的密度来计算的,而 sp 是根据手机设置的字体大小来计算的,如果用 dp 来代替 sp 会有一个问题,那么就是无论用户在手机里面怎么设置字体大小,应用的字体大小不会产生任何变化。这种场景对年轻人来讲没有太大的影响,而对一些老龄用户,例如我们的爸妈,他们一般会把手机的字体调大,这样才能看清楚里面的字,如果我们采用 dp 来代替 sp 的方案,会对这类用户造成不便,换位思考,我们终有一天也会变老,变得老眼昏花,我们会如何看待这个事情?
  • 显然这种方式是不合理的,也非常地不人性化。网上这种方案可能主要就是为了解决把控件宽高写死之后,在某些字体上显示比较大的机型会出现字显示不全的问题,而这种把控件宽高写死的方式本身也是不合理的,应该在不得已的情况下才把控件的宽高写死,一般情况下我们应当使用自适应的方式,让控件自己测量自己的宽高,特别是在有显示字体的控件下,就更不应该把宽高写死。

为什么没有用 DialogFragment 来防止内存泄漏

  • DialogFragment 的出现就是为了解决 Dialog 和 Activity 生命周期不同步导致的内存泄漏问题,在 AndroidProject 曾经引入过,也经过了很多个版本的更新迭代,不过在 10.0 版本后就被移除了,原因是 Dialog 虽然有坑,但是 DialogFragment 也有坑,可以说解决了一个问题又引发了各种问题。先来细数我在 DialogFragment 上踩过的各种坑:

    1. DialogFragment 会占用 Dialog 的 Cancel 和 Dismiss 监听,为了就是在 Dialog 消失之后将自己(Fragment)从 Activity 上移除,这样的操作看起很合理,但是会引发一个问题,那么就是会导致我们原先给 Dialog 设置的 Cancel 和 Dismiss 监听被覆盖掉,间接导致我们无法使用这个监听,因为 Dialog 的监听器只能有一个观察者,而 AndroidProject 前期解决这个问题的方式是:将 Dialog 的监听器使用的观察者模式,从一对一改造成一对多,也就是一个被观察者可以有很多个观察者,由此来解决这个问题。
    2. DialogFragment 的显示和隐藏操作都不能在后台中进行,否则会出现一个报错 java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState,这个是因为 DialogFragment 的 show 和 dismiss 方法使用了 FragmentTransaction.commit 方法,这个 commit 方法会触发对 Activity 状态的检查,如果 Activity 的状态已经保存了(即已经调用了 onSaveInstanceState 方法),这个时候把 Fragment commit 到 Activity 上会抛出异常,这种场景在执行异步操作(例如请求网络)未结束前,用户手动将 App 返回到桌面,然后异步操作执行完毕,下一步就是回调异步监听器,这个时候我们的 App 已经处于后台状态,那么我们如果在监听回调中 show 或 dismiss DialogFragment,那么就会触发这个异常。AndroidProject 前期对于这个问题的解决方案是重写 DialogFragment.show 方法,加一个对 Activity 的状态判断,如果 Activity 处于后台状态,那么不去调用 super.show(),但是这样会导致一个问题,虽然解决了崩溃的问题,但是又会导致 Dialog 没显示出来,而重写 DialogFragment.dismiss 方法,直接调用 dismissAllowingStateLoss 方法,因为这个方法不会去检查 Activity 的状态。虽然这种解决方式不够完美,但却是我那个时候能想到的最好方法。
    3. 最后一个问题是关于 DialogFragment 屏幕旋转的问题,首先 DialogFragment 是通过自身 onCreateDialog 方法来获取 Dialog 对象的,但是如果我们直接通过外层给 DialogFragment 传入 Dialog 的对象时,这样的代码逻辑貌似没有问题,但是在用户进行屏幕旋转,而刚好我们的应用没有固定屏幕方向时,DialogFragment 对象会跟随 Activity 销毁重建,因为它本身就是一个 Fragment,但是会导致之前的外层传入 Dialog 对象被回收并置空,然后再调用到 onCreateDialog 方法时,返回的是一个空对象的 Dialog,那么就会直接 DialogFragment 内部引发空指针异常,而 AndroidProject 前期解决这个问题的方案是,重写 onActivityCreated,赶在 onCreateDialog 方法调用之前,先判断 DialogFragment 对象内部持有的 Dialog 是否为空,如果是一个空对象,那么就将自己 dismissAllowingStateLoss 掉。
  • 看过这些问题,你是不是和我一样,感觉这 DialogFragment 不是一般的坑,不过最终我放弃了使用 DialogFragment,并不是因为 DialogFragment 又出现了新问题,而是我想到了更好的方案来代替 DialogFragment,方案就是 Application.registerActivityLifecycleCallbacks,想必大家现在已经猜到我想干啥,和 DialogFragment 的作用一样,通过监听 Activity 的方式来管控 Dialog 的生命周期,但唯一不同的是,它不会出现刚刚说过 DialogFragment 的那些问题,这种方式在 AndroidProject 上迭代了几个版本过后,这期间没有发现新的问题,也没有收到别人反馈过这块的问题,证明这种方式是可行的。

为什么没有用腾讯 X5 WebView

  • 首先我问大家一个问题,腾讯出品的 X5 WebView 就一定比原生 WebView 好吗?我觉得未必,我依稀记得 Android 9.0 还是 Android 10 刚出来的时候,我点了升级按钮,然后就发现微信和 QQ 的网页浏览卡得让我怀疑人生,不过后面突然某一天就变好了,从这件事可以得出两点结论:

    1. 第一个 SDK 有自我更新功能,意味着 WebView 掌控权握在腾讯公司手中
    2. 第二个是 SDK 需要腾讯来持续维护,意味着这个项目的生命周期会跟随腾讯公司的发展和决策
  • 基于以上两点,我的个人建议是优先使用原生 WebView,如果不满足需求了,可以自行替换成 X5 WebView,当然不是说 X5 WebView 一定不好,用原生 WebView 一定就好,而是 AndroidProject 的目标是稳中求胜,另外一个是 AndroidProject 中有针对 WebView 做统一封装,后续替换成 X5 WebView 的成本还算是相对较低的。

为什么没有用单 Activity 多 Fragment

  • 这个问题在前几年是一个比较火热的话题,我表示很能理解,因为新鲜的事物总是能勾起人的好奇,让人忍不住试一试,但是我先问大家一个问题,单 Activity 多 Fragment 和写多个 Activity 有什么优点?大家第一个反应应该是每写一个页面都不需要在清单文件中注册了,但是这个真的是优点吗?我可以很明确地告诉大家,我已经写了那么多句代码,不差那句在清单文件注册的代码。那么究竟什么才是对我们有价值的?我觉得就两点,一是减少前期开发的工作量,二是降低后续维护的难度。所以省那一两句有前途吗?我们是差那一两句代码的人吗?如果这种模式能够帮助我们写好代码,这个当然是有价值的,非常值得一试的,否则就是纯属瞎扯淡。不仅如此,我个人觉得这种模式有很大的弊端,会引发很多问题,例如:

    1. 有的页面是全屏有的页面是非全屏,有的页面是固定竖屏有的页面是横屏,进入时怎么切换?返回时怎么切换回来?然后又该怎么去做统一的封装?
    2. 不同 Fragment 之间应该怎样通讯?Activity 有 onActivityResult 方法可以用,但是 Fragment 有什么方法可以用?还是全用 EventBus 来处理?如果是这样做会不会太低效了?每次都要写一个 Event 类,并且在代码中找起来是不是也不太好找?
    3. 如何保证这个 Activity 被系统回收之后,然后引发重建操作,又该如何保证这个 Activity 中的多个 Fragment 之间的回退栈是否正常?假设这个 Activity 里面有 10 个Fragment,一下子引发 10 个 Fragment 创建是否会对内存和性能造成影响呢?
  • 如果单 Activity 多 Fragment 不能为我们创造太大的价值时,这种模式根本就不值得我们去做,因为我们最终得到的,永远抵不上付出的。

为什么没有用 ConstraintLayout 来写布局

  • 大家如果有仔细观察的话,会发现 AndroidProject 其实没有用到 ConstraintLayout 布局,在这里谈谈我对这个布局的看法,约束布局有一个优点,没有布局嵌套,所以能减少测量次数,从而提升布局绘制的速度,但是优点也是它的缺点,正是因为没有布局嵌套,View 也就没有层级概念,所以它需要定义很多 ViewID 来约束相邻的 View 的位置,就算这个 View 我们在 Java 代码中没有用到,但是在约束布局中还是要定义。这样带来的弊端有几个:

    1. 我们每次都要想好这个 ViewID 的名称叫什么,这个就有点烧脑筋了,既要符合代码规范,也要明确和突出其作用。
    2. 要考虑好每个 View 上下左右之间的约束关系,否则就会容易出现越界的情况,例如一个 TextView 设计图上有 5 个字,但是后台返回了 10 个字,这个时候 TextView 的控件宽度会被拉长,如果没有设置好右边的约束,极有可能出现遮盖右边 View 的情况。
    3. View 之间的关系会变得复杂起来,具体表现为布局一旦发生变更,例如删除或增加某一个 View,都会影响整个 ConstraintLayout 布局,因为很多约束关系会因此发生改变,并且在布局预览中就会变得错乱起来,简单通俗点来讲就是,你拆了一块瓦,很可能会导致房倒屋塌。
    4. 是我们无法直接在布局中无法直接预判这个 View 在 Java 代码中是否有使用到,因为每个 View 几乎都有定义 ID,要想知道这个 View 有没有用到,还是得在 Java 代码中找一找。
  • 我的想法是:项目里面大多数页面还是比较简单的,可以结合 LinearLayout 和 FrameLayout 布局来写,并且不需要嵌套得太深,我觉得合理的嵌套是 2~3 层,如果超过 5 层可以考虑用 ConstraintLayout 布局来写,当然这种情况在实际项目中还是比较少的。
  • 另外一个问题,就是我发现有些人写布局喜欢嵌套很多层,但是真正的情况并不是真的就需要嵌套那么多,而是这个人对这个布局的特性和属性不太熟悉而导致,正确的方式是深入学习,这样才能用好每一个布局。

为什么不拆成多个框架来做这件事

  • AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 XXPermissions,网络请求框架 EasyHttp、吐司框架 ToastUtils 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点:

    1. 和业务的耦合性高,例如 Dialog 组件引用了很多项目的基类,例如 BaseDialogBaseAdapter
    2. 业务有定制化需求,因为 Dialog 的 UI 风格要跟随项目的设计走,所以代码如果在项目中,修改起来会非常方便,如果抽取到框架中,要怎么修改和统一 UI 风格呢?我个人认为框架不适合做 UI 定制化,因为每个产品的设计风格都不一样,就算开放再多的 API 给外部调用的人设置 UI 风格,也无法满足所有人的需求。
  • 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 AndroidProject 一起更新比较好。当然像权限请求这种东西,我个人觉得抽成框架是比较合适的,因为它和业务的关联性不大,更重要的是,如果某一天你觉得 XXPermissions 做得不够好,你随时可以在 AndroidProject 替换掉它,并且整个过程不需要太大的改动。

为什么最低兼容到 Android 5

  • AndroidProject 从 v11.0 版本,已经将 minSdkVersion 从 19 升级到 21,原因也很简单,我不推荐大家兼容 Android 4.4 版本,因为这个版本兼容性的问题太多,对 dex 分包矢量图的支持不是特别好,这个我们开发者处理不了,除此之外还有很多 API 要做高低版本兼容,这个我们开发者能做,但是我觉得没什么必要性,因为这个版本的机型会越来越少,会逐步退出历史舞台,而 AndroidProject 一旦投入到项目中使用,minSdkVersion 基本不会有变动,所以我的想法是,不如在一开始就不兼容这个版本,免得后面给大家带来一些不必要的麻烦,Android 4.4 有些问题是真硬伤,这是一个非常令人头疼的问题。

为什么不加入扫描二维码功能

  • AndroidProject 的定位是做一个技术架构,不是什么都做的 Demo 工程,如果只是解决大家的需求问题,那样在我看来意义其实并不大,当然实现需求固然很重要,但并不是所有的技术点在不同项目都会用到,AndroidProject 只是在做架构的同时顺道把模板做了,如果说架构是理论,那么模板就是实践,代码写得再好,如果不实践,那么也只是纸上谈兵,又或者中看不中用。
  • AndroidProject 并不会为个人做定制,包括我自己,我可以给大家举个栗子,AndroidProject 集成了我很多自己的框架,但并不是所有我写的框架都会加入到里面去,例如多语种框架,主要原因是 App 国际化的场景并不多,大部分国内的 App 没有上架 GooglePlay,少数服从多数的原则,所以我没有加入这个框架到 AndroidProject 中,并不是框架做得不好,虽然加入会对这个框架有利,会有推广作用,但是不符合大部分人的利益,于是在大我和小我之间,我还是选择大我。这无疑是一个艰难的抉择,但是我必须得这么做。

为什么不加入 EventBus

  • EventBus 我之前其实有加入过一版,只不过在 v10.0 版本上面移除了,原因很简单,它不是一个项目的必需品,我们用 EventBus 的初衷应该是,当需求在现有的基础上实现起来比较困难或者麻烦时,我们可以考虑用一用,但是到了实际项目中,会出现很多滥用的情况出现,在这里我建议大家,能用正常方式实现通讯的,尽量不要用 EventBus 实现。另外大家如果真的有需要,可以自行加入,集成也相对比较简单。

为什么没有用 Retrofit 和 RxJava

  • 我想问大家一个问题,这两个框架搭配起来好用吗?可能大家的回答都不一致,但是我个人觉得不好用,接下来让我们分析一下 Retrofit 有什么问题:

    1. 功能太少:如果你用过 Retrofit,一说到功能这个词,我相信你的脑海中第一个想到能不能用 OkHttp 的拦截器来实现,没错常用的功能 Retrofit 都没有封装,例如最常用的功能有:添加通用请求头和参数、请求和响应的日志打印、动态 Host 及多域名配置,Retrofit 统统都没有,需要自己去实现。有时候我在想,如果 Retrofit 没有 Square 公司背书,现在应该估计不会有多少人用吧。
    2. 不够灵活:Retrofit 其实是支持上传的,但是有一个缺点,不能获取进度监听,只能获取到成功和失败。当然网上也有一些解决方案,例如通过设置拦截器,来对 RequestBody 进行二次包装来获取上传进度,但是整个实现的过程十分地麻烦,在 Retrofit 上也没有给出一个好的方案,明明可以由 Retrofit 来做的事,为什么要分发到每个开发者上面。
    3. 学习成本高:Retrofit 主要的学习成本来源于它的注解,我现在把它里面所有注解罗列出来:@Url、@Body、@Field、@FieldMap、@FormUrlEncoded、@Header、@HeaderMap、@Headers、@HTTP、@Multipart、@Part、@PartMap、@Path、@Query、@QueryMap、@QueryName、@Streaming。我们了解过多少个注解的作用?这个时候大多数人肯定会说,我都是按照别人的写法复制一遍,具体有什么作用我还真的不知道。其实这个是学习成本高带来的弊端,人们往往只会记住最常用的那几个。
  • 我感觉,大家用的不一定就是最好的,盲目地从众不是件好事,谈谈我的看法,在选用一个框架之前,我会分析它在项目实战中的优缺点,如果缺点大于优点,那是肯定不能接受的,如果优点过多,同时现有的缺点还能接受,还是可以考虑投入到项目中使用的。
  • AndroidProject 在很长的时间内都没有加入网络请求框架,是因为我还没有找到合适的网络请求框架,如果一旦加入 Retrofit,我就不得不面对它带来的各种各样的问题,例如有很多人会问你这个功能怎么实现?那个功能怎么实现?与其这样,那我为什么不自己做一个呢?
  • 但我深知做好一个网络请求框架不是一件简单的事情,从 OkGo 作者弃更的事件来看,我大概就知道了这块领域一入坑深似海,但是网络请求是一个项目必不可少的部分,想要做好 AndroidProject,那网络请求这块一定不能少。终于在经过了半年多的设计和开发,EasyHttp 在 2019 年 12 月 7 日面世了,当我兴高采烈地发布时,却发现基本没有什么热度,有很多人都说我用 Retrofit + RxJava 它难道不香吗?
  • EasyHttp 在被备受冷落的期间,我也很难受,难道半年的心血要付之东流?我重新分析了 EasyHttp 的设计,它确实是块好料,但是要做到大部分人认可还需要一段时间的打磨,所以我选择了坚守,因为我相信是金子终有一天会发光,我愿意付出大量的时间和精力来维护它。最近有一个好消息可以跟大家分享,我渐渐收到了很多关于 EasyHttp 的夸赞,都是说 EasyHttp 很好用、灵活性很高,这让我越发觉得自己做的是对的,如果没有这些肯定,我可能早就坚持不下去了。

为什么没有用 Jetpack 全家桶

  • AndroidProject 里面其实有运用到和 Jetpack 相关的技术,例如 Lifecycle 特性,在 BaseDialog 加入了此特性,不仅如此,里面引入的 EasyHttp 网络请求框架也采用了 Lifecycle 特性来管控网络请求,Lifecycle 是一个好东西,把组件的生命周期抽象化了,这样我们无需要关心这个组件是 Activity 或 Fragment,又或者是其他类型的组件。
  • 但是除了 Lifecycle 组件之外,LiveData 和 ViewModel 组件在 AndroidProject 基本没有用到,这个是因为 AndroidProject 有自己的代码设计思想,只会集成一些合适的代码库,不会一味地去追求什么全家桶,框架选型是要综合考虑很多方面的因素,并没有大家想得那么简单。

为什么不对图片加载框架进行再次封装

  • 常用的图片加载框架无非就两种,最常用的是 Glide,其次是 Fresco。我曾做过一个技术调研:

  • 无疑 Glide 已成大家最喜爱的图片加载框架,当然也有人使用 Fresco,但是占比极少。
  • 那既然萝卜白菜各有所爱,那么为什么不对图片加载框架抽取成接口呢?这样不就把所有的问题都解决了?
  • 其实 AndroidProject 10.0 之前的版本有做过这块的内容,但是移除的原因是,抽取接口其实不难,难的是后续的扩展,例如 Glide 给我们开放了很多 API,我们最常用的是加载网络图片、加载圆角图片、加载圆形图片,但是如果是其他形状的图片呢?那就要涉及到 Glide 图形变换的 API 了,还有一个就是加载监听的事件,也需要涉及到 Glide 的 API,缓存策略,不止如此,还有很多 API 都涉及到 Glide 的 API,如果直接用 Glide 来做,我们可以轻松实现,但是如果经过一层的代码封装,那么会把框架本身的灵活性给扼杀掉。但并不是不可以实现,而是没有这个必要,就算做了付出和收益也会远远不成正比,同时也会给大家带来一定的学习成本。

模板 架构 技术中台有什么区别

  • AndroidProject 正式从 安卓架构 更名为 安卓技术中台,因为它符合技术中台的特性,既能够做到快速开发,同时又能保证后续维护也能快速迭代。大家可以也将技术中台理解为:模板+架构,一般写模板代码的人做不了架构设计,而做架构设计的人又不想写模板代码,那么技术中台的概念便出现了,并且结合了这两种的优点,开发和维护都兼顾到位。

为什么不按业务来划分包名

  • 有一些业务职责不明确,无法限定属于哪一个业务模块,并且大多数模块的类都是比较少,只有少部分的模块拥有一定数量的类,所以在一般的中小项目开发中,我更推荐以类的作用来划分包名。

为什么没有关于列表多 type 的封装

  • 原生的 RecyclerView.Adapter 本身就支持多 type,只需要重写适配器的 getItemType 方法即可,具体用法不做过多介绍。

这不就是一个模板工程换成我也能写一个

  • 想把 AndroidProject 做出来并不难,我当时只花了一两个星期,而做好它需要无限的时间和精力,我花了两年多的时间仍然还在半路之上,尽管有很多人认为它很好用,没有任何 Bug,但是在我看来还不够,因为每个人衡量标准的程度不同,我的标准是随着时间的推移和技术的提升而不断提高。具体付出了多少努力,可以先让我们看一组数据

  • 与其说 AndroidProject 做的是模板工程,但实际我在架构设计上花费的时间和精力会更多,其实这两者我都有在做,因为我的目的只有一个,能够帮助大家更好地开发和维护项目。具体 AndroidProject 在代码设计上有什么亮点,这里我建议你看一下里面的代码,我相信你看完后会有收获的,后面我可能也会出一篇文章具体讲述 AndroidProject 的亮点。

假设 AndroidProject 更新了该怎么升级它

  • 原因和解释:首先纠正一点,AndroidProject 严格意义上来说,不是框架一种,而属于架构一种,架构升级本身就是一件大事,并且存在很多未知的风险点,我不推荐已使用 AndroidProject 开发的项目去做升级,因为开发和测试的成本极其高,间接能为业务带来价值其实很低,很多时候我知道大家很喜欢 AndroidProject 的代码,想用到公司项目中去,但是我仍然不推荐你那么做,假设这是你的个人项目可以那么做,但是公司项目最好不要,因为公司和你都是要靠这个项目赚钱,谁也不希望项目出现问题,如果是公司要开发人员重构公司项目,也可以考虑那么做,毕竟这个时候的风险公司已经承担了大部分了,接下来的话只需要服从公司安排即可。
  • 更新的方式:由于 AndroidProject 不是一个单独的框架那么简单,无法通过更新远程依赖的方式进行升级,所以只能通过替换代码的形式进行更新,需要注意的是,代码覆盖完需要经过严格的自测及测试,测试是做这件事情的关键流程,需要重视起来,对每一处功能进行详细测试,一定要详细,特别涉及到主流程的功能。

为什么不用谷歌 ActivityResultContracts

  • ActivityResultContract是 Activity 1.2.0-alpha02 和 Fragment 1.3.0-alpha02 中新追加的新 API,但是在此之前 AndroidProject 早已经对 onActivityResult 回调进行了封装,详情请见 BaseActivity
public abstract class BaseActivity extends AppCompatActivity {

    /** Activity 回调集合 */
    private SparseArray<OnActivityCallback> mActivityCallbacks;

    /**
     * startActivityForResult 方法优化
     */

    public void startActivityForResult(Class<? extends Activity> clazz, OnActivityCallback callback) {
        startActivityForResult(new Intent(this, clazz), null, callback);
    }

    public void startActivityForResult(Intent intent, OnActivityCallback callback) {
        startActivityForResult(intent, null, callback);
    }

    public void startActivityForResult(Intent intent, @Nullable Bundle options, OnActivityCallback callback) {
        if (mActivityCallbacks == null) {
            mActivityCallbacks = new SparseArray<>(1);
        }
        // 请求码必须在 2 的 16 次方以内
        int requestCode = new Random().nextInt((int) Math.pow(2, 16));
        mActivityCallbacks.put(requestCode, callback);
        startActivityForResult(intent, requestCode, options);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        OnActivityCallback callback;
        if (mActivityCallbacks != null && (callback = mActivityCallbacks.get(requestCode)) != null) {
            callback.onActivityResult(resultCode, data);
            mActivityCallbacks.remove(requestCode);
            return;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    public interface OnActivityCallback {

        /**
         * 结果回调
         *
         * @param resultCode        结果码
         * @param data              数据
         */
        void onActivityResult(int resultCode, @Nullable Intent data);
    }
}
  • 至于要不要换成谷歌出的那种呢?我们先来对比这两种的方式的用法
// Google 的用法
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
    @Override
    public void onActivityResult(ActivityResult result) {
        Intent data = result.getData();
        int resultCode = result.getResultCode();
    }
}).launch(new Intent(this, HomeActivity.class));

// AndroidProject 的用法
startActivityForResult(HomeActivity.class, new OnActivityCallback() {
    @Override
    public void onActivityResult(int resultCode, @Nullable Intent data) {
        
    }
});
  • 对这两种经过对比,得出结论如下:

    1. 谷歌原生的没有 AndroidProject 封装得那么人性化,谷歌那种方式调用稍微麻烦一点
    2. 谷歌那种方式直接集成进 AndroidX 包的,要比直接在 BaseActivity 中封装要好
    3. AndroidProject 封装 onActivityResult 回调至少要比谷歌要早一两年,并非谷歌之后的产物
    4. 之前使用 AndroidProject 的人群已经习惯和记忆了那种方式,所以 API 不能删也不能改
  • 所以并不是我不想用,而是谷歌封装得还不够好,至少在我看来还不够好,抛去 AndroidProject 封装的时间早不说,谷歌封装出来的效果也是强差人意,我感觉谷歌工程师的封装得越来越敷衍了,看起来像是在完成任务,而不是在做好一件事。

轮子哥你怎么看待层出不穷的新技术

  • 新东西的出现总能引起别人的好奇和尝试,但是我建议有兴趣的人可以学一下,但是如果要应用到项目中,我个人建议还是要慎重,因为纵观历史,我们不难发现,技术创新虽然很受欢迎,但是大多数都经不住时间的考验,最终一个个气尽倒下,这是因为很多新技术,表面看起来很美好,但实际上一入坑深似海。当然也有一些优秀的技术创新活了下来,但是毕竟占的是少数。
  • 谈谈我对新技术的看法,首先我会思考这种新技术能解决什么痛点,这点非常重要,再好的技术创新,也必须得创造价值,否则就是在扯淡。有人肯定会问,什么样的技术才算有价值?对于我们 Android 程序员来讲,无非就围绕两点,开发和维护。要么在前期开发上,能发挥很大的作用,要么在后续维护上面,能体现它的优势。
  • 还有谷歌的新技术不一定都是好的,也有一些是 KPI 产物,别忘了,他们也是打工的,他们也有 KPI 考核,为了年终奖和晋升,他们不得不卖力宣传,纵使他们知道这个东西有硬伤,但是他们也会推出来看看市场反应。所以我们看待一种新技术,不要太看重是否是大公司出品的,也不要太看重是哪个行业名人写的,我们应该要重点关注的是,产品的质量以及能带给我们带来哪些帮助,还有会带来哪些不好的影响,这个才是正确的技术价值观。

artifactory安装和使用

本文首发地址 https://www.jianshu.com/p/ba57e23ddc1d
最新更新地址 https://gitee.com/chenjim/chenjimblog

artifactory-pro-6.6.0 安装使用

  1. 下载 artifactory-pro-6.6.0.zip 并解压,如需文件,邮件到 me@h89.cn
    下载 artifactory-injector-1.1.jar
  2. 绿化处理,命令java -jar artifactory-injector-1.1.jar,选择2,然后需要输入artifactory解压后的目录,详细如下

    $ java -jar artifactory-injector-1.1.jar
    What do you want to do?
    1 - generate License String
    2 - inject artifactory
    exit - exit
    2
    where is artifactory home? ("back" for back)
    D:\artifactory\artifactory-pro-6.6.0/
    artifactory detected. continue? (yes/no)
    yes
    putting another WEB-INF/lib/artifactory-addons-manager-6.6.0.jar
    META-INF/
    org/
    org/jfrog/
    ...
    
  3. 生成授权License,选择1,记录生成的license,然后exit退出

    What do you want to do?
    1 - generate License String
    2 - inject artifactory
    exit - exit
    1
    eyJhcnRpZmFjdG9yeSI6eyJ......ydGllcyI6e319fQ==
    

    备注

    • 如果没有2、3的处理,不能使用全部功能,跟社区版本差不多,只能添加部分镜像
    • 此处的license在后续配置中会使用到,需要保存一下
  4. 启动 .\artifactory-pro-6.6.0\bin\artifactory.bat start
    Linux:./artifactory-pro-6.6.0/bin/artifactory.sh
    ps -ef | grep artifactory Linux下查看启动的进程信息
    出现 Artifactory successfully started表示启动成功
  5. 建议修改 artifactory-pro-6.6.0\tomcat\conf\server.xml 中 端口 808118081server.xml中的其它端口建议同步修改,避免冲突引起启动异常
  6. 浏览器打开 http://192.168.3.242:18081 进行相应的配置,包含输入步骤3生成的license
    如需修改端口更改 为相应的值即可,如18081,如果本机还有服务使用此文件的其它端口,注意更改。
  7. 菜单 Admin -> Repositories -> Remote -> NEW 选择 maven
    添加如下镜像代理

    Repository KeyURL
    aliyun_publichttps://maven.aliyun.com/repository/public
    aliyun_googlehttps://maven.aliyun.com/repository/google
    jitpackhttps://jitpack.io
  8. 菜单 Admin -> Repositories -> Virtual -> NEW 选择 maven
    Repository Key 填写 android
    RepositoriesSelected Repositories 选择上面添加的 aliyun_publicaliyun_googlejitpack
  9. 此时项目中所有 maven{ *** } 都可以替换为

       maven {
          url 'http://192.168.3.242:18081/artifactory/android'
          //gradle 7.0+  需要打开以下
          //allowInsecureProtocol = true
       }

artifactory 社区版安装使用

  1. 下载地址 https://jfrog.com/open-source/
  2. 安装使用说明
    https://www.jfrog.com/confluence/display/JFROG/Installing+Artifactory
    完整的功能列表请参考:
    https://www.jfrog.com/confluence/display/JCR/Overview
  3. 默认用户名 admin 密码 password

代理 Gradle 本地依赖代理

Gradle项目中下载首次编译,需要从 https://services.gradle.org/distributions 下载 gradle-*.zip
可能会比较慢,我们可以用 artifactory 做代理

方案一

  1. 菜单 Admin -> Repositories -> Remote -> NEW
  2. 弹出对话框 Select Package Type, 选择 Generic
  3. Repository Key 填写 distributionsURL 填写 https://services.gradle.org/distributions
  4. 保存即可

方案二

  1. 菜单 Admin --> Repositories --> Local
  2. 选择 New,选择 maven,输入 Repository Key 内容为 distributions
  3. 菜单 Artifacts 选择上一步添加的 distributions,再点右上角 Deploy
  4. 在弹出界面上传下载的 gradle-*.zip 即可

以上两个方案都可以,网络见到的都是第二种,第一种相对简单,不用每次上传本地的 gradle-*.zip

最后替换 gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=http\://192.168.3.242\:18081/artifactory/distributions/gradle-7.0.2-all.zip
即可


安卓上传 aar 到 artifactory

  1. Baselibrary/build.gradle同目录添加文件 artifactoryPublish.gradle,内容如下

    apply plugin: 'maven-publish'
    apply plugin: 'com.jfrog.artifactory'
    
    //定义artifactory仓库的地址,按照你自己的修改
    def MAVEN_LOCAL_PATH = 'http://192.168.3.242:18081/artifactory'
    
    // 定义构件的相关信息
    // 当其他项目远程依赖该构件的时候,结构类似就是 implementation 'GROUP_ID:ARTIFACT_ID:VERSION_NAME'
    def GROUP_ID = 'com.chenjim.android'
    def ARTIFACT_ID = 'andlibs'
    def VERSION_NAME = '0.0.1'
    
    publishing {
     publications {
         aar_pub(MavenPublication) {//注意这里定义的 aar_pub,在artifactoryPublish 下需要使用
             groupId = GROUP_ID
             artifactId = ARTIFACT_ID
             version = VERSION_NAME
    
             // aar文件所在的位置
             // module打包后所在路径为module模块下的build/outputs/aar,生成的aar名称为:module名-release.aar
             artifact("$buildDir/outputs/aar/${project.getName()}-debug.aar")
    
             //当有其他 dependencies api 时可能需要以下
             //参考自 https://chowdera.com/2021/03/20210305112748155c.html
    //            pom.withXml {
    //                def dependencies = asNode().appendNode("dependencies")
    //                configurations.api.allDependencies.each {
    //                    def dependency = dependencies.appendNode("dependency")
    //                    dependency.appendNode("groupId", it.group)
    //                    dependency.appendNode("artifactId", it.name)
    //                    dependency.appendNode("version", it.version)
    //                }
    //            }
         }
     }
    }
    
    artifactoryPublish {
     contextUrl = MAVEN_LOCAL_PATH
     publications('aar_pub')        //注意这里使用的是上面定义的 aar_pub
    
    
    //需要提前创建 Local Repositories,名为 jx_android   
     clientConfig.publisher.repoKey = 'jx_android'        //上传到的仓库地址
     clientConfig.publisher.username = 'jx_jfrog'        //artifactory 登录的用户名
     clientConfig.publisher.password = 'djofYSKEWC2vPh1'    //artifactory 登录的密码
    }
  2. Baselibrary/build.gradle 中添加
    apply from: "./artifactoryPublish.gradle"
  3. 根目录 build.gradledependencies 节点添加
    classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.15.2"

参考自
https://blog.csdn.net/u010976213/article/details/105710511
https://chowdera.com/2021/03/20210305112748155c.html


用artifactory做npm本地镜像

  1. 菜单 Admin --> Repositories --> Remote --> NEW
  2. 选择 npm
  3. Repository Key 填写 npm , URL 填写 https://registry.npm.taobao.org
    经过如上修改,我们就可以使用artifactory做npm的镜像了
  4. 用淘宝镜像配置为 npm config set registry https://registry.npm.taobao.org
    此配置可以替换为 npm config set registry http://192.168.3.242:18081/artifactory/npm

artifactory 上传文件大小配置

  1. 菜单 Admin --> GeneralConfiguration
  2. 更改 File Upload Max Size 内容即可

artifactory无法访问,提示401授权异常

  1. 菜单 Admin --> Security Configuration
  2. 勾选 Allow Anonymous Access ,早期的版本已默认勾选,新版本可能没有

@[toc]

安卓 webrtc 开启 h264 软编解码

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/120199702
最新更新地址 https://gitee.com/chenjim/chenjimblog
本文基于libmediasoupclient 3.2.0 和 webrtc branch-heads/4147(m84)
本文得熟悉相关基础,参考 文1文2
除了需要加编译参数 rtc_use_h264=true, 还需要以下修改以支持h264软编解码
网络搜索到的有很多复制、粘贴,缺少部分内容的,或者版本环境不匹配,很让人头疼。。。。
  1. 修改 ffmpeg_generated.gni 开启 openh264 编解码
    安卓平台默认未支持 h264 解码,会显示黑屏
    third_party/ffmpeg/ffmpeg_generated.gni 中我们可以开到,默认未支持安卓平台
    修改 ffmpeg_generated.gniuse_linux_config,添加 || is_android以支持,结果如下
    use_linux_config = is_linux || is_fuchsia || is_android
    参考自 https://www.codeleading.com/article/3215969775
    而对于软编解码,android webrtc采用openh264 +ffmpeg,这两块的代码都在webrtc源码src/third_party当中
  2. 增加 codec_list parser_list h264 支持
    third_party/ffmpeg/chromium/config/Chrome/android/arm64/libavcodec/parser_list.c 中添加 &ff_h264_parser,
    third_party/ffmpeg/chromium/config/Chrome/android/arm64/libavcodec/codec_list.c 中添加 &ff_h264_decoder,
    third_party/ffmpeg/chromium/config/下有 Chrome ChromeOS Chromium,这里用的是 Chrome,(其它可能有问题。。。)
    需要在编译参数添加 ffmpeg_branding="Chrome"
    这里路径 android/arm64 是需要配合参数 --arch "arm64-v8a",
    如果是 armeabi-v7a 需要同步修改目录 android/arm-neon/libavcodec下内容,
    最终编译参数参考:
    ./tools_webrtc/android/build_aar.py --extra-gn-args 'rtc_use_h264=true ffmpeg_branding="Chrome" rtc_enable_protobuf=false use_rtti=true use_custom_libcxx=false' --arch "arm64-v8a"
  3. 增加文件 LibH264Decoder.java
    复制 sdk/android/api/org/webrtc/LibvpxVp8Decoder.java为同目录 LibH264Decoder.java,修改后如下:

    package org.webrtc;
    public class LibH264Decoder extends WrappedNativeVideoDecoder {
      @Override
      public long createNativeVideoDecoder() {
     return nativeCreateDecoder();
      }
      static native long nativeCreateDecoder();
    }
  4. 增加文件 LibH264Encoder.java
    复制 sdk/android/api/org/webrtc/LibvpxVp8Encoder.java 为同目录 LibH264Encoder.java,修改后如下:

    package org.webrtc;
    public class LibH264Encoder extends WrappedNativeVideoEncoder {
      @Override
      public long createNativeVideoEncoder() {
     return nativeCreateEncoder();
      }
      static native long nativeCreateEncoder();
      @Override
      public boolean isHardwareEncoder() {return false;}
    }
    
  5. 增加 h264_codec.cc
    复制 sdk/android/src/jni/vp8_codec.cc 为同目录 h264_codec.cc,修改后如下:

    #include <jni.h>
    #include "modules/video_coding/codecs/h264/include/h264.h"
    #include "sdk/android/generated_libH264_jni/LibH264Decoder_jni.h"
    #include "sdk/android/generated_libH264_jni/LibH264Encoder_jni.h"
    #include "sdk/android/src/jni/jni_helpers.h"
    namespace webrtc {
    namespace jni {
    static jlong JNI_LibH264Encoder_CreateEncoder(JNIEnv* jni) {
      return jlongFromPointer(H264Encoder::Create().release());
    }
    static jlong JNI_LibH264Decoder_CreateDecoder(JNIEnv* jni) {
      return jlongFromPointer(H264Decoder::Create().release());
    }
    }  // namespace jni
    }  // namespace webrtc
  6. 修改H264 Create()
    添加上面要用到的 H264Encoder::Create(),修改 diff 如下:

    --- a/modules/video_coding/codecs/h264/h264.cc
    +++ b/modules/video_coding/codecs/h264/h264.cc
    @@ -84,6 +84,16 @@ std::vector<SdpVideoFormat> SupportedH264Codecs() {
                         "0")};
     }
    
    +std::unique_ptr<H264Encoder> H264Encoder::Create() {
  7. RTC_LOG(LS_INFO) << "Creating H264EncoderImpl.";
  8. return std::make_unique(cricket::VideoCodec("H264"));
    +#else
  9. RTC_NOTREACHED();
  10. return nullptr;
    +#endif
  11. }
    +
    std::unique_ptr H264Encoder::Create(
    const cricket::VideoCodec& codec) {
    RTC_DCHECK(H264Encoder::IsSupported());

    --- a/modules/video_coding/codecs/h264/include/h264.h
    +++ b/modules/video_coding/codecs/h264/include/h264.h
    @@ -43,6 +43,7 @@ std::vector SupportedH264Codecs();

    class RTC_EXPORT H264Encoder : public VideoEncoder {
    public:

  12. static std::unique_ptr Create();
    static std::unique_ptr Create(const cricket::VideoCodec& codec);
    // If H.264 is supported (any implementation).
    static bool IsSupported();

  13. 修改 sdk/android/BUILD.gn
    将其中 vp8 相关地方复制添加 h264 相关,我的一份修改 diff 如下,若编译有问题或者无效,需仔细核对此处。。。

    @@ -45,6 +45,7 @@ if (is_android) {
        ":java_audio_device_module_java",
        ":libjingle_peerconnection_java",
  14. ":libH264_java",

    ":libvpx_vp8_java",
    ":libvpx_vp9_java",
    ":logging_java",

    @@ -489,6 +490,20 @@ if (is_android) {
    ]
    }

  15. rtc_android_library("libH264_java") {
  16. visibility = [ "*" ]
  17. sources = [
  18. "api/org/webrtc/LibH264Decoder.java",
  19. "api/org/webrtc/LibH264Encoder.java",
  20. ]
  21. deps = [
  22. ":base_java",
  23. ":video_api_java",
  24. ":video_java",
  25. "//rtc_base:base_java",
  26. ]
  27. }
    +
    rtc_android_library("libvpx_vp9_java") {
    visibility = [ "*" ]
    sources = [
    @@ -512,6 +527,7 @@ if (is_android) {

    deps = [

    ":base_java",
  28. ":libH264_java",

    ":libvpx_vp8_java",
    ":libvpx_vp9_java",
    ":video_api_java",

    @@ -783,6 +799,18 @@ if (current_os == "linux" || is_android) {
    ]
    }

  29. rtc_library("libH264_jni") {
  30. visibility = [ "*" ]
  31. allow_poison = [ "software_video_codecs" ]
  32. sources = [ "src/jni/h264_codec.cc" ]
  33. deps = [
  34. ":base_jni",
  35. ":generated_libH264_jni",
  36. ":video_jni",
  37. "../../modules/video_coding:webrtc_h264",
  38. ]
  39. }
    +
    rtc_library("libvpx_vp9_jni") {
    visibility = [ "*" ]
    allow_poison = [ "software_video_codecs" ]
    @@ -799,6 +827,7 @@ if (current_os == "linux" || is_android) {
    visibility = [ "*" ]
    allow_poison = [ "software_video_codecs" ]
    deps = [
  40. ":libH264_jni",

    ":libvpx_vp8_jni",
    ":libvpx_vp9_jni",

    ]
    @@ -1203,6 +1232,16 @@ if (current_os == "linux" || is_android) {
    jni_generator_include = "//sdk/android/src/jni/jni_generator_helper.h"
    }

  41. generate_jni("generated_libH264_jni") {
  42. sources = [
  43. "api/org/webrtc/LibH264Decoder.java",
  44. "api/org/webrtc/LibH264Encoder.java",
  45. ]
    +
  46. namespace = "webrtc::jni"
  47. jni_generator_include = "//sdk/android/src/jni/jni_generator_helper.h"
  48. }
    +
    generate_jni("generated_libvpx_vp9_jni") {
    sources = [

    "api/org/webrtc/LibvpxVp9Decoder.java",
  49. 修改 SoftwareVideoDecoderFacoty.java 和 SoftwareVideoEncoderFacoty.java
    需要 分别注册 H.264 并添加创建 codec 的代码, 我的修改 DIFF 如下

    --- a/sdk/android/api/org/webrtc/SoftwareVideoDecoderFactory.java
    +++ b/sdk/android/api/org/webrtc/SoftwareVideoDecoderFactory.java
    @@ -26,6 +26,10 @@ public class SoftwareVideoDecoderFactory implements VideoDecoderFactory {
    @Nullable
    @Override
  50. if (codecType.getName().equalsIgnoreCase("H264")) {
  51. return new LibH264Decoder();
  52. }
    +
    if (codecType.getName().equalsIgnoreCase("VP8")) {

    return new LibvpxVp8Decoder();

    }
    @@ -45,6 +49,9 @@ public class SoftwareVideoDecoderFactory implements VideoDecoderFactory {
    List codecs = new ArrayList();

    codecs.add(new VideoCodecInfo("VP8", new HashMap<>()));
    +

  53. codecs.add(new VideoCodecInfo("H264", new HashMap<>()));
    +
    if (LibvpxVp9Decoder.nativeIsSupported()) {

    codecs.add(new VideoCodecInfo("VP9", new HashMap<>()));

    }

    --- a/sdk/android/api/org/webrtc/SoftwareVideoEncoderFactory.java
    +++ b/sdk/android/api/org/webrtc/SoftwareVideoEncoderFactory.java
    @@ -19,6 +19,9 @@ public class SoftwareVideoEncoderFactory implements VideoEncoderFactory {
    @Nullable
    @Override
    public VideoEncoder createEncoder(VideoCodecInfo info) {

  54. if (info.name.equalsIgnoreCase("H264")) {
  55. return new LibH264Encoder();
  56. }
    if (info.name.equalsIgnoreCase("VP8")) {

    return new LibvpxVp8Encoder();

    }
    @@ -38,6 +41,9 @@ public class SoftwareVideoEncoderFactory implements VideoEncoderFactory {
    List codecs = new ArrayList();

    codecs.add(new VideoCodecInfo("VP8", new HashMap<>()));
    +

  57. codecs.add(new VideoCodecInfo("H264", new HashMap<>()));
    +
    if (LibvpxVp9Encoder.nativeIsSupported()) {

    codecs.add(new VideoCodecInfo("VP9", new HashMap<>()));

    }

  58. 编译使用吧,祝好运。。。

本文参考自 webrtc M75支持android安卓H264软编解遇到的一些坑
在其基础补充、完善部分说明和详细修改结果,如有问题欢迎反馈


其它相关文档

Git配置和常用命令

Git下载地址 https://git-scm.com/downloads
本文地址 https://blog.csdn.net/CSqingchen/article/details/105674924
最新 文章连接,本文不再同步

初始配置

  1. 账号邮箱配置

    git config --global user.name chenjim
    git config --global user.email me@h89.cn

  2. alias简写配置

    git config --global alias.cp cherry-pick
    git config --global alias.co checkout
    git config --global alias.ci commit
    git config --global alias.br branch
    git config --global alias.st status

    在~/.bashrc添加gl支持
    alias gl="git log --oneline --all --graph --decorate"

  3. warning: LF will be replaced by CRLF
    mac/linux:
    git config --global core.autocrlf input
    windows:
    git config --global core.autocrlf true(默认情况) 详细配置说明 https://www.jianshu.com/p/0a747b2b76a2
  4. windows中文名称、路径变成xx%解决
    git config core.quotepath false
  5. git代理配置
  • 全局git代理配置
    git config --global http.proxy socks5://127.0.0.1:1080
  • 只github.com 使用代理
    git config --global http.https://github.com.proxy socks5://127.0.0.1:1080
  • 移除代理配置
    git config --global --unset http.proxy
    git config --global --unset http.https://github.com.proxy
  1. 生成ssh的key命令
    ssh-keygen 直接回车
  2. 查看当前用户的配置
    vi ~/.gitconfig 或者git config -l
  3. 长期存储密码
    git config --global credential.helper store
    或者将账号密码存储在http url
    git remote rm origin
    git remote add origin http://yourname:password@git.oschina.net/name/project.git
  4. 设置文本编辑器
    git config --global core.editor vi
  5. 配置是否忽略文件权限
    git config --global core.filemode false
    windows下:false不忽略可执行权限;true忽略。Linux下相反。
    windows下对文件gradlew增加可执行权限
    git add --chmod=+x gradlew 增加+x;移除-x
    或者git update-index --chmod=+x gradlew
  6. windows 不忽略文件大小写,默认忽略大小写。
    git config --global core.ignorecase false
    git config core.ignorecase false

创建commit模板

  1. 建立~/.git/template文件,内容如下
    OverView: (简单的修改描述)
    Bug ID: (项目名称,bug号:例如name12345,添加新功能bug为0)
    Description: (bug标题,修改的详细描述)
  1. 设置模板
    git config --global commit.template ~/.git/template
  2. gerrit commit 提交
    git commit -s 会自动打开模板,填好save就行了,git会自动提交

    • gerrit 提交到master
      git push origin HEAD:refs/for/master
    • gerrit 审核不通过,再次commit
      git commit -s --amend
    • 对于git项目为了在提交时自动产生change-id,
      scp -P 29418 -p username@10.1.11.10:/hooks/commit-msg .git/hooks
      scp -P 29418 -p chenjim@review.putao.io:hooks/commit-msg .git/hooks/

ssh的配置文件的使用

  • 创建配置文件

    $ cd ~/.ssh
    $ touch config
    $ vi config
  • 输入以下文件内容

    Host ha
    HostName review.putao.io
    User chenjim
    Port 29418
    IdentityFile ~/.ssh/id_rsa
  • 完成后,下面写法结果等效
    git clone ssh://chenjim@review.putao.io:29418/PTlauncher
    git clone ssh://ha/PTlauncher
  • 使用
    repo init -u ssh://ha/t700_v5.1/manifest
    repo sync

创建新git仓库并push到远程:

  mkdir GitCamera
  cd GitCamera
  git init
  touch README.md
  git add README.md
  git commit -m "first commit"
  git remote add origin  https://git.oschina.net/chenjim/GitCamera.git
  git push -u origin master

push已有项目到远程仓库

cd existing_git_repo
git remote add origin  https://git.oschina.net/chenjim/GitCamera.git
git push -u origin master

查看、添加、提交、删除、找回,重置修改文件

git help 显示command的help
git show 显示最后一次提交的内容
git show de0d8f066ace 显示commit id 为 de0d8f066ace 的提交修改的内容
git co . 抛弃工作区修改
git add . 将所有修改过的工作文件提交暂存区
git rm abc/abc.java 从版本库中删除文件
git reset abc/abc.java 从暂存区恢复到工作文件
git reset . 从暂存区恢复到工作文件
git reset --hard 恢复最近一次提交过的状态,即放弃上次提交后的所有本次修改
git reset HEAD~1 --hard 销毁最后一次本地提交
git ci -am "some comments" 将git add, git rm和git ci等操作都合并在一起
git ci --amend 修改最后一次提交记录
git revert <$id> 还原某次提交,会创建一个新的commit
git revert HEAD 恢复最后一次提交,会创建一个新的commit


git stash 的使用

git stash 缓存当前所有修改
git stash list 列出当前所有的缓存
git stash pop 应用最近一次的缓存,并删除当前缓存
git stash show -p stash@{0} 显示第0个缓存的详细修改,无-p,只显示修改的文件列表
git stash clear 清除所有的缓存


查看文件diff

git diff 比较当前文件和暂存区文件差异
git diff <id1> <id2> 比较两次提交之间的差异
git diff dev main 在两个分支之间比较
git diff --staged 比较暂存区和版本库差异
git diff --stat HEAD~1 仅比较跟上一次修改的统计信息


查看提交记录

git log . 查看当前目录所有的提交记录
git log --stat 查看提交统计信息
git log --stat --since=3.weeks 查看最近三周提交统计信息
git log --stat --since="2018-06-02" 查看日期"2018-06-02"之后的提交统计信息
git log --pretty=oneline -4 显示最近4次修改记录,包含修改内容
git log -p da28c5581 显示某次详细修改内容
git log -p -4 显示最近4次修改记录,包含修改内容
git log -4 显示最近4次修改记录,只主要信息,不包含修改内容
git log -4 --name-status 显示最后4次修改牵扯到的文件
git log --author="jim.chen" 显示某个作者的log
git show da28c5581 显示某某次修改的内容


Git 本地分支管理--查看、切换、创建和删除分支

注意br 为 branch 的 alias 简写
git br --all 查看所有本地和远程分支名称
git br -r 查看远程分支
git br dev 基于当前HEAD创建新的分支dev
git br -v 查看各个分支最后提交信息
git br --merged 查看已经被合并到当前分支的分支
git br --no-merged 查看尚未被合并到当前分支的分支
git co dev 切换到已经存在的dev分支
git co -b dev 基于当前HEAD,创建新的分支dev,并且切换到新的分支
git co $id 切换代码到某次提交记录,无分支信息,切换到其他分支会自动删除
git co -b dev origin/master 把远程的master迁出到本地分支dev
git co -b debug 0788dee 基于提交0788dee切换到新的debug分支
git br -d dev 删除dev本地分支
git br -D dev 强制删除dev分支 (未被合并的分支被删除的时候需要强制)

分支 master 设置为跟踪来自 origin 的远程分支 master。
git branch --set-upstream-to=origin/master master
或者
git branch --set-upstream master origin/master
git checkout -b master remotes/origin/master

当前HEAD所在分支的名称
git rev-parse --abbrev-ref HEAD


分支合并和rebase

git merge dev 将dev分支合并到当前分支
git merge origin/main --no-ff 不要Fast-Foward合并远程main分支,这样可以生成merge提交
git rebase master 将当前分支 rebase 到master节点之后
Git补丁管理(方便在多台机器上开发同步时用)
git diff > ../sync.patch 生成补丁
git apply ../sync.patch 使用补丁
git apply --check ../sync.patch 测试补丁能否成功
patch -p1 < ../sync.patch 使用patch命令打补丁


Git暂存管理

git stash 暂存
git stash pop 恢复暂存的内容
git stash list 列所有stash
git stash apply 恢复暂存的内容
git stash drop 删除暂存区
git stash clear 清除所有暂存


Git远程分支管理

git pull 抓取远程仓库所有分支更新并合并到本地
git pull --no-ff 抓取远程仓库所有分支更新并合并到本地,不要快进合并
git pull --rebase 抓取远程分支到本地,将本地修改rebase远程最新后
git fetch origin 抓取远程仓库更新
git merge origin/master 将远程主分支合并到本地当前分支
git push push 所有分支
git push origin dev 将本地dev分支推到远程dev分支
git push -u origin main 将本地main分支推到远程(如无远程主分支则创建,用于初始化远程仓库)
git push origin :dev 删除远程分支dev
git push github HEAD:dev --force 将当前HEAD强制推送到dev分支,github 参见下一节 'Git远程仓库管理'


Git远程仓库管理

git remote -v 查看远程服务器地址和仓库名称
git remote show origin 查看远程服务器仓库状态
git remote add origin git@gitee.com:chenjim/thirdPartyJniSo.git 添加远程仓库地址
git remote add github git@github.com:chenjim/thirdPartyJniSo.git 添加远程仓库地址
git remote set-url origin git@gitee.com:chenjim/thirdPartyJniSo.git 设置远程仓库地址(用于修改远程仓库地址)
git remote rm 删除远程仓库地址
git remote prune origin 删除已删除的分支(同步本地远程分支)


创建远程仓库

git clone --bare thirdPartyJniSo git@gitee.com:chenjim/thirdPartyJniSo.git 用带版本的项目创建纯版本仓库
scp -r thirdPartyJniSo git@git.csdn.net:~ 将纯仓库上传到服务器上
mkdir robbin_site.git && cd robbin_site.git && git --bare init 在服务器创建纯仓库
git remote add origin git@gitee.com:chenjim/thirdPartyJniSo.git 设置远程仓库地址
git push -u origin master 客户端首次提交
git push -u origin develop 首次将本地develop分支提交到远程develop分支,并且track
git remote set-head origin master 设置远程仓库的HEAD指向master分支


tag 使用

创建带有说明的标签,用-a指定标签名,-m指定说明文字:
git tag -a VV1.0 -m "version 1.0 released push url" d5a65e9
git show V1.0 查看标签V1.0信息
git tag V1.0 给当前commit添加tag V1.0
git tag V1.0 471fd27 给指定commit 471fd27 添加tag
git push origin V1.0 将指定tag推送到远程
git push --tags 将所有tag推送到远程
git push origin --tags 将所有tag推送到远程
git tag -d V1.0 删除本地tag
git push origin :V1.0 删除远程tag
git push origin --delete tag V1.0 删除远程tag
git ls-remote --tags origin 查询远程tags

Git 如何同步本地tag与远程tag

  • 问题场景:
    同事A在本地创建tagA并push同步到了远程
    ->同事B在本地拉取了远程tagA(git fetch)
    ->同事A工作需要将远程标签tagA删除
    ->同事B用git fetch同步远端信息,git tag后发现本地仍然记录有tagA
  • 分析:
    对于远程repository中已经删除了的tag,
    即使使用git fetch --prune,甚至"git fetch --tags"确保下载所有tags,也不会让其在本地也将其删除的。
    而且,似乎git目前也没有提供一个直接的命令和参数选项可以删除本地的在远程已经不存在的tag
  • 解决方法:
    git tag -l | xargs git tag -d #删除所有本地tag分支
    git fetch origin --prune #从远程拉取所有信息

reflog 使用

reflog是Git操作的一道安全保障,它能够记录几乎所有本地仓库的改变。
包括所有分支commit提交,已经删除(其实并未被实际删除)commit都会被记录。
总结:只要HEAD发生变化,就可以通过reflog查看到。

git reflog 查看最近的git变化
git checkout HEAD@{90} 将指定的变化迁出


**原创文章,转载请注明出处、原文链接!
邮件 mailto:me@h89.cn ,主页 https://chenjim.com**


相关链接

@[toc]

安卓mediasoup输出H264流(支持H264编码)

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/120218832
最新更新地址 https://gitee.com/chenjim/chenjimblog
首先得让mediasoup支持H264编解码,参见 前文

默认视频编码是VP8源码分析

相关源码流程、注释如下

//文件 RoomClient.java 中 
@WorkerThread
private void joinImpl() {
    mMediasoupDevice = new Device();
    //从服务端获取编解码能力
    String routerRtpCapabilities = mProtoo.syncRequest("getRouterRtpCapabilities");
    //最终会调用到 libmediasoupclient/src/Device.cpp 中  Device::Load 
    mMediasoupDevice.load(routerRtpCapabilities);
}

//文件 libmediasoupclient/src/Device.cpp 中 
void Device::Load(json routerRtpCapabilities, const PeerConnection::Options* peerConnectionOptions){
  ...
  //将设备编码能力和服务端编码能力匹配
  this->extendedRtpCapabilities = ortc::getExtendedRtpCapabilities(nativeRtpCapabilities, routerRtpCapabilities);
  ...
}

//最终视频编码方案代码在  文件 libmediasoupclient/src/Transport.cpp 中 
SendTransport::SendTransport(...){
    ...
    auto sendingRtpParametersByKindV= ortc::getSendingRtpParameters("video", *extendedRtpCapabilities);
}

//查看 libmediasoupclient/src/ortc.cpp 中 getSendingRtpParameters 我们可以看到如下
json getSendingRtpParameters(const std::string& kind, const json& extendedRtpCapabilities){
  ...
  for (const auto& extendedCodec : extendedRtpCapabilities["codecs"]){
    ...
    //找到一个编码器,就 break,跳出了
    // NOTE: We assume a single media codec plus an optional RTX codec.
        break;
  }
  ...
}

修改支持H264编码

通过上节分析,我们需要使 routerRtpCapabilities"mimeType": "video/H264", 字段靠前
可以在 RoomClient.java 中修改,也可以在 Device.cppDevice::Load 修改
以下是用后者的修改方案,只保留H264,参考自 mediasoup支持h264

--- a/src/Device.cpp
+++ b/src/Device.cpp
@@ -55,9 +55,25 @@ namespace mediasoupclient
      if (this->loaded)
              MSC_THROW_INVALID_STATE_ERROR("already loaded");

+     auto &remoteCaps = routerRtpCapabilities["codecs"];
+     std::string mimeTypeH264 = "video/H264";
+     std::transform(mimeTypeH264.begin(), mimeTypeH264.end(), mimeTypeH264.begin(), ::tolower);
+     for (nlohmann::json::iterator itr=remoteCaps.begin(); itr!= remoteCaps.end();) {
+         nlohmann::json tmp = *itr;
+         std::string tmpCodec = tmp["mimeType"];
+         std::transform(tmpCodec.begin(), tmpCodec.end(), tmpCodec.begin(), ::tolower);
+         if (tmpCodec != mimeTypeH264 && tmp["kind"] == "video") {
+             itr = remoteCaps.erase(itr);
+         } else {
+             itr++;
+         }
+     }
+
      // This may throw.
      ortc::validateRtpCapabilities(routerRtpCapabilities);

到这里,SDP已经支持H264编码,最终还得依赖设备的H264编码能力,参考 前文

我们可以在 https://v3demo.mediasoup.org 看到设备推出流的编码信息,如下图


其它相关文档