注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Dart中async和async*有什么区别?

水文我在行 在Dart中两个关键字,长得很像async和async*,可能还有的朋友还不知道他们两个有什么区别。现在简单介绍一下。 简单答案 简单回答这个问题就是: async返回Future. async*返回Stream. async async不必多...
继续阅读 »

水文我在行


在Dart中两个关键字,长得很像asyncasync*,可能还有的朋友还不知道他们两个有什么区别。现在简单介绍一下。


简单答案


简单回答这个问题就是:



  • async返回Future.

  • async*返回Stream.


async


async不必多言,有了解的都知道这是异步调用。
当一个函数被标记成async的时候,意味这个方法可能要从事耗时工作,比如说网络请求、处理图片等等。被async标记的方法会把返回值用Future包裹一下。


Future<int> doSomeLongTask() async {
await Future.delayed(const Duration(seconds: 1));
return 42;
}

我们可以通过await来获取Future里的返回值:


main() async {
int result = await doSomeLongTask();
print(result); // 等待一分钟后打印 '42'
}

async*


async*比多了一个*,加上*其实是函数生成器的意思。
async*标记的函数会在返回一组返回值,这些返回值会被包裹在Stream中。async*其实是为yield关键字发出的值提供了一个语法糖。


Stream<int> countForOneMinute() async* {
for (int i = 1; i <= 60; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}

上面的其实就是异步生成器了。我们可以使用yield替代return返回数据,因为这个是时候我们的函数还在执行中。
此时,我们就可以使用await for去等待Stream发出的每一个值了。


main() async {
await for (int i in countForOneMinute()) {
print(i); // 打印 1 到 60,一个秒一个整数
}
}

应用


初一看,好像并没有什么用。因为自从我使用Flutter以来,我几乎没有使用过async*。但是现在假使我们有这样的一个需求,我们需要每一秒钟请求一次接口,一共请求10次,来看看京东还剩多少茅台。


首先看看使用async的代码:


  getMaoTai() async{
for (int i = 0; i <10; i++){
await Future.delayed(Duration(seconds: 1), ()async {
MaoTaiData data = await fetchMaoTaiData();
setState(){
//更新UI
};
});
}

上面的代码里使用了循环,然后每一秒钟请求依次接口,返回数据后调用setState()更新UI。这样做会导致你每隔一两秒就setState()一次,如果不怕性能问题,不怕产品经理打你,你这么玩玩。这个时候async*就应该上场了:


    Stream<MaoTaiData> getData() async* {
for (int i = 0; i <10; i++) {
await Future.delayed(Duration(seconds: 1));
yield await fetchMaoTaiData();
}
}

这样我们就可以使用StreamBuilder包裹下Widget,就不必每次都去setState()了。


作者:JarvanMo
链接:https://juejin.cn/post/6933201564794355726
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter + Rust 高性能的跨端尝试

稍作配置,同一份代码横跨 Android & IOS,相比于 React Native 方案更加高性能。除此之外,得益于 Rust 跨平台加持,Rust 部分的代码可在种种场合复用。 这篇文章旨在记录作者尝试结合 Rust 和 Flutter 的过程,...
继续阅读 »

稍作配置,同一份代码横跨 Android & IOS,相比于 React Native 方案更加高性能。除此之外,得益于 Rust 跨平台加持,Rust 部分的代码可在种种场合复用。


这篇文章旨在记录作者尝试结合 Rust 和 Flutter 的过程,且仅为初步尝试。不会涉及诸如:



  • 如何搭建一个 Flutter 开发环境,以及 Dart 语言怎么用

  • 如何搭建一个 Rust 开发环境,以及 Rust 语言怎么学


Environment



  • Flutter: Android, IOS 工具配置妥当
    -w672

  • Rust: Stable 就好
    -w513


Rust Part


Prepare cross-platform toolchains & deps


IOS


# Download targets for IOS ( 64 bit targets (real device & simulator) )
rustup target add aarch64-apple-ios x86_64-apple-ios

# Install cargo-lipo to generate the iOS universal library
cargo install cargo-lipo

Android


这里有一些行之有效的辅助脚本用于更加快捷配置交叉编译工具。




  1. 获取 Android NDK


    sdkmanager --verbose ndk-bundle

    如果已经准备好了 Android NDK ,则设置环境变量 $ANDROID_NDK_HOME


    # example:
    export ANDROID_NDK_HOME=/Users/yinsiwei/Downloads/android-ndk-r20b


  2. Create the standalone NDK


    # $(pwd) == ~/Downloads
    git clone https://github.com/kennytm/rust-ios-android.git
    cd rust-ios-android
    ./create-ndk-standalone.sh


  3. 在 Cargo default config VS 配置 Android 交叉编译工具


    cat cargo-config.toml >> ~/.cargo/config

    执行上述命令后会在 Cargo 默认配置中,增加有关 Android 跨平台目标 (targets, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android) 的工具信息,指向刚刚创建的 standalone NDK


    [target.aarch64-linux-android]
    ar = ...
    linker = ..

    [target.armv7-linux-androideabi]
    ...

    [target.i686-linux-android]
    ..


  4. 下载 Rust 支持 Android 交叉编译的依赖





rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
```


Start a simple rust library




  1. 创建一个 Rust 项目





cargo init my-app-base --lib
```




  1. 编辑 Cargo.toml 修改 crate-type





[lib]
name = "my_app_base"
crate-type = ["staticlib", "cdylib"]
```
Rust 构建出来的二进制库,在 IOS 中是静态链接进最终的程序之中,需要对构建 staticlib 的支持;在 Android 是通过动态链接在运行时装在进程序运行空间的,需要对构建 cdylib 的支持。




  1. 写一些符合 C ABI 的函数 src/lib.rs


    use std::os::raw::c_char;
    use std::ffi::CString;

    #[no_mangle]
    pub unsafe extern fn hello() -> *const c_char {
    let s = CString::new("world").unwrap();
    s.into_raw()
    }

    在上述代码中,每次当外部调用 hello 函数时,会在晋城堆空间中创建一个字符串 ( CString ),并将所有权 ( 释放该字符串所占堆空间的权利 ) 移交给调用者




Build libraries


# IOS
cargo lipo --release

# Android
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

然后在 target 目录下会得到以下有用的物料。


target
├── aarch64-linux-android
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── armv7-linux-androideabi
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── i686-linux-android
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── universal
│   └── release
│   └── libmy_app_base.a

至此, Rust 部分就告于段落了。


Flutter Part


Copy build artifacts to flutter project


from: target/universal/release/libmy_app_base.a 
to: ios/

from: target/aarch64-linux-android/release/libmy_app_base.so
to: android/app/src/main/jniLibs/arm64-v8a/

from: target/armv7-linux-androideabi/release/libmy_app_base.so
to: android/app/src/main/jniLibs/armeabi-v7a/

from: target/i686-linux-android/release/libmy_app_base.so
to: android/app/src/main/jniLibs/x86/

Call FFI function in Dart




  1. 添加依赖


    pubspec.yaml -> dev_dependencies: += ffi: ^0.1.3




  2. 添加代码


    (直接在生成的项目上修改,暂不考虑代码设计问题,就简简单单的先把项目跑起来 )


    import 'dart:ffi';
    import 'package:ffi/ffi.dart';

    // ...
    final dylib = Platform.isAndroid ? DynamicLibrary.open('libmy_app_base.so') :DynamicLibrary.process();
    var hello = dylib.lookupFunction<Pointer<Utf8> Function(),Pointer<Utf8> Function()>('hello');

    // ...
    hello();
    // -> world


Build Android Project


flutter run # 如果连接着 Android 设备就直接运行了起来

Build IOS Project


( 复杂了许多 )



  1. 跟随 Flutter 官方文档,配置 XCode 项目。

  2. Build PhasesLink Binary With Libraries 添加 libmy_app_base.a 文件
    (按照图上箭头点...)
    -w1140

  3. Build SettingsOther Linker Flags 中添加 force_load 的参数。
    -w855


这是由于在 Dart 中通过动态的方式调用了该库的相关函数,但在编译期间静态分析的时候,这些都是未曾被调用过的无用函数,就被剪裁掉了。要通过 force_load 方式解决这个问题。


Result


2020-02-15 12.39.59-w300


ezgif-6-785f61b1b53b


Troubleshooting


XCode & IOS


Error getting attached iOS device: ideviceinfo could not find device


sudo xattr -d com.apple.quarantine ~/flutter/bin/cache/artifacts/libimobiledevice/ideviceinfo

将后面的路径替换成你的


dyld: Library not loaded


dyld: Library not loaded: /b/s/w/ir/k/homebrew/Cellar/libimobiledevice-flutter/HEAD-398c120_3/lib/libimobiledevice.6.dylib
Referenced from: /Users/hey/flutter/bin/cache/artifacts/libimobiledevice/idevice_id
Reason: image not found

删除&重新下载


rm -rf /Users/hey/flutter/bin/cache && flutter doctor -v

真机无法启动 Flutter 程序


参见 github.com/flutter/flu…
不要升级到 IOS 13.3.1 系统


What's next




  • 如何高效的实现 Rust & Dart 部分的通信


    我们知道 Flutter 和广大 GUI 库类似,属于单线程模型结合事件系统,因此在主线程中使用 FFI 调用 Rust 部分的代码不能阻塞线程。Dart 语言提供 async/await 语法特性用于在 Flutter 中处理网络请求等阻塞任务。而 Rust 也在最近版本中提供了 async/await 语法支持,如何优雅的把两部分结合起来,这是一个问题。




  • 对 MacOS Windows Linux 桌面端的支持


    Flutter 已经有了对桌面端的实验性支持,可以研究下如何结合在一起,实现跨 6 个端共享代码。




References



作者:Heymind
链接:https://juejin.cn/post/6844904065567031310
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

起初Jetpack Navigation把我逼疯了,可是后来真香

1. Navigation到底该如何正确的使用 相信大家对 Navigation都有所耳闻,我不细说怎么用了,官方的讲解也很详细。我是想说一下到底该如何更好的使用这个组件。 这个组件其实是需要配合官方的MVVM架构使用的,ViewModel+LiveData结...
继续阅读 »

1. Navigation到底该如何正确的使用


相信大家对 Navigation都有所耳闻,我不细说怎么用了,官方的讲解也很详细。我是想说一下到底该如何更好的使用这个组件。


这个组件其实是需要配合官方的MVVM架构使用的,ViewModel+LiveData结合才能更好的展现出Navigation的优势。


在官方的讲解示例中没有用到ViewModelLiveData,官方只是演示了Navigation怎么用怎么在页面之间传值,和这个组件的一些特性之类的。但真正用好还是要结合ViewModelLiveData


2. Navigation大家都以为的缺陷


起初我用Navigation的时候,最头疼的是当按下返回键回到上个页面的时候整个页面被重建了,这是开发中不想要的结果,很多时候大家都会去寻求一种方式:将官方的replace方式替换为HideShow。起初也是想到这个方式,然后结合在网上得到的资料自己写了一个方式FragmentNavigatorHideShow


3. 然而这不是缺陷


但是很快啊,我发现这个方式(HideShow)存在严重的逻辑问题。



这里可以看到,有一些场景下,我们有某个页面可以打开和自己相同的页面,只不过是展示的数据不同而已。当我用hideshow的方式展示下个页面的时候,会发现打开的还是上个页面。当按下返回键之后,上个相同的页面不见了,新打开的页面和上个页面尽然是同一个对象,这肯定不符合业务逻辑。于是我又开始研究起replace的方式,当然我在使用这个Navigation的时候就采用了MVVM + ViewModel+LiveData,这时候我想起ViewModel是不受Fragment重建影响的。于是我打印了一下在使用replace方式下页面生命周期的变化。


HomeFragment进入MyFragment生命周期变化:


变化Log


可以看到,在replace之后HomeFragment并没有执行onDestory而是执行了onDestoryView这也使得页面必须要重建。而onDestoryView不会导致 ViewModel的销毁。也就是说 ViewModel还在,ViewModel中的LiveData所保存的数据也是存在的。当我按下返回键,重新回到HomeFragment页面理所当然的执行了onViewCreated,此时代码中页面对ViewModel中的LiveData所观察数据又重新进行了observe观察,因为LiveData之前保存过数据所以这段代码也理所当然的被执行了。页面上也重新填充了数据。


    override fun initLiveData() {
viewModel.liveData.observe(this) {
Log.d(TAG, "data change : $it ")
textView.text = it
}
}

这个时候,你会发现,页面好像没有重建一样。我这才理解了谷歌的用意。它这步棋下的很巧啊。


也里所当然的我抛弃了FragmentNavigatorHideShow,又拥抱回了谷歌爸爸。


说回上面那个问题,当一个页面中可以打开自己的时候,在FragmentNavigator源码中只要是导航到下一个目的地就会重新创建一个新的Fragment,上一个Fragment会被加入回退栈里,所以才可以在Fragment中打开一个新的自己,来展示不同的信息。而hideshow的方式会每次都去查找之前有没有创建过这个页面,如果有,就Show,如果没有就创建。所以才会导致自己打开自己,永远都是同一个Fragment对象。


4. 那么到底该如何正确使用


到底该如何正确使用Navigation,这也是我这段时间使用的一点点经验。


Fragment中的所有动态数据都由ViewModel中的LiveData保存。我们只监听LiveData的数据变化,这也符合MVVM 的架构麻,当然还有一个Model我没说Repository,这个我就不解释了。



Fragment之间传递的数据都交给Bundle页面重建的时候这些数据也会被保存,再次走一遍从Bundle中取数据的过程是完全不会报错的。所以页面上的数据不会被丢失了,而像RecyclerView,ViewPager之类的控件它们也会保存自己之前的状态,页面重建后,RecyclerView,ViewPager会记录自己滑动的位置的,这个不用担心,还有一点就是有一些控件,比如CoordinatorLayout你可能需要给它和它的子View控件一个Id才能保存滑动状态。


遵循这样的一个规则之后呢,就可以忽略这个页面重建的问题了。


5. Navigation的页面转场动画的一些问题


用过Navigation的都知道,页面转场动画要一个一个的添加,就像这样:


<!--这是官方的Demo示例-->
<fragment
android:id="@+id/title_screen"
android:name="com.example.android.navigationsample.TitleScreen"
android:label="fragment_title_screen"
tools:layout="@layout/fragment_title_screen">
<action
android:id="@+id/action_title_screen_to_register"
app:destination="@id/register"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
<action
android:id="@+id/action_title_screen_to_leaderboard"
app:destination="@id/leaderboard"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
</fragment>

每一个标签都要写一遍一样的代码,让我很头疼。于是我还是想到了,重写FragmentNavigator将所有的增加一个判断如果标签中没有设置专场动画,那么我就给这个Fragment添加上专场动画。


      	//我一开始设想的载源码位置处添加的动画操作
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : 动画id;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : 动画id;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : 动画id;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : 动画id;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}

然而我太天真了,我们想到的,谷歌爸爸都考虑过了。因为如果像我一样天真的加上这样的判断之后,你会发现,第一个默认Fragment也拥有了动画属性。而且做隐式链接跳转的时候,这个动画会非常影响观感。所以第一个默认Fragment不能有转场动画。当然后来我想到了判断返回栈是否存在为空,通过这个判断是否是第一个页面。但是我都能想到谷歌爸爸肯定也想到了。他们不这么做肯定是有原因的吧。还是等待官方优化,于是我放弃了,老老实实的挨个复制粘贴,


不过后来我在Navigationissues 找到了这个问题,因该在优化的计划中吧。


6. Replace在重建Fragment的时候,过度动画卡顿


在使用 Navigation的时候,按下返回键回到上个页面,页面重建,这个时候会发现过度动画会有那么几百毫秒卡那么一下,一个转场动画也就400毫秒左右,卡那么一下效果是非常明显的。这也归功于Fragment重建的原因了,页面展示的数据量巨大的时候,重建时的绘制工作量也是相当的大,所以肯定会卡那么一下下啦。


后来我发现了一个方法:


    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
return super.onCreateAnimation(transit, enter, nextAnim)
}

我们可以把数据加载的过程放在动画执行之后再请求。


    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (enter) {
if (nextAnim > 0) {
val animation = AnimationUtils.loadAnimation(requireActivity(), nextAnim)
animation.setAnimationListener(object : Animation.AnimationListener {

override fun onAnimationEnd(animation: Animation?) {
onEnterAnimEnd()//动画结束后再去请求网络数据、或者初始化LiveData
}

})
return animation
} else {
onEnterAnimEnd()
}
} else {
if (nextAnim > 0) {
return AnimationUtils.loadAnimation(requireActivity(), nextAnim)
}
}
return super.onCreateAnimation(transit, enter, nextAnim)
}

/**
* 子类重写,判断是否需要加载数据,或者初始化LiveData
*/
fun onEnterAnimEnd(){
Log.d(TAG, "onEnterAnimEnd: ")
}

然后我们再找到onViewCreated方法,因为Base类我们通常会将初始化方法进行抽象所以我们要进行两个事情:


1: 在View进行绘制初始化的时候暂停过场动画


2: 在View与Data初始化结束后再开始动画的执行



override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//暂停过场动画
postponeEnterTransition()
//View与数据初始化
initViewAndData(view)
initLiveData()//LiveData的初始化可以放到动画结束之后
//最后使用这个方法监听视图结构,并开始执行过场动画
(view.parent as? ViewGroup)?.apply {
OneShotPreDrawListener.add(this){
startPostponedEnterTransition()
}
}
}

这样操作之后就可以预防由于RecyclerView大量数据加载时导致的过场动画掉帧问题了,但是也不是完全不掉帧,不过这个解决办法还是有效的,剩下的就是优化自己代码了,防止做太多的耗时操作。


我以前的解决办法是将过场动画进行延时100毫秒执行,但这个方式,我自己也是不满足,于是还是翻阅官方的文档,查找解决办法,掘友也给我提出过相关的问题,这段时间不忙了,所以重新修改一下以前自己犯的问题。


其他推荐


《LiveData巧妙封装,我再也不怕Navigation重建Fragment啦!》对这篇文章进行了LiveData的使用补充


作者:年小个大
链接:https://juejin.cn/post/6925331537399382030
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

(转载)5G、元宇宙和被重新定义的社交出海

【融云全球互联网通信云】[疫情突发,人们的社交生活被重新定义。熬过“孤独”的后疫情时代,海外市场线上社交需求不断增长,社交玩法与场景也逐渐多元。](WICC 2021 全球互联网通信云大会-广州站 预约报名-融云活动-活动行 (huodongxing.com)...
继续阅读 »

【融云全球互联网通信云】[疫情突发,人们的社交生活被重新定义。熬过“孤独”的后疫情时代,海外市场线上社交需求不断增长,社交玩法与场景也逐渐多元。](WICC 2021 全球互联网通信云大会-广州站 预约报名-融云活动-活动行 (huodongxing.com))

作为安全、可靠的互联网通信云服务商,全球通信云服务商中的佼佼者,“融云”是如何看待风云莫测的海外市场环境变化的?又是如何应对后疫情时代的通信需求?

近期,在出海赛道行业媒体【扬帆出海】的专访中,融云联合创始人兼 CTO 杨攀分享了“新航海时代”融云的思考与实践。

以下为访谈实录。

图片

疫情下的出海趋势

扬帆出海记 者: 能否请您介绍一下,疫情爆发前后,全球通信产业、社交业务发生了怎样的变化?

融云杨攀: 海内外市场在疫情影响下,发展方向不甚相同。

国内市场受疫情倒逼,明显的趋势是 “云经济” 崛起,各种线下服务都搬到了线上去做,借机进行了数字化转型,发展势头迅猛。

而海外市场,在漫长的疫情周期里,“居家抗疫”成为主流,满足人们日常精神需求和情感连接的在线社交娱乐类产品,逐渐成为社会的刚需。而这恰巧也是我国目前出海的重要品类,借此迎来了新一轮的爆发。

自 2016 年移动互联网浪潮崛起,中国企业尤其活跃。在我国互联网企业出海初始阶段,出海应用多是与地方文化关系不大的工具类 APP,后来由于出海赛道变宽,社交和游戏品类紧随其后,逐渐占领市场份额。

早期,还只有以直播为主的单一类型社交应用出海;时过境迁,如今国内各种各样新奇的玩法,如 1v1、语聊房等,都已经成为出海市场中非常火热的产品类型。

“新大陆”在哪里

*扬帆出海记者: *海外不同区域市场之间,是否也呈现差异化的发展?我们能看到哪些市场机会呢?

融云杨攀: 经过一定时间的摸索验证,以及出海策略的调整优化,国内绝大多数移动互联网产品,都已经把目光从美欧发达市场,转移到了那些较中国互联网发展稍落后的国家和地区,通过先进的产品和领先的商业逻辑领跑整个海外市场。

从地区差异角度看:从东南亚到印度、到中东、非洲、南美,在时间线上依次落后几年。 目前我们能看到的主要趋势是,刨除受国际关系影响的印度市场,东南亚、中东是中国应用出海最热的两个地区,其次就是非洲和南美。

东南亚因为距离中国很近,成为中国互联网出海品类最全、业务覆盖最广的区域,国内各种各样的出海品类在东南亚都能看得到,包括社交游戏、电商购物等。

中东市场,虽然用户基础规模较大,但用户付费能力分布不均匀,除少数客户付费能力强,大部分用户的付费能力都比较弱。但如果能够利用这一市场特点,有针对性地设计出一些产品,让付费意愿高的用户多付费,付费能力差一些的客户主要负责促进平台活跃,或许也会是出海社交  APP 很好的机会。比如融云客户 Beeto 就在中东市场取得了很好的成绩( 《WICC 话题剧透|Beeto 陈昊:中东爆款社交平台是怎样炼成的》 )。

非洲受支付和网络基建的掣肘,通常产品还停留在小游戏和较简单的社交、新闻类 APP,而较大的直播社交平台和线上游戏还尚未普及。这也是未来增长的机会。

南美市场上,类似巴西这类整体基础设施条件比较不错的地域,近年来有很多国内公司去开拓自己的业务,都收获了比较可观的收益。

应对全球市场的复杂性

*扬帆出海记者: *相比于国内来说,国外的通信环境更复杂。融云是如何应对海外通信市场这种复杂性的?

融云杨攀: 置身全球,并非所有国家和地区都与中国一样,有着非常好的互联网基础设施,反而很多国家地区的基建相当有限。作为通信云服务商,融云能做的就是通过技术手段,解决“最后一公里”的质量问题,确保全球范围内的通信低延迟。

比如,融云一直在经营的全球通信网络**,除了遍布全球数量极多的节点外,更能够通过独有的算法调度流量,帮助客户在最后一公里找到更快、更高质量的接入节点。** 这是基础通信服务厂商要突破的核心技术难点,也恰是融云的核心优势之一。

而除了纯技术层面,我们也会跟客户探讨一些业务层问题,共同应对出海挑战。

图片

(杨攀在扬帆出海 PAGC 活动发表主题演讲)

比如内容全球化问题。 美欧很多社交应用如 WhatsApp、Instagram 等都定位于“Global”产品,它们假定跨国用户交流都使用英语。

但实际上,这几年随着技术的发展,我们几乎已经能够做到文字、图片、语音的实时翻译。把翻译技术与社交结合起来,可以让社交突破语言的边界,让不同地区的人使用各自的母语流畅沟通。依据“六度分割理论”,社交的核心是连接,连接越多,社交用户的规模自然越大,用户群也越活跃,其中就蕴含着巨大的商业价值。

图片

再比如合规问题。 随着各国数据主权意识提升,GDPR 等法律法规的发布推动,合规的重要性日益突显。融云的专长或许不在于权威讲解具体的法律条规,但我们能够从全局的视角帮助客户梳理。想做全球业务,单“合规”这一个点就能够拆分出个人隐私、数据安全、数据主权、内容安全、通信安全等方方面面。任何一方面考虑不到,都有可能给出海业务造成比较大的隐患。

现今的融云,无论从技术、服务还是客户范围上,都早已经做到全球覆盖。未来我们的方向,在于针对不同区域的市场特征,跟厂商共同打造具有地域特色的产品品类,不断打磨更细致的场景应用和玩法

元宇宙与 5G 时代下的社交风口

*扬帆出海记者: *尽管服务理念不断升级,但“一切为了开发者”始终是融云不变的使命。说到开发者,他们的需求必然也会随着环境不断变化,比如说像现在比较火的元宇宙、云社交、云游戏等,那么融云是如何应对新领域中的新需求呢?

融云杨攀: 元宇宙确实是当下热门的话题。其实元宇宙涉及的产业链是非常之多的,比如硬件 VR 设备,以及 3D 技术等。其中就涉及通信技术。

在元宇宙范畴中,与通信相关的主要有两类:

一类是产品中与聊天相关的基础设施,但表现形态可能与日常所用的微信等不太一样。

另一类则是音频。在三维空间中,人们需要社交,本质上是进行语音在线对话。其中涉及很多相关技术,比如音频处理技术 —— 用户之间的语音会被谁听见,用户之间的距离处理等,更核心的是,这种处理技术需要持续在线以保证产品的通信能力,这正是元宇宙产品对通信厂商的基础需求。

*扬帆出海记者: *元宇宙要表达的核心是虚拟与现实的场景融合,所以要完成元宇宙的构建,5G成为核心一环,5G 创造的社会生活新范式,也将让更多人实实在在地享受到数字技术的红利。国内 5G 正处于高速建设的状态,全球范围又是怎样?融云又是如何利用 5G 来突破自我的呢?

融云杨攀: 5G 带来的是整个市场翻天覆地的变化。3G、4G 时代的特点是“业务推着网络走”,具体表现就是业务需求已经到了,但网络速度跟不上,人们只能更多地应用缓存、下载到本地的方式实现需要;4G 的到来,才逐渐可以满足人们“开盖即食”的视频需求。但 5G 则全然不同,5G 领先需求提前一步到来,应用场景还处于相对落后的状态。

就 5G 的三大特点【低延迟】、【高并发】、【海量连接】 来说,目前纯粹依赖这些技术特点的场景,如远程手术操作、VR三维空间视频流通信、物联网等,尚处于探索阶段。所以从通信产业运营商或设备厂商的角度来说,仍要继续探索 5G 三大特点能够同时覆盖的场景,甚至包括 5G 协议的版本迭代也是未来仍需继续探索的道路。

显然,融云对未来的规模化发展已经进行了周密的布局。5G 时代下的新机会,对融云来说又会是一个新的跨越。

*扬帆出海记者: *5G 时代还需要通信产业不断摸索,但听说 6G 都已经在路上了,这是怎么回事?

融云杨攀: 当年 5G 的设计工作大概从 2012 年就已经开始了,而实际应用则是在十年后的今天,所以通常来说,技术开发和设计需要很长时间的铺垫。

而这也恰巧引申出一个现象。像传统的短信、通话产品,迭代速度很慢,但如今互联网基础上的迭代速度却非常之快。随着 4G、5G 时代的来临,流量都流向互联网,分工趋向专业。 底层运营商的职责就是把基础设施铺好、解决好“管道”的问题;而管道上面通信能力衍化的场景,则会由融云这样的厂商在软件层去解决 

未来已来,拥抱变化

*扬帆出海记者: *未来已来。据我了解,在引领行业发展方面融云一直走在行业前列,这是不是也是融云举办全球互联网通信云大会 WICC 的初衷?

融云杨攀: 是的。实际上,融云在 2019 创办第一届全球互联网通信云大会 WICC 时,便以为通信领域开发者和技术人员创建技术交流和行业探索的平台为出发点。如今第四次举办,我们最大的期望就是,整个行业可以搭乘 WICC 这个平台, 一起去见证时代的发展和产业的变化,一起踏踏实实探讨和解决现有的各种难题,一起仰望星空、脚踏实地。****

本届 WICC 我们会专注于泛娱乐、社交、出海等领域话题,相信这次也一定会收获一场硕果累累的行业盛会。

扬帆出海记者: 我们也共同期待这场盛会的到来。预祝 11 月 20 日 WICC 广州站取得圆满成功!

原文链接: https://juejin.cn/post/7022871011284484126


收起阅读 »

Flutter 基础 | Dart 语法

该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~声明并初始化变量int i = 1; // 非空类型必须被初始化 int? k...
继续阅读 »

该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~

声明并初始化变量

int i = 1; // 非空类型必须被初始化
int? k = 2; // 可空类型
int? h; // 只声明未初始化,则默认为 null
var j = 2; // 自动推断类型为int
late int m; // 惰性加载
final name = 'taylor'; // 不可变量
final String name = 'taylor'; // 不可变量

Dart 中语句的结尾是带有分号;的。

Dart 中声明变量时可以选择是否为它赋初始值。但非空类型必须被初始化。

Dart 中声明变量可以显示指明类型,类型分为可空和非空,前者用类型?表示。也可以用var来声明变量,此时编译器会根据变量初始值自动推断类型。

late关键词用于表示惰性加载,它让非空类型惰性赋值成为可能。得在使用它之前赋值,否则会报运行时错误。

惰性加载用于延迟计算耗时操作,比如:

late String str = readFile();

str 的值不会被计算,直到它第一次被使用。

??可为空类型提供默认值

String? name;
var ret = name ?? ''

如果 name 为空则返回空字串,否则返回 name 本身。

数量

在 Dart 中intdouble是两个有关数量的内建类型,它们都是num的子类型。

若声明变量为num,则可同时被赋值为intdouble

num i = 1;
i = 2.5;

字串

''""都可以定义一个字串

var str1 = 'this is a str';
var str2 = "this is another str";

字串拼接

使用+拼接字符串

var str = 'abc'+'def'; // 输出 abcdef

多行字串

使用'''声明多行字符串

var str = '''
this is a
multiple line string
'''
;

纯字串

使用r声明纯字符串,其中不会发生转义。

var str = r'this is a raw \n string'; // 输出 this is a raw \n string

字串内嵌表达式

字符串中可以内嵌使用${}来包裹一个有返回值的表达式。

var str = 'today is ${data.get()}';

字串和数量相互转化:

int.parse('1'); // 将字串转换为 int
double.parse('1.1'); // 将字串转换为 double
1.toString(); // 将 int 转换为字串
1.123.toStringAsFixed(2); // 将 double 转换为字串,输出 '1.12'

集合

声明 List

与有序列表对应的类型是List

[]声明有序列表,并用,分割列表元素,最后一个列表元素后依然可以跟一个,以消灭复制粘贴带来的错误。

var list = [1,2,3,];

存取 List 元素

列表是基于索引的线性结构,索引从 0 开始。使用[index]可以获取指定索引的列表元素:

var first = list[0]; // 获取列表第一个元素
list[0] = 1; //为列表第一个元素赋值

展开操作符

...是展开操作符,用于将一个列表的所有元素展开:

var list1 = [1, 2, 3];
var list2 = [...list1, 4, 5, 6];

上述代码在声明 list2 时将 list1 展开,此时 list2 包含 [1,2,3,4,5,6]

除此之外,还有一个可空的展开操作符...?,用于过滤为null的列表:

var list; // 声明时未赋初始值,则默认为 null
var list2 = [1, ...?list]; // 此时 list2 内容还是[1]

条件插入

iffor是两个条件表达式,用于有条件的向列表中插入内容:

var list = [
'aa',
'bb',
if (hasMore) 'cc'
];

如果 hasMore 为 true 则 list 中包含'cc',否则就不包含。

var list = [1,2,3];
var list2 = [
'0',
for (var i in list) '$i'
];// list2 中包含 0,1,2,3

在构建 list2 的时候,通过遍历 list 来向其中添加元素。

Set

Set中的元素是可不重复的。

{}声明Set,并用,分割元素:

var set = {1,2,3}; // 声明一个 set 并赋初始元素
var set2 = {}; // 声明一个空 set
var set3 = new Set(); // 声明一个空 set
var set4 = Set(); // 声明一个空 setnew 关键词可有可无

Map

Map是键值对,其中键可以是任何类型但不能重复。

var map = {
'a': 1,
'b': 2,
}; // 声明并初始化一个 map,自动推断类型为 Map

var map2 = Map(); // 声明一个空 map
map2['a'] = 1; // 写 map
var value = map['a']; //读 map

读写Map都通过[]

const

const是一个关键词,表示一经赋值则不可修改:

// list
var list = const [1,2,3];
list.add(4); // 运行时报错,const list 不可新增元素

// set
var set = const {1,2,3};
set.add(4); // 运行时报错,const set 不可新增元素

// map
var map = const {'a': 1};
map['b'] = 2; // 运行时报错,const map 不能新增元素。

声明类

class Pointer {
double x;
double y;

void func() {...} // void 表示没有返回值
double getX(){
return x;
}
}
  • 用关键词 class声明一个类。
  • 类体中用类型 变量名;来声明类成员变量。
  • 类体中用返回值 方法名(){方法体}来声明类实例方法。

构造方法

上述代码会在 x ,y 这里报错,说是非空字段必须被初始化。通常在构造方法中初始化成员变量。

构造方法是一种特殊的方法,它返回类实例且签名和类名一模一样。

class Point {
double x = 0;
double y = 0;
// 带两个参数的构造方法
Point(double x, double y) {
this.x = x;
this.y = y;
}
}

这种给成员变量直接赋值的构造方法有一种简洁的表达方式:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y); // 当方法没有方法体时,得用;表示结束
}

命名构造方法

Dart 中还有另一个构造方法,它的名字不必和类名一致:

class Point {
double x;
double y;

Point.fromMap(Map map)
: x = map['x'],
y = map['y'];
}

为 Point 声明一个名为fromMap的构造方法,其中的:表示初始化列表,初始化列表用来初始化成员变量,每一个初始化赋值语句用,隔开。

初始化列表的调用顺序是最高的,在一个类实例化时会遵循如下顺序进行初始化:

  1. 初始化列表
  2. 父类构造方法
  3. 子类构造方法

Point.fromMap() 从一个 Map 实例中取值并初始化给成员变量。

然后就可以像这样使用命名构造方法:

Map map = {'x': 1.0, 'y': 2.0};
Point point = Point.fromMap(map);

命名构造方法的好处是可以将复杂的成员赋值的逻辑隐藏在类内部。

继承构造方法

子类的构造方法不能独立存在,而是必须调用父类的构造方法:

class SubPoint extends Point {
SubPoint(double x, double y) {}
}

上述 SubPointer 的声明会报错,提示得调用父类构造方法,于是改造如下:

class SubPoint extends Point {
SubPoint(double x, double y) : super(x, y);
}

在初始化列表中通过super调用了父类的构造方法。父类命名构造方法的调用也是类似的:

class SubPoint extends Point {
SubPoint(Map map) : super.fromMap(map);
}

构造方法重定向

有些构造方法的目的只是调用另一个构造方法,为此可以在初始化列表中通过this实现:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y);
Point.onlyX(double x): this(x, 0);
}

Point.onlyX() 通过调用另一个构造方法并为 y 值赋值为 0 来实现初始化。

方法

Dart 中方法也是一种类型,对应Function类,所以方法可以被赋值给变量或作为参数传入另一个方法。

// 下面声明的两个方法是等价的。
bool isValid(int value){
return value != 0;
}

isValid(int value){// 可自动推断返回值类型为 bool
return value != 0;
}

声明一个返回布尔值的方法,它需传入一个 int 类型的参数。

其中方法返回值bool是可有可无的。

bool isValid(int value) => value != 0;

如果方法体只有一行表达式,可将其书写成单行方法样式,方法名和方法体用=>连接。

Dart 中的方法不必隶属于一个类,它也可以顶层方法的形式出现(即定义在.dart文件中)。定义在类中的方法没有可见性修饰符public private protected ,而是简单的以下划线区分,_开头的函数及变量是私有的,否则是公有的。

可选参数 & 命名参数

Dart 方法可以拥有任意数据的参数,对于非必要参数,可将其声明为可选参数,调用方法时,就不用为其传入实参:

bool isValid(int value1, [int value2 = 2, int value3 = 3]){...}

定义了一个具有两个可选参数的方法,其中第二三个参数用[]包裹,表示是可选的。而且在声明方法时为可选参数提供了默认值,以便在未提供相应实参时使用。所以如下对该方法的调用都是合法的。

var ret = isValid(1) // 不传任何可选参数
var ret2 = isValid(1,2) // 传入1个可选参数
var ret3 = isValid(1,2,3) // 传入2个可选参数

使用[]定义可选参数时,如果想只给 value1,value3 传参,则无法做到。于是乎就有了{}

bool isValid(int value1, {int value2 = 2, int value3 = 3}) {...}

然后就可以跳过 value2 直接给 value3 传参:

var ret = isValid(1, value3 : 3)

这种语法叫可选命名参数

Dart 还提供了关键词required指定在众多可选命名参数中哪些是必选的:

bool isValid(int value1, {int value2, required int value3}) {...}

匿名方法

匿名方法表示在给定参数上进行一顿操作,它的定义语法如下:

(类型 形参) {
方法体
};

如果方法体只有一行代码可以将匿名函数用单行表示:

(类型 形参) => 方法体;

操作符

三元操作符

三元操作符格式如下:布尔值 ? 表达式1 : 表达式2;

var ret = isValid ? 'good' : 'no-good';

如果 isValid 为 true 则返回表达式1,否则返回表达式2。

瀑布符

该操作符..用于合并在同一对象上的多个连续操作:

val paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0

构建一个画笔对象并连续设置了 3 个属性。

如果对象可控则需使用?..

paint?..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0

类型判定操作符

as 是强转操作符,表示将一个类型强转为另一个类型。

is 是类型判定操作符,用于判断某个实例是否是指定类型。

is! 是与 is 相反的判定。

流程控制

if-else

if (isRaining()) {
you.bringRainCoat();
} else if (isSnowing()) {
you.wearJacket();
} else {
car.putTopDown();
}

for

for (var i = 0; i < 5; i++) {
message.write('!');
}

如果不需要关心循环的索引值,则可以这样:

for (var item in list) {
item.do();
}

while

while (!isDone()) {
doSomething();
}
do {
printLine();
} while (!atEndOfPage());

break & continue

break & continue 可用于 for 和 while 循环。

break用于跳出循环

var i = 0
while (true) {
if (i > 2) break;
print('$i');
i++;
} // 输出 0,1,2

continue用于跳过当前循环的剩余代码:

for (int i = 0; i < 10; i++) {
if (i % 2 == 0) continue;
print('$i');
}// 输出 1,3,5,7,9

switch-case

Dart 中的 switch-case 支持 String、int、枚举的比较,以 String 为例:

var command = 'OPEN';
switch (command) {
case 'CLOSED':
case 'PENDING': // 两个 case 共用逻辑
executePending();
break; // 必须有 break
case 'APPROVED':
executeApproved();
break;
case 'DENIED':
executeDenied();
break;
case 'OPEN':
executeOpen();
break;
default: // 当所有 case 都未命中时执行 default 逻辑
executeUnknown();
}

关键词

所有的关键词如下所示:

abstract 2elseimport 2show 1
as 2enuminstatic 2
assertexport 2interface 2super
async 1extendsisswitch
await 3extension 2late 2sync 1
breakexternal 2library 2this
casefactory 2mixin 2throw
catchfalsenewtrue
classfinalnulltry
constfinallyon 1typedef 2
continueforoperator 2var
covariant 2Function 2part 2void
defaultget 2required 2while
deferred 2hide 1rethrowwith
doifreturnyield 3
dynamic 2implements 2set 2


作者:唐子玄
链接:https://juejin.cn/post/7028710779171897351
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

原来我一直在错误的使用 setState()?

导语 任何前端系统与用户关系最密切的部分就是UI。一个按钮,一个标签,都是通过对应的UI元素展示与交互。初学时,我们往往只关注如何使用。但如果只知道如何使用,遇到问题我们很难找到解决的办法和思路,也无法针对一些特定场景进行优化。本期针对Flutter的UI系统...
继续阅读 »

导语


任何前端系统与用户关系最密切的部分就是UI。一个按钮,一个标签,都是通过对应的UI元素展示与交互。初学时,我们往往只关注如何使用。但如果只知道如何使用,遇到问题我们很难找到解决的办法和思路,也无法针对一些特定场景进行优化。本期针对Flutter的UI系统和大家一起进阶学习:


1、原来我一直在错误的使用 setState()?


2、面试必问:说说Widget和State的生命周期


3、Flutter的布局约束原理


4、15个例子解析Flutter布局过程


读完本文你将收获:Flutter的渲染机制以及setState()背后的原理




引言


初学Flutter的时候,当需要更新页面数据时,我们通常会想到调用setState()。但很多博客以及官方文章并不建议我们在页面的节点使用setState()因为这样会带来不必要的开销(仅针对页面节点,当然Flutter的Widget刷新一定离不开setState()),很多状态管理方案也是为了达到所谓的“局部刷新”。到这我们不仅要思考为什么使用setState()能刷新页面,又为何可能会带来额外的损耗?这个函数背后做了什么逻辑?这篇文章和大家一一揭晓。




一、为什么setState()能刷新页面


1、setState()


我们的demo从一个最简单的计数器开始



在页面中点击底部的➕号,本地变量加一,之后调用了当前页面的setState(),页面重新构建,显示的数据增加。从现象推断,整个流程必然会经过setState()-···················->当前State的build()-················->页面绘制-············->屏幕刷新。
那么下面我们看看setState()到底做了什么?


State#setState(VoidCallback fn)


@protected
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}

在去掉所有的断言之后,其实setState只做了两件事儿


1、调用我们传入的VoidCallback fn


2、调用_element.markNeedsBuild()




2、element.markNeedsBuild()


Flutter开发中我们一般和Widget打交道,但Widget上有这样一个注释。



Describes the configuration for an [Element].



abstract class Widget extends DiagnosticableTree {
final Key key;
Element createElement();
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}

Widget只是用于描述Element的一个配置文件,实际在Framework层管理页面的构建,渲染等,都是通过Element完成,Element由Widget创建,并且持有Widget对象,每一种Widget都会对应的一种Element



在上面的demo中,我们在HomePageState调用了setState(),这里的Element有HomePage对象创建。HomePage(Widget) - HomePageState(State) - HomePageElement(StatefulElement) 三者一一对应。



Element#markNeedsBuild()


/// The object that manages the lifecycle of this element.
/// 负责管理所有element的构建以及生命周期
@override
BuildOwner get owner => _owner;

void markNeedsBuild() {
//将自己标记为脏
_dirty = true;
owner.scheduleBuildFor(this);
}

调用了BuildOwner.scheduleBuildFor(element),这里的BuildOwnerWidgetsBinding的初始化中完成实例化,负责管理widget框架,每个Element对象在mount到element树中之后都会从父节点获得它的引用


WidgetsBinding#initInstances()


void initInstances() {
super.initInstances();
_instance = this;
_buildOwner = BuildOwner();
buildOwner.onBuildScheduled = _handleBuildScheduled;
/······/
}

BuildOwner#scheduleBuildFor(Element element)


void scheduleBuildFor(Element element) {
//添加到_dirtyElements集合中
_dirtyElements.add(element);
element._inDirtyList = true;
}

最后将自己添加到BuildOwner中维护的一个脏element集合。



总结:1、Element: 持有Widget,存放上下文信息,RenderObjectElement 额外持有 RenderObject。通过它来遍历视图树,支撑UI结构。


2、setState()过程其实只是将当前对应的Element标记为脏(demo中对应HomePageState),并且添加到_dirtyElements合中。





3、Flutter渲染机制


上面的过程看起来没做任何渲染相关的事儿,那么页面是如何重新绘制?关键点就在于Flutter的渲染机制



开始FrameWork层会通知Engine表示自己可以进行渲染了,在下一个Vsync信号到来之时,Engine层会通过Windows.onDrawFrame回调Framework进行整个页面的构建与绘制。(这里我想为什么要先由Framework发起通知,而不是直接由Vsync驱动。如果一个页面非常卡顿,恰好每一帧绘制的时间大于一个Vsync周期,这样每帧都不能在一个Vsync的时间段内完成绘制。而先由framework保证上完成构建与绘制后,发起通知在下一个Vsync信号再绘制则可以避免这样的情况)。每次收到渲染页面的通知后,Engine调用Windows.onDrawFrame最终交给_handleDrawFrame()方法进行处理。


@protected
void ensureFrameCallbacksRegistered() {
//构建帧前的处理,主要是进行动画相关的计算
window.onBeginFrame ??= _handleBeginFrame;
//Windows.onDrawFrame交给_handleDrawFrame进行处理
window.onDrawFrame ??= _handleDrawFrame;
}
复制代码

SchedulerBinding#handleDrawFrame()


void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
// 关键回调
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
/·····························/
}
}

Flutter AnimationController回调原理一期中我们提到过,在Flutter的SchedulerBinding中维护了这样三个队列




  • Transient callbacks,由系统的[Window.onBeginFrame]回调,用于同步应用程序的行为 到系统的展示。例如,[Ticker]s和[AnimationController]s触发器来自与它。

  • Persistent callbacks 由系统的[Window.onDrawFrame]方法触发回调。例如,框架层使用他来驱动渲染管道进行build, layout,paint

  • Post-frame callbacks在下一帧绘制前回调,主要做一些清理和准备工作 Non-rendering tasks 非渲染的任务,可以通过此回调获取一帧的渲染时间进行帧率相关的性能监控



SchedulerBinding.handleDrawFrame()中对_persistentCallbacks_postFrameCallbacks集合进行了回调。根据上面的描述可知,_persistentCallbacks中是一些固定流程的回调,例如build,layout,paint。跟踪这个_persistentCallbacks这个集合,发现在RendererBinding.initInstances()初始化中调用了addPersistentFrameCallback(_handlePersistentFrameCallback)方法。这个方法只有一行调用就是drawFrame()



总结:



  • SchedulerBinding中维护了这样三个队列TransientCallbacks(动画处理),PersistentCallbacks(页面构建渲染),PostframeCallbacks(每帧绘制完成后),并在合适的时机对其进行回调。

  • 当收到Engine的渲染通知之后通过Windows.onDrawFrame方法回调到Framework层调用handleDrawFrame

  • handleDrawFrame回调PersistentCallbacks(页面构建渲染),最终调用drawFrame()





4、drawFrame()


查看drawFrame()方法一般会直接点击到RendererBinding


RendererBinding#drawFrame()


void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

从这几个方法名能大致看出,这里调用了布局,绘制,渲染帧的。而且看类名,这是负责渲染的Binding,并没有调用Widget的构建。这是因为WidgetsBinding是onRendererBinding的(理解为继承),其中重写了drawFrame(),实际上调用的应该是WidgetsBinding.drawFrame()


WidgetsBinding#drawFrame()


@override
void drawFrame() {
try {
if (renderViewElement != null)
// buildOwner就是前面提到的负责管理widgetbuild的对象
// 这里的renderViewElement是整个UI树的根节点
buildOwner.buildScope(renderViewElement);
super.drawFrame();
//将不再活跃的节点从UI树中移除
buildOwner.finalizeTree();
} finally {
/·················/
}
}

super.drawFrame()之前,先调用 buildOwner.buildScope(renderViewElement)
BuildOwner#buildScope(Element context, [ VoidCallback callback ])


void buildScope(Element context, [ VoidCallback callback ]) {
if (callback == null && _dirtyElements.isEmpty)
return;
try {
_scheduledFlushDirtyElements = true;
_dirtyElementsNeedsResorting = false;
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
///关键在这
_dirtyElements[index].rebuild();
} catch (e, stack) {
/···············/
}
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
}
}

前面在setState()之后,将homePageState添加到_dirtyElements里面。而这个方法会对集合内的每一个对象调用rebuild()rebuild()这个方法最终走到performRebuild(),这是一个Element中的一个抽象方法。




二、为什么高位置的setState ()会消耗性能


1、performRebuild()


查看StatelessElementStatefulElement共同祖先CompantElement中的实现


CompantElement#performRebuild()


void performRebuild() {
Widget built;
try {
built = build();
} catch (e, stack) {
built = ErrorWidget.builder();
}
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
built = ErrorWidget.builder();
_child = updateChild(null, built, slot);
}

}

这个方法直接调用子类的build方法返回了一个Widget,对应调用前面的HomePageState()中的build方法。


将这个新build()出来的widget和之前挂载在Element树上的_child(Element类型)作为参数,传入updateChild(_child, built, slot)中。setState()的核心逻辑就在 updateChild(_child, built, slot)


2、updateChild(_child, built, slot)


StatefulElement#updateChild(Element child, Widget newWidget, dynamic newSlot)


@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
//child == null && newWidget == null
deactivateChild(child);
//child != null && newWidget == null
return null;
}
if (child != null) {
if (child.widget == newWidget) {
//child != null && newWidget == child.widget
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
//child != null && Widget.canUpdate(child.widget, newWidget)
child.update(newWidget);
return child;
}
deactivateChild(child);
}
// child != null && !Widget.canUpdate(child.widget, newWidget)
return inflateWidget(newWidget, newSlot);
}

这个方法上官方提供了这样的注释:






















newWidget == nullnewWidget != null
child == nullReturns null.Returns new [Element].
child != nullOld child is removed, returns null.Old child updated if possible, returns child or new [Element].

总的来说,根据之前挂载在Element树上的_child以及再次调用build()出来的newWidget对象,共有四种情况




  • 如果之前的位置child为null

    • A、如果newWidget为null的话,说明这个位置始终没有子节点,直接返回null即可。

    • B、如果newWidget不为null,说明这个位置新增加了子节点调用inflateWidget(newWidget, newSlot)生成一个新的Element返回



  • 如果之前的child不为null

    • C、如果newWidget为null的话,说明这个位置需要移除以前的节点,调用 deactivateChild(child)移除并且返回null

    • D、如果newWidget不为null的话,先调用Widget.canUpdate(child.widget, newWidget)对比是否能更新。这个方法会对比两个Widget的runtimeTypekey,如果一致则说明子Widget没有改变,只是需要根据newWidget(配置清单)更新下当前节点的数据child.update(newWidget);如果不一致说明这个位置发生变化,则deactivateChild(child)后返回inflateWidget(newWidget, newSlot)





而在demo中,观察代码我们可以知道



在homePageState中调用setState()后,child和newWidget都不为空都是Scaffold类型,并且由于我们没有显示的指定key,所以会走child.update(newWidget)方法**(注意这里的child已经变成Scaffold)**。


3、递归更新


update(covariant Widget newWidget)是一个抽象方法,不同element有不同实现,以StatulElement为例


void update(StatefulWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
final StatefulWidget oldWidget = _state._widget;
// Notice that we mark ourselves as dirty before calling didUpdateWidget to
// let authors call setState from within didUpdateWidget without triggering
// asserts.
_dirty = true;
_state._widget = widget;
try {
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild();
}

这个方法先回调用_state.didUpdateWidget我们可以在State中重写这个方法,走到最后发现最终再次调用了rebuild()。但这里需要注意这次调用rebuild()的已经不是HomePageState了,而是他的第一个子节点Scaffold。所以整个过程又会再次走到performRebuild(),又在再次调用updateChild(_child, built, slot)更新子节点。不断的递归直到页面的最子一级节点。如图



build()过程虽然只是调用一个组件的构造方法,不涉及对Element树的挂载操作。但因为我们一个组件往往是N多个Widget的嵌套组合,每个都遍历一遍开销算下来并不小(感兴趣可以数数Scaffold有多少层嵌套)。


回到我们的demo中,其实我们的诉求只是点击+号改变以前显示的数据。



但直接在页面节点调用setState()将会重新调用所有Widget(包括他们中的各种嵌套)的build()方法,如果我们的需求是一个较为复杂的页面,这样带来的开销消耗可想而知。


而要想解决这个问题可以参考告别setState()! 优雅的UI与Model绑定 Flutter DataBus使用~




总结


当我们在一个高节点调用setState()的时候会构建再次build所有的Widget,虽然不一定挂载到Element树中,但是平时我们使用的Widget中往往嵌套多个其他类型的Widget,每个build()方法走下来最终也会带来不小的开销,因此通过各种状态管理方案,Stream等方式,只做局部刷新,是我们日常开发中应该养成的良好习惯。




最后


本期我们分析了setState()过程,重点分析了递归更新的过程。正如安卓Activity或者Fragment的生命周期,Flutter中Widget和State同样也提供了对应的回调,如initState()build()。这些方法背后是谁在调用,他们的调用时序是如何?Element的生命周期是如何调用的?将会在下一期和大家一一分析~


作者:Nayuta
链接:https://juejin.cn/post/6905996819445055495
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter 毁了客户端和 Web 开发!

Google 重磅发布了专为 Web、移动和桌面而构建的 Flutter 2.0!将 Flutter 从移动开发框架扩展成可移植框架,因而开发者无需重写代码即可将应用扩展至桌面或网页。看似为了帮助Web和移动开发者,实际上不然,而本文作者认为,现在不应该再去想...
继续阅读 »

Google 重磅发布了专为 Web、移动和桌面而构建的 Flutter 2.0!将 Flutter 从移动开发框架扩展成可移植框架,因而开发者无需重写代码即可将应用扩展至桌面或网页。看似为了帮助Web和移动开发者,实际上不然,而本文作者认为,现在不应该再去想创建一个需要部署到所有平台的应用程序,Flutter反而毁了Web和移动开发。


以下为译文: 大家好,我是一名软件开发人员,我叫 Luke。


由于我选择了这个相当大胆的标题,为了避免误会,我要对其进行详细的解释。从技术角度来讲,Flutter 的确是一个跨平台的框架。也不止其,所有跨断技术都是非常糟糕的设计。


但是,我有点不同的看法。


从 Flutter 2.0 发布以来,我就察觉到它被炒的有点过了。但不应该再去想创建一个需要部署到所有平台的应用程序,Flutter反而毁了Web和移动开发。


请不要误会,我并不是要否定它,其实我也是 Flutter 的粉丝,亦将一如既往的拥护它。


我在日常工作中经常使用 Flutter 来开发 iOS 和 Android 应用程序。由于早前我是用 Kotlin 或者 Swift 来开发原生的应用,支持多种特性,如:扫描 / 页面识别、pin/biometric 应用程序认证、通知、firebase 统计和一些高级的用户流,现在用 Flutter 来开发应用,我对 Flutter 的优缺点的了解更加透彻。


1、六大平台


image.png


通过今年的 Flutter Engage 会议我们可知已经可以使用 Flutter 在 iOS、 Android、 Mac、 Windows、 Linux 和 Web 这六个平台中的任何一个平台上开发应用。这太棒了!但事情远没有这么简单...你的确可以在这 6 个平台上部署你的应用程序,但是说实话,我很少这么做。我很难想象一个人会在不同的平台上部署同一个应用程序,我认为应该根据不同的平台特点使用不同的设计模式。在大型设备上使用底部弹窗、应用程序条、简洁的列表就很别扭。一般来说,适合在移动设备上的组件和设计模式在桌面设备上却不合时宜,反之亦然。


我的一个非常好的朋友 Filip Hracek 在 Flutter Engage 演讲中提到“神奇的设计开发者”的相关话题,我非常赞同他的看法。我认为需要有更多的开发者真正知道他们正在做的是什么,而且不是盲目地跟从迭代面板。


Scrum Sprint 是一个可重复的固定时间框,在这个时间框内创造一个高价值的产品。-- 维基百科


强烈推荐大家观看 Filip 在 Youtube 上的相关视频片段http://www.youtube.com/watch?v=MIe…


接下来,我们重新回到 Flutter 这个话题:


2、不应该再去想创建一个需要部署到所有平台的应用程序


你更应该去想如何将你要编写的应用程序模块化,以便在未来更好地复用这些模块。给你们举个例子:在我的公司,我们正在开发专注于用户数据的应用程序。


这就需要创建自定义和高级的调查报告,我们不希望每次添加新问题时都要编写新的窗口小部件。我们的做法是:编写一个包含所有可能的调查逻辑的模块,在许多其他项目中复用它(而不需要每次都重写一遍相似的代码)


我给你举上面这个例子的目的是提醒你在构建一个应用程序时,你更应该着重思考你要做的应用程序或整个业务的重点是什么。更应该去重点思考,它背后的业务逻辑是什么?


在计算机软件中,业务逻辑或领域建模也是程序的一部分,它对真实世界的业务规则进行编码,确定如何创建、存储和修改数据。


当你明确了领域划分,你可以将一个领域封装成独立的模块,你可以将该模块在需要开发的 Flutter 应用程序中复用。


但 Luke,这有什么好大惊小怪的吗?


对,这是一个好问题!


对于相同的业务逻辑,你可以用不同的用户流来创建多个 Flutter 应用。你可以将要开发的 Flutter 应用进行分类(如:移动应用、桌面应用和 Web应用),这将能帮助关注到不同平台的差异,对特定平台进行特定处理最终将获得更好的用户体验。


3、针对不同平台要编****写多个应用程序


虽然 Flutter 还算是一个相对比较新的技术,还主要针对小公司和个人开发者,但这不妨碍它成为一个人人皆可用的伟大工具。


我参与开发过多个企业级应用程序。根据我的经验,系统的每个部分都需要有一个清晰的工作流程。开发一个系统通常需要前端、后端等。为了节约成本,编写一个应用程序,在不同的平台运行也越发流行。为了实现这个目的,你需要雇一个团队进行专门开发。你敢想象,十几个人的团队开发同一套代码来实现所有平台的特性吗?这简直是管理层的噩梦。很可能出现:一部分开发人员开发的桌面特性与移动团队正在开发的特性相冲突的情况。


其次,应用程序包也会越来越臃肿,然而很多时候并不是每个平台都需要有一份软件包。现在,正值 Flutter 2.0 发布的时候,由于我并没有将所有的包都进行升级,还不支持 null 安全还需要手动解决依赖冲突的问题。


4、为什么 Flutter 不是一个跨平台的框架


在读了这篇文章之后,或许你能够理解为什么我会认为 Flutter 不是一个真正的跨平台框架。Flutter 是一个为我们提供了为每个平台构建应用程序所需的功能的工具。我认为,真正实现跨平台不应该只开发一个应用程序,更应该开发一组由相同的业务逻辑驱动的应用程序集合。


此外,当我们编写 Flutter 应用程序时,我们并没有跨越任何平台。我们这种所谓的跨平台,不过是用 Xamarin 或其他工具将写好的代码翻译成原生元素。


如果非要把 Flutter 和其他东西进行类比的话,那么与之相似的就是游戏引擎(如 Unity)。我们不需要专门在 Windows 或者 Mac 系统上开发对应平台的游戏。我们可以使用 Unity 编写,然后将其导出到一个特定的平台。使用 Unity 编写一个游戏然后导出到多个平台和真正的跨平台完全也是两码事。


因为每个项目都有技术债务,你应该停止抱怨,并开始重构。每次开发新功能之前都应该进行小型代码重构。但接入 Flutter 大规模的重构和重写永远不会有好结果。


5、结尾


全文都在讨论跨平台相关话题, 以上就是我认为 flutter 毁了 Web 开发的原因。很多人对这一说法很感兴趣,并热切地加入了辩论。如果你认为 flutter 并没有那么糟糕,或许你会持有不同意见 。


作者:传道士
链接:https://juejin.cn/post/7027828523213684773
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

n皇后问题

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 典型的回溯法问题 思路: 尝试性的放置 ,从第一行开始,接着在下一行放置,(这里的好处就是不需要考虑行了,只需要考虑列和对角线) 一直纠...
继续阅读 »

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。


典型的回溯法问题


思路:


尝试性的放置 ,从第一行开始,接着在下一行放置,(这里的好处就是不需要考虑行了,只需要考虑列和对角线)

一直纠结第一行的问题,代码中直接传参数为0,一直好奇如何控制第一行的列的变化,后来将0自己模拟走了一遍才明白。

注意判断的是否符合规则的公式:(列==列)(abs(列-列)==abs(行-行))

具体细节见注释(仔细阅读,一定能看懂,)

#include<iostream>
#include <math.h>
#define N 8
using namespace std;

int num=0;//用来记录总的放置个数
int cur[8];//此全局变量是用来记录第i行放在得第j列,其中下标为i,值为j
int check(int n){//传进来行
for(int i=0;i<n;i++){
if(cur[i]==cur[n]||abs(n-i)==abs(cur[n]-cur[i])){//判断当前放置的位置是否与之前的放置位置是否在同一列或同斜列
return 0;
}
}
return 1;
}
void putQueen(int n){
if(n==N){//如果找到了最后一行的下一行,那么就可以将次数+1了(就是之前把所有的行已经放完了,数组下标从0开始的勿忘)
num++;
}else{
for(int j=0;j<N;j++){//列的位置从0往最后放置
cur[n]=j;//记录下当前行的当前列
if(check(n)){//判断当前放置的行列是否合适
putQueen(n+1);//开始进行下一行的放置
}
}
}

}
int main(){
putQueen(0);
cout<<num;
return 0;
}

作者:半夏的故事
链接:https://juejin.cn/post/7025241982951768094
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

还在用Swagger?试试这款零注解侵入的API文档生成工具,跟Postman绝配!

前后端接口联调需要API文档,我们经常会使用工具来生成。之前经常使用Swagger来生成,最近发现一款好用的API文档生成工具smart-doc, 它有着很多Swagger不具备的特点,推荐给大家。 SpringBoot实战电商项目mall(50k+star...
继续阅读 »

前后端接口联调需要API文档,我们经常会使用工具来生成。之前经常使用Swagger来生成,最近发现一款好用的API文档生成工具smart-doc, 它有着很多Swagger不具备的特点,推荐给大家。



SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…


聊聊Swagger


在我们使用Swagger的时候,经常会需要用到它的注解,比如@Api@ApiOperation这些,Swagger通过它们来生成API文档。比如下面的代码:



Swagger对代码的入侵性比较强,有时候代码注释和注解中的内容有点重复了。有没有什么工具能实现零注解入侵,直接根据代码注释生成API文档呢?smart-doc恰好是这种工具!


smart-doc简介


smart-doc是一款API文档生成工具,无需多余操作,只要你规范地写好代码注释,就能生成API文档。同时能直接生成Postman调试文件,一键导入Postman即可调试,非常好用!


smart-doc具有如下优点:



生成API文档



接下来我们把smart-doc集成到SpringBoot项目中,体验一下它的API文档生成功能。




  • 首先我们需要在项目中添加smart-doc的Maven插件,可以发现smart-doc就是个插件,连依赖都不用添加,真正零入侵啊;


<plugin>
<groupId>com.github.shalousun</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>2.2.8</version>
<configuration>
<!--指定smart-doc使用的配置文件路径-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>mall-tiny-smart-doc</projectName>
</configuration>
</plugin>


  • 接下来在项目的resources目录下,添加配置文件smart-doc.json,属性说明直接参考注释即可;


{
"serverUrl": "http://localhost:8088", //指定后端服务访问地址
"outPath": "src/main/resources/static/doc", //指定文档的输出路径,生成到项目静态文件目录下,随项目启动可以查看
"isStrict": false, //是否开启严格模式
"allInOne": true, //是否将文档合并到一个文件中
"createDebugPage": false, //是否创建可以测试的html页面
"packageFilters": "com.macro.mall.tiny.controller.*", //controller包过滤
"style":"xt256", //基于highlight.js的代码高设置
"projectName": "mall-tiny-smart-doc", //配置自己的项目名称
"showAuthor":false, //是否显示接口作者名称
"allInOneDocFileName":"index.html" //自定义设置输出文档名称
}


  • 打开IDEA的Maven面板,双击smart-doc插件的smart-doc:html按钮,即可生成API文档;




  • 此时我们可以发现,在项目的static/doc目录下已经生成如下文件;




  • 运行项目,访问生成的API接口文档,发现文档非常详细,包括了请求参数和响应结果的各种说明,访问地址:http://localhost:8088/doc/index.html




  • 我们回过来看下实体类的代码,可以发现我们只是规范地添加了字段注释,生成文档的时候就自动有了;


public class PmsBrand implements Serializable {
/**
* ID
*/
private Long id;

/**
* 名称
* @required
*/
private String name;

/**
* 首字母
* @since 1.0
*/
private String firstLetter;

/**
* 排序
*/
private Integer sort;

/**
* 是否为品牌制造商(0,1)
*/
private Integer factoryStatus;

/**
* 显示状态(0,1)
* @ignore
*/
private Integer showStatus;

/**
* 产品数量
*/
private Integer productCount;

/**
* 产品评论数量
*/
private Integer productCommentCount;

/**
* 品牌logo
*/
private String logo;

/**
* 专区大图
*/
private String bigPic;

/**
* 品牌故事
*/
private String brandStory;
//省略getter、setter方法
}


  • 再来看下Controller中代码,我们同样规范地在方法上添加了注释,生成API文档的时候也自动有了;


/**
* 商品品牌管理
*/
@Controller
@RequestMapping("/brand")
public class PmsBrandController {
@Autowired
private PmsBrandService brandService;

/**
* 分页查询品牌列表
*
* @param pageNum 页码
* @param pageSize 分页大小
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1")
Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "3")
Integer pageSize) {
List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize);
return CommonResult.success(CommonPage.restPage(brandList));
}
}


  • 当然smart-doc还提供了自定义注释tag,用于增强文档功能;

    • @ignore:生成文档时是否要过滤该属性;

    • @required:用于修饰接口请求参数是否必须;

    • @since:用于修饰接口中属性添加的版本号。



  • 为了写出优雅的API文档接口,我们经常会对返回结果进行统一封装,smart-doc也支持这样的设置,在smart-doc.json中添加如下配置即可;


{
"responseBodyAdvice":{ //统一返回结果设置
"className":"com.macro.mall.tiny.common.api.CommonResult" //对应封装类
}
}


  • 我们也经常会用枚举类型来封装状态码,在smart-doc.json中添加如下配置即可;


{
"errorCodeDictionaries": [{ //错误码列表设置
"title": "title",
"enumClassName": "com.macro.mall.tiny.common.api.ResultCode", //错误码枚举类
"codeField": "code", //错误码对应字段
"descField": "message" //错误码描述对应字段
}]
}


  • 配置成功后,即可在API文档中生成错误码列表




  • 有时候我们也会想给某些接口添加自定义请求头,比如给一些需要登录的接口添加Authorization头,在smart-doc.json中添加如下配置即可;


{
"requestHeaders": [{ //请求头设置
"name": "Authorization", //请求头名称
"type": "string", //请求头类型
"desc": "token请求头的值", //请求头描述
"value":"token请求头的值", //请求头的值
"required": false, //是否必须
"since": "-", //添加版本
"pathPatterns": "/brand/**", //哪些路径需要添加请求头
"excludePathPatterns":"/admin/login" //哪些路径不需要添加请求头
}]
}


  • 配置成功后,在接口文档中即可查看到自定义请求头信息了。



使用Postman测试接口



我们使用Swagger生成文档时候,是可以直接在上面测试接口的,而smart-doc的接口测试能力真的很弱,这也许是它拥抱Postman的原因吧,毕竟Postman是非常好用的接口测试工具,下面我们来结合Postman使用下!




  • smart-doc内置了Postman的json生成插件,可以一键生成并导入到Postman中去,双击smart-doc:postman按钮即可生成;




  • 此时将在项目的static/doc目录下生成postman.json文件;




  • postman.json文件直接导入到Postman中即可使用;




  • 导入成功后,所有接口都将在Postman中显示,这下我们可以愉快地测试接口了!



总结


smart-doc确实是一款好用的API文档生成工具,尤其是它零注解侵入的特点。虽然它的接口测试能力有所不足,但是可以一键生成JSON文件并导入到Postman中去,使用起来也是非常方便的!


参考资料


官方文档:gitee.com/smart-doc-t…


项目源码地址


github.com/macrozheng/…


作者:MacroZheng
链接:https://juejin.cn/post/7028377863850033183
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

你的列表很卡?这4个优化能让你的列表丝般顺滑

前言 列表 ListView 是应用中最为常见得组件,而列表往往也会承载很多元素,当元素多,尤其是那种图片文件比较大的场合,就可能会导致列表卡顿,严重的时候可能导致应用崩溃。本篇来介绍如何优化列表。 优化点1:使用 builder构建列表 当你的列表元素是动态...
继续阅读 »

前言


列表 ListView 是应用中最为常见得组件,而列表往往也会承载很多元素,当元素多,尤其是那种图片文件比较大的场合,就可能会导致列表卡顿,严重的时候可能导致应用崩溃。本篇来介绍如何优化列表。


优化点1:使用 builder构建列表


当你的列表元素是动态增长的时候(比如上拉加载更多),请不要直接用children 的方式,一直往children 的数组增加组件,那样会很糟糕。


//糟糕的用法
ListView(
children: [
item1,
item2,
item3,
...
],
)

//正确的用法
ListView.builder(
itemBuilder: (context, index) => ListItem(),
itemCount: itemCount,
)

对于 ListView.builder 是按需构建列表元素,也就是只有那些可见得元素才会调用itemBuilder 构建元素,这样对于大列表而言性能开销自然会小很多。



Creates a scrollable, linear array of widgets that are created on demand.
This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.



优化点2:禁用 addAutomaticKeepAlives 和 addRepaintBoundaries 特性


这两个属性都是为了优化滚动过程中的用户体验的。
addAutomaticKeepAlives 特性默认是 true,意思是在列表元素不可见后可以保持元素的状态,从而在再次出现在屏幕的时候能够快速构建。这其实是一个拿空间换时间的方法,会造成一定程度得内存开销。可以设置为 false 关闭这一特性。缺点是滑动过快的时候可能会出现短暂的白屏(实际会很少发生)。


addRepaintBoundaries 是将列表元素使用一个重绘边界(Repaint Boundary)包裹,从而使得滚动的时候可以避免重绘。而如果列表很容易绘制(列表元素布局比较简单的情况下)的时候,可以关闭这个特性来提高滚动的流畅度。


addAutomaticKeepAlives: false,
addRepaintBoundaries: false,

优化点3:尽可能将列表元素中不变的组件使用 const 修饰


使用 const 相当于将元素缓存起来实现共用,若列表元素某些部分一直保持不变,那么可以使用 const 修饰。


return Padding(
child: Row(
children: [
const ListImage(),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
padding: EdgeInsets.all(10.0),
);

优化点4:使用 itemExtent 确定列表元素滚动方向的尺寸


对于很多列表,我们在滚动方向上的尺寸是提前可以根据 UI设计稿知道的,如果能够知道的话,那么使用 itemExtent 属性制定列表元素在滚动方向的尺寸,可以提升性能。这是因为,如果不指定的话,在滚动过程中,会需要推算每个元素在滚动方向的尺寸从而消耗计算资源。


itemExtent: 120,

优化实例


下面是一开始未改造的列表,嗯,可以认为是垃圾代码


class LargeListView extends StatefulWidget {
const LargeListView({Key? key}) : super(key: key);

@override
_LargeListViewState createState() => _LargeListViewState();
}

class _LargeListViewState extends State<LargeListView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('大列表'),
brightness: Brightness.dark,
),
body: ListView(
children: List.generate(
1000,
(index) => Padding(
padding: EdgeInsets.all(10.0),
child: Row(
children: [
Image.network(
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7869eac08a7d4177b600dc7d64998204~tplv-k3u1fbpfcp-watermark.jpeg',
width: 200,
),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
),
),
),
);
}
}

当然,实际不会是用 List.generate 来生成列表元素,**但是也不要用一个 List<Widget> 列表对象一直往里面加列表元素,然后把这个列表作为 ListView 的 **children
改造后的代码如下所示,因为将列表元素拆分得更细,代码量是多一些,但是性能上会好很多。


import 'package:flutter/material.dart';

class LargeListView extends StatefulWidget {
const LargeListView({Key? key}) : super(key: key);

@override
_LargeListViewState createState() => _LargeListViewState();
}

class _LargeListViewState extends State<LargeListView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('大列表'),
brightness: Brightness.dark,
),
body: ListView.builder(
itemBuilder: (context, index) => ListItem(
index: index,
),
itemCount: 1000,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemExtent: 120.0,
),
);
}
}

class ListItem extends StatelessWidget {
final int index;
ListItem({Key? key, required this.index}) : super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
child: Row(
children: [
const ListImage(),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
padding: EdgeInsets.all(10.0),
);
}
}

class ListImage extends StatelessWidget {
const ListImage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Image.network(
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7869eac08a7d4177b600dc7d64998204~tplv-k3u1fbpfcp-watermark.jpeg',
width: 200,
);
}
}

总结


本篇介绍了 Flutter ListView 的4个优化要点,非常实用哦!实际上,这些要点都可以从官网的文档里找出对应得说明。因此,如果遇到了性能问题,除了搜索引擎外,也建议多看看官方的文档。另外一个,对于列表图片,有时候也需要前后端配合,比如目前的手机都是号称1亿像素的,如果上传的时候直接上传原图,那么加载如此大的图片肯定是非常消耗资源的。对于这种情况,建议是生成列表缩略图(可能需要针对不同屏幕尺寸生成不同的缩略图,比如掘金的文章头图,就分了几种分辨率)。


作者:岛上码农
链接:https://juejin.cn/post/7028354670519123976
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Handler 源码分析

一、ThreadLocal是什么ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。每个线程都有一...
继续阅读 »


一、ThreadLocal是什么

ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。每个线程都有一个ThreadLocalMap类型的threadLocals变量,ThreadLocalMap是一个自定义哈希映射,仅用于维护线程本地变量值。ThreadLocalMap是ThreadLocal的内部类,主要有一个Entry数组,Entry的key为ThreadLocal,value为ThreadLocal对应的值。

也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。注意上图的虚线,它代表一个弱引用类型,而弱引用的生命周期只能存活到下次GC前。

二、ThreadLocal是什么ThreadLocal为什么会内存泄漏

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

三、Handler机制

我们主要看一下Looper源码, Looper 在程序启动的时候系统就已经帮我们创建好了

在main方法中系统调用了 Looper.prepareMainLooper();来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环。来看看Looper.prepareMainLooper()是怎么创建出这两个对象的

//系统实例化 Handler
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

可以看到,在这个方法中调用了 prepare(false);方法和 myLooper();方法,那么再进入prepare()

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}// 往当前线程的私有变量里添加 Looper
sThreadLocal.set(new Looper(quitAllowed));
}

在这里可以看出,sThreadLocal对象保存了一个Looper对象,首先判断是否已经存在Looper对象了,以防止被调用两次。sThreadLocal对象是ThreadLocal类型,因此保证了每个线程中只有一个Looper对象。

// Looper 在实例化的时候也实例化了一个消息队列同时还持有了当前线程的引用
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

//然后我们从发送消息查看源码
public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}

------经过几个方法的调用进入下面的方法

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
// mQueue 在 Handler 实例化的时候就从当前线程中取出消息队列并赋值了
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(this + "sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// 重点在这里,把当前 Handler 的引用赋值给 msg 的 target
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

------进入消息队列的源码

boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
// p == null 代表前面没有消息, when 是延迟消息的时间值
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//当前消息的 next 是 p 引用,形成一个单链表结构,如果是第一个消息的话,p 为空
msg.next = p;
// 赋值消息到轮询器
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
// 发送了第一个消息后
mMessages 就不为空了
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
收起阅读 »

Android基础-LRU缓存策略

前言缓存策略在Android开发是比较常见的,尤其是在图片使用业务场景中缓存策略发挥重要作用。对于移动应用常用UI就是图片组件而且图片显示资源常常来自网络,因此为了更快的图片加载和减省流量就需要使用缓存策略实现一个完备的图片加载框架。图片加载库的基本逻辑如下:...
继续阅读 »

前言

缓存策略在Android开发是比较常见的,尤其是在图片使用业务场景中缓存策略发挥重要作用。对于移动应用常用UI就是图片组件而且图片显示资源常常来自网络,因此为了更快的图片加载和减省流量就需要使用缓存策略实现一个完备的图片加载框架。

图片加载库的基本逻辑如下:优先从内存中找图片资源;然后从本地资源找图片;最后两者都没有的情况下才从网络中请求图片资源。

请求图片加载
是否有内存缓存
直接加载内存图片资源
是否有本地缓存
加载本地图片资源
请求加载网络图片资源
图片加载完成

内存缓存策略

关于内存缓存策略,在Android原生代码中有LruCache类。LruCache它的核心缓存策略算法是LRU(Least Recently Used)当缓存超出设置最大值时,会优先删除近期使用最少的缓存对象。当然本地存储缓存也能参考LRU策略,两者相结合就能实现一套如上图展示的完善基础资源缓存策略。

LruCache

LruCache是个泛型类,内部主要采用LinkedHashMap<K, V>存储形式缓存对象。提供put和get操作,当缓存超出最大存储值时会移除使用较少的缓存对象,put新的缓存对象。另外还支持remove方法,主动移除缓存对象释放更多缓存存储值。

  • get
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);

if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}

if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
  • put
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
return previous;
}
  • remove
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}

V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, null);
}

return previous;
}

通过put和remove方法会发现不管是添加还是删除都会执行safeSizeOf方法,在safeSizeOf中需要开发者自行实现sizeOf方法计算缓存大小累加到缓存池当中。另外put方法还多了trimToSize方法用来check缓存池是否超出最大缓存值。

收起阅读 »

提高app的响应能力-布局优化

提高app的响应能力-布局优化在刚开始开发安卓的时候,应用无响应(ANR)是很常见的,随着安卓手机的性能和编程能力的提高,现在不会遇到这样低级别的错误了,这篇文档分享如何提高应用程序的响应能力。前言应用响应能力意为用户操作时的速度,也就是让使用者感觉到轻、快、...
继续阅读 »

提高app的响应能力-布局优化

在刚开始开发安卓的时候,应用无响应(ANR)是很常见的,随着安卓手机的性能和编程能力的提高,现在不会遇到这样低级别的错误了,这篇文档分享如何提高应用程序的响应能力。

前言

应用响应能力意为用户操作时的速度,也就是让使用者感觉到轻、快、流畅才行。虽然现在的安卓设备一年比一年强劲,但我们在开发中要避免这些“慢”操作,打造让用户感觉到流畅的应用。

帧率控制

肉眼无法感知超过60FPS的动画

虽然有些证明人眼的感知极限是高于60FPS的,但60FPS的帧率已经完全满足。
那么每一帧的切换就是1000/60 等于16毫秒,手机原本为了保持视觉的流畅度,它的屏幕刷新频率是60hz,所以我们在开发中也应该注意这个时间,处理的间隔应当小于这个时间。

布局优化

GPU过度绘制

在开发者选项中有个一个很好用的功能叫“GPU过度绘制”,它的作用是可视化的显示出过度绘制的区域,那么什么叫过度绘制的区域呢。比如我们组件View是从上到下分布的,最顶部的View如果重合下面的View颜色,就叫做过度绘制。

它通过颜色来表示过度绘制的等级。

1.png

来看看下面的颜色级

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<RelativeLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp">

<RelativeLayout
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp">

<RelativeLayout
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>

Screenshot_2021-11-07-20-12-40-86_2a27335eaa331505125090a61677c0b2.jpg

颜色越浅越好,虽然现在的设备对视图的要求并不高,但为了追求极致,我们在开发中还是应该注意这方面的问题。

GPU 呈现模式分析

开发者选项中-GPU呈现模式分析 这个功能开启之后,可以分析GPU的渲染速度,它对每个应用会单独显示一个图形。

Screenshot_2021-11-07-20-27-26-10_2a27335eaa331505125090a61677c0b2.jpg

注意水平方向的每一个竖条代表一帧,绿色线条代表16毫秒,开发者尽量做到没帧在该竖条以下。但由于复杂的业务和动画,我们经常会超出这个值,所以我们作为参考,在闲暇之余往这个方向去追求。


收起阅读 »

SharedFlowBus:30行代码实现消息总线你确定不看吗

前言最近看到很多关于livedata和flow的文章,大家都在学那我肯定不能落后。便去学习一番,偶得SharedFlowBus(卷死你们)。那么正式开始前我们先大概了解下 StateFlow 和 SharedFlowStateFl...
继续阅读 »

前言

最近看到很多关于livedata和flow的文章,大家都在学那我肯定不能落后。便去学习一番,偶得SharedFlowBus(卷死你们)。

那么正式开始前我们先大概了解下 StateFlow 和 SharedFlow

StateFlow

StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。还可通过其value属性读取当前状态值。

在 Android 中,StateFlow 非常适合需要让可变状态保持可观察的类。

与使用 flow 构建器构建的冷数据流不同,StateFlow 是热数据流:从此类数据流收集数据不会触发任何提供方代码。StateFlow 始终处于活跃状态并存于内存中,而且只有在垃圾回收根中未涉及对它的其他引用时,它才符合垃圾回收条件。

当新使用方开始从数据流中收集数据时,它将接收信息流中的最近一个状态及任何后续状态。您可在 LiveData 等其他可观察类中找到此操作行为。

SharedFlow

SharedFlow 是 StateFlow 的可配置性极高的泛化数据流。您可以使用 SharedFlow 将 tick 信息发送到应用的其余部分,以便让所有内容定期同时刷新。除了获取最新资讯之外,您可能还想要使用用户最喜欢的主题集刷新用户信息部分。

class MainViewModel :  ViewModel() {

private val _sharedFlow = MutableSharedFlow<Int>(0, 1, BufferOverflow.DROP_OLDEST)
val sharedFlow: SharedFlow<Int> = _sharedFlow

init {
viewModelScope.launch {
for (i in 0..10) {
sharedFlow.tryEmit(i)
}
}
}

}

class MainFragment : Fragment() {

private val viewModel: MainViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

CoroutineScope(Dispatchers.Main).launch {
viewModel.sharedFlow.collect {
println(it)
}
}
}

}

您可通过以下方式自定义 SharedFlow 行为:

  • 通过 replay,您可以针对新订阅者重新发送多个之前已发出的值。
  • 通过 onBufferOverflow,您可以指定相关政策来处理缓冲区中已存满要发送的数据项的情况。默认值为 BufferOverflow.SUSPEND,这会使调用方挂起。其他选项包括 DROP_LATEST 或 DROP_OLDEST

MutableSharedFlow 还具有 subscriptionCount 属性,其中包含处于活跃状态的收集器的数量,以便您相应地优化业务逻辑。MutableSharedFlow 还包含一个 resetReplayCache 函数,供您在不想重放已向数据流发送的最新信息的情况下使用。

没错,以上信息摘自 Android Developers ,我真是太能水了,干脆改行写小说得了哈哈哈。

SharedFlowBus的使用

// 发送消息
SharedFlowBus.with(objectKey: Class<T>).tryEmit(value: T)
// 发送粘性消息
SharedFlowBus.withSticky(objectKey: Class<T>).tryEmit(value: T)

// 订阅消息
SharedFlowBus.on(objectKey: Class<T>).observe(owner){ it ->
println(it)
}
// 订阅粘性消息
SharedFlowBus.onSticky(objectKey: Class<T>).observe(owner){ it ->
println(it)
}

通过上面的使用方法可以看出 SharedFlowBus 的优点

  • 使用者不用显示调用反注册方法。
  • 感知生命周期,防止内存泄漏。
  • 实时数据刷新。

SharedFlowBus的实现

object SharedFlowBus {

private var events = ConcurrentHashMap<Any, MutableSharedFlow<Any>>()
private var stickyEvents = ConcurrentHashMap<Any, MutableSharedFlow<Any>>()

fun <T> with(objectKey: Class<T>): MutableSharedFlow<T> {
if (!events.containsKey(objectKey)) {
events[objectKey] = MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
}
return events[objectKey] as MutableSharedFlow<T>
}

fun <T> withSticky(objectKey: Class<T>): MutableSharedFlow<T> {
if (!stickyEvents.containsKey(objectKey)) {
stickyEvents[objectKey] = MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
}
return stickyEvents[objectKey] as MutableSharedFlow<T>
}

fun <T> on(objectKey: Class<T>): LiveData<T> {
return with(objectKey).asLiveData()
}

fun <T> onSticky(objectKey: Class<T>): LiveData<T> {
return withSticky(objectKey).asLiveData()
}

}

源码说明

以上就是 SharedFlowBus 的源码,可以直接拷贝到项目中使用。

收起阅读 »

面试官:你给我说一下线程池里面的几把锁。

你好呀,我是歪歪。 最近有个读者给我说,面试聊到线程池的时候,相谈甚欢,基本都回答上来了,但是其中有一个问题直接把他干懵逼了。 面试官问他:你说一下线程池里面的锁吧。 结果他关于线程池的知识点其实都是在各个博客或者面经里面看到的,没有自己去翻阅过源码,也就根本...
继续阅读 »

你好呀,我是歪歪。


最近有个读者给我说,面试聊到线程池的时候,相谈甚欢,基本都回答上来了,但是其中有一个问题直接把他干懵逼了。


面试官问他:你说一下线程池里面的锁吧。


结果他关于线程池的知识点其实都是在各个博客或者面经里面看到的,没有自己去翻阅过源码,也就根本就没有注意过线程池里面还有锁的存在。


他还给我抱怨:



他这么一说,我也觉得,好像大家聊到线程池的时候,都没有怎么聊到里面用到的锁。


确实是存在感非常低。


要不我就安排一下?



mainLock


其实线程池里面用到锁的地方还是非常的多的。


比如我之前说过,线程池里面有个叫做 workers 的变量,它存放的东西,可以理解为线程池里面的线程。


而这个对象的数据结构是 HashSet。


HashSet 不是一个线程安全的集合类,这你知道吧?


所以,你去看它上面的注释是怎么说的:



当持有 mainLock 这个玩意的时候,才能被访问。


就算我不介绍,你看名字也能感觉的到:如果没有猜测的话,那么 mainLock 应该是一把锁。


到底是不是呢,如果是的话,它又是个什么样子的锁呢?



在源码中 mainLock 这个变量,就在 workers 的正上方:



原来它的真身就是一个 ReentrantLock。


用一个 ReentrantLock 来保护一个 HashSet,完全没毛病。


那么 ReentrantLock 和 workers 到底是怎么打配合的呢?


我们还是拿最关键的 addWorker 方法来说:



用到锁了,那么必然是有什么东西需要被被独占起来的。


你再看看,你加锁独占了某个共享资源,你是想干什么?


绝大部分情况下,肯定是想要改变它,往里面塞东西,对不对?


所以你就按照这个思路分析,addWorker 中被锁包裹起来的这段代码,它到底在独占什么东西?


其实都不用分析了,这里面的共享数据一共就两个。两个都需要进行写入操作,这两共享数据,一个是workers 对象,一个是 largestPoolSize 变量。


workers 我们前面说了,它的数据结构是线程不安全的 HashSet。


largestPoolSize 是个啥玩意,它为什么要被锁起来?



这个字段是用来记录线程池中,曾经出现过的最大线程数。


包括读取这个值的时候也是加了 mianLock 锁的:



其实我个人觉得这个地方用 volatile 修饰一下 largestPoolSize 变量,就可以省去 mainLock 的上锁操作。


同样也是线程安全的。


不知道你是不是也是这样觉得的?


如果你也是这样想的话,不好意思,你想错了。


在线程池里面其他的很多字段都用到了 volatile:



为什么 largestPoolSize 不用呢?


你再看一下前面 getLargestPoolSize 方法获取值的地方。


如果修改为 volatile,不上锁,就少了一个 mainLock.lock() 的操作。


去掉这个操作,就有可能少了一个阻塞等待的操作。


假设 addWorkers 方法还没来得及修改 largestPoolSize 的值,就有线程调用了 getLargestPoolSize 方法。


由于没阻塞,直接获取到的值,只是那一瞬间的 largestPoolSize,不是一定是 addWorker 方法执行完成后的


加上阻塞,程序是能感知到 largestPoolSize 有可能正在发生变化,所以获取到的一定是 addWorker 方法执行完成后的 largestPoolSize。


所以我理解加锁,是为了最大程度上保证这个参数的准确性。


除了前面说的几个地方外,还是有很多 mainLock 使用的地方:



我就不一一介绍了,你得自己去翻一翻,这玩意介绍起来也没啥意思,都是一眼就能瞟明白的代码。


说个有意思的。


你有没有想过这里 Doug Lea 老爷子为什么用了线程不安全的 HashSet,配合 ReentrantLock 来实现线程安全呢?


为什么不直接搞一个线程安全的 Set 集合,比如用这个玩意 Collections.synchronizedSet?


答案其实在前面已经出现过了,只是我没有特意说,大家没有注意到。


就在 mainLock 的注释上写着:



我捡关键的地方给你说一下。


首先看这句:



While we could use a concurrent set of some sort, it turns out to be generally preferable to use a lock.



这句话是个倒装句,应该没啥生词,大家都认识。


其中有个 it turns out to be,可以介绍一下,这是个短语,经常出现在美剧里面的对白。


翻译过来就是四个字“事实证明”。


所以,上面这整句话就是这样的:虽然我们可以使用某种并发安全的 set 集合,但是事实证明,一般来说,使用锁还是比较好的。


接下来老爷子就要解释为什么用锁比较好了。


我翻译上这句话的意思就是我没有乱说,都是有根据的,因为这是老爷子亲自解释的为什么他不用线程安全的 Set 集合。


第一个原因是这样说的:



Among the reasons is that this serializes interruptIdleWorkers, which avoids unnecessary interrupt storms, especially during shutdown. Otherwise exiting threads would concurrently interrupt those that have not yet interrupted.



英文是的,我翻译成中文,加上自己的理解是这样的。


首先第一句里面有个 “serializes interruptIdleWorkers”,这两个单词组合在一起还是有一定的迷惑性的。


serializes 在这里,并不是指我们 Java 中的序列化操作,而是需要翻译为“串行化”。


interruptIdleWorkers,这玩意根本就不是一个单词,这是线程池里面的一个方法:



在这个方法里面进来第一件事就是拿 mainLock 锁,然后尝试去做中断线程的操作。


由于有 mainLock.lock 的存在,所以多个线程调用这个方法,就被 serializes 串行化了起来。


串行化起来的好处是什么呢?


就是后面接着说的:避免了不必要的中断风暴(interrupt storms),尤其是调用 shutdown 方法的时候,避免退出的线程再次中断那些尚未中断的线程。


为什么这里特意提到了 shutdown 方法呢?


因为 shutdown 方法调用了 interruptIdleWorkers:



所以上面啥意思呢?


这个地方就要用一个反证法了。


假设我们使用的是并发安全的 Set 集合,不用 mainLock。


这个时候有 5 个线程都来调用 shutdown 方法,由于没有用 mainLock ,所以没有阻塞,那么每一个线程都会运行 interruptIdleWorkers。


所以,就会出现第一个线程发起了中断,导致 worker ,即线程正在中断中。第二个线程又来发起中断了,于是再次对正在中断中的中断发起中断。


额,有点像是绕口令了。


所以我打算重复一遍:对正在中断中的中断,发起中断。


因此,这里用锁是为了避免中断风暴(interrupt storms)的风险。


并发的时候,只想要有一个线程能发起中断的操作,所以锁是必须要有的。有了锁这个大前提后,反正 Set 集合也会被锁起来,索性就不需要并发安全的 Set 了。


所以我理解,在这里用 mainLock 来实现串行化,同时保证了 Set 集合不会出现并发访问的情况。


只要保证这个这个 Set 操作的时候都是被锁包裹起来的就行,因此,不需要并发安全的 Set 集合。


即注释上写的:Accessed only under mainLock.


记住了,有可能会被考哦。


然后,老爷子说的第二个原因:



It also simplifies some of the associated statistics bookkeeping of largestPoolSize etc.



这句话就是说的关于加锁好维护 largestPoolSize 这个参数,不再贅述了。


哦,对了,这是有个 etc,表示“诸如此类”的意思。


这个 etc 指的就是这个 completedTaskCount 参数,道理是一样的:



另一把锁


除了前面说的 mainLock 外,线程池里面其实还有一把经常被大家忽略的锁。


那就是 Worker 对象。



可以看到 Worker 是继承自 AQS 对象的,它的很多方法也是和锁相关的。



同时它也实现了 Runnable 方法,所以说到底它就是一个被封装起来的线程,用来运行提交到线程池里面的任务,当没有任务的时候就去队列里面 take 或者 poll 等着,命不好的就被回收了。


我们还是看一下它加锁的地方,就在很关键的 runWorker 方法里面:



java.util.concurrent.ThreadPoolExecutor#runWorker




那么问题就来了:


这里是线程池里面的线程,正在执行提交的任务的逻辑的地方,为什么需要加锁呢?


这里为什么又自己搞了一个锁,而不用已有的 ReentrantLock ,即 mainLock 呢?


答案还是写在注释里面:



我知道你看着这么大一段英文瞬间就没有了兴趣。


但是别慌,我带你细嚼慢咽。


第一句话就开门见山的说了:



Class Worker mainly maintains interrupt control state for threads running tasks.



worker 类存在的主要意义就是为了维护线程的中断状态。


维护的线程也不是一般的线程,是 running tasks 的线程,也就是正在运行的线程。


怎么理解这个“维护线程的中断状态”呢?


你去看 Worker 类的 lock 和 tryLock 方法,都各自只有一个地方调用。


lock 方法我们前面说了,在 runWorker 方法里面调用了。


在 tryLock 方法是在这里调用的:



这个方法也是我们的老朋友了,前面刚刚才讲过,是用来中断线程的。


中断的是什么类型的线程呢?



就是正在等待任务的线程,即在这里等着的线程:



java.util.concurrent.ThreadPoolExecutor#getTask




换句话说:正在执行任务的线程是不应该被中断的。


那线程池怎么知道那哪任务是正在执行中的,不应该被中断呢?


我们看一下判断条件:



关键的条件其实就是 w.tryLock() 方法。


所以看一下 tryLock 方法里面的核心逻辑是怎么样的:



核心逻辑就是一个 CAS 操作,把某个状态从 0 更新为 1,如果成功了,就是 tryLock 成功。


“0”、“1” 分别是什么玩意呢?


注释,答案还是在注释里面:



所以,tryLock 中的核心逻辑compareAndSetState(0, 1),就是一个上锁的操作。


如果 tryLock 失败了,会是什么原因呢?


肯定是此时的状态已经是 1 了。


那么状态什么时候变成 1 呢?


一个时机就是执行 lock 方法的时候,它也会调用 tryAcquire 方法。


那 lock 是在什么时候上锁的呢?


runWorker 方法里面,获取到 task,准备执行的时候。


也就是说状态为 1 的 worker 肯定就是正在执行任务的线程,不可以被中断。


另外,状态的初始值被设置为 -1。



我们可以写个简单的代码,验证一下上面的三个状态:



首先我们定义一个线程池,然后调用 prestartAllCoreThreads 方法把所有线程都预热起来,让它们处于等待接收任务的状态。


你说这个时候,三个 worker 的状态分别是什么?



那必须得是 0 ,未上锁的状态。


当然了,你也有可能看到这样的局面:



-1 是从哪里来的呢?


别慌,我等下给你讲,我们先看看 1 在哪呢?


按照之前的分析,我们只需要往线程池里面提交一个任务即可:



这个时候,假如我们调用 shutdown 呢,会发什么?


当然是中断空闲的线程了。


那正在执行任务的这个线程怎么办呢?


因为是个 while 循环,等到任务执行完成后,会再次调用 getTask 方法:



getTask 方法里面会先判断线程池状态,这个时候就能感知到线程池关闭了,返回 null,这个 worker 也就默默的退出了。



好了,前面说了这么多,你只要记住一个大前提:自定义 worker 类的大前提是为了维护中断状态,因为正在执行任务的线程是不应该被中断的。


接着往下看注释:



We implement a simple non-reentrant mutual exclusion lock rather than use ReentrantLock because we do not want worker tasks to be able to reacquire the lock when they invoke pool control methods like setCorePoolSize.



这里解释了为什么老爷子不用 ReentrantLock 而是选择了自己搞一个 worker 类。


因为他想要的是一个不能重入的互斥锁,而 ReentrantLock 是可以重入的。


从前面分析的这个方法也能看出来,是一个非重入的方法:



传进来的参数根本没有使用,代码里面也没有累加的逻辑。


如果你还没反应过来是怎么回事的话,我给你看一下 ReentrantLock 里面的重入逻辑:



你看到了吗,有一个累加的过程。


释放锁的时候,又有一个与之对应的递减的过程,减到 0 就是当前线程释放锁成功:



而上面的累加、递减的逻辑在 worker 类里面通通是没有的。


那么问题又来了:如果是可以重入的,会发生什么呢?


目的还是很前面一样:不想打断正在执行任务的线程。


同时注释里面提到了一个方法:setCorePoolSize。


你说巧不巧,这个方法我之前写线程池动态调整的时候重点讲过呀:



可惜当时主要讲 delta>0 里面的的逻辑去了。


现在我们看一下我框起来的地方。


workerCountOf(ctl.get()) > corePoolSize 为 true 说明什么情况?


说明当前的 worker 的数量是多于我要重新设置的 corePoolSize,需要减少一点。


怎么减少呢?


调用 interruptIdleWorkers 方法。


这个方法我们前面刚刚分析了,我再拿出来一起看一下:



里面有个 tryLock,如果是可以重入的,会发生什么情况?


是不是有可能把正在执行的 worker 给中断了。


这合适吗?



好了,注释上的最后一句话:



Additionally, to suppress interrupts until the thread actually starts running tasks, we initialize lock state to a negative value, and clear it upon start (in runWorker).



这句话就是说为了在线程真正开始运行任务之前,抑制中断。所以把 worker 的状态初始化为负数(-1)。


大家要注意这个:and clear it upon start (in runWorker).


在启动的时候清除 it,这个 it 就是值为负数的状态。


老爷子很贴心,把方法都给你指明了:in runWorker.


所以你去看 runWorker,你就知道为什么这里上来先进行一个 unLock 操作,后面跟着一个 allow interrupts 的注释:



因为在这个地方,worker 的状态可能还是 -1 呢,所以先 unLock,把状态刷到 0 去。


同时也就解释了前面我没有解释的 -1 是哪里来的:



想明白了吗,-1 是哪里来的?


肯定是在启动过程中,执行了 workers.add 方法,但是还没有来得及执行 runWorker 方法的 worker 对象,它们的状态就是 -1。



最后说一句


好了,看到了这里了,点赞安排一个吧。写文章很累的,需要一点正反馈。


给各位读者朋友们磕一个了:



作者:why技术
链接:https://juejin.cn/post/7025456717055918094
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

还在频繁定义常量?不试试用枚举代替

1、简介 不知道大家有没有在自己项目中看到过类似下面这样的代码: public static void fruitsHandle(String fruits) { switch (fruits) { case "Apple": ...
继续阅读 »

1、简介


不知道大家有没有在自己项目中看到过类似下面这样的代码:


public static void fruitsHandle(String fruits) {

switch (fruits) {
case "Apple":
// TODO
break;
case "Banana":
// TODO
break;
case "Orange":
// TODO
break;
default:
throw new IllegalStateException("Unexpected value: " + fruits);
}

}

出现上面这种情况是非常少的,小萌新一般也不会直接在方法中重复定义字符串进行比较,而会将其定义为常量,或者统一抽取为常量类。所以一般会看到这种代码(小捌经常在项目中看到类似这样的代码,但是小捌不敢吭声😄😄):


private static final String APPLE = "Apple";
private static final String BANANA = "Banana";
private static final String ORANGE = "Orange";

public static void fruitsHandle(String fruits) {

switch (fruits) {
case APPLE:
// TODO
break;
case BANANA:
// TODO
break;
case ORANGE:
// TODO
break;
default:
throw new IllegalStateException("Unexpected value: " + fruits);
}

}

上面这种情况我们在代码中出现的频率非常高;它需要程序员提供一组固定常量,并且这一组固定常量在开发时或者说编译时就知道了具体的成员,这个时候我们就应该使用枚举。


枚举类型(enum type)是指由一组固定常量组成合法值的类型。




2、优势


使用枚举类型,相比直接定义常量能够带来非常多的好处。




2.1 类型安全


分别定义一个简单的肉类枚举和水果枚举


// 肉类枚举
public enum MeetEnums {

BEEF,
PORK,
FISH;

}

// 水果枚举
public enum FruitsEnums {

APPLE,
BANANA,
ORANGE;

}

我们改造上面的代码,修改入参类型即可


public static void fruitsHandle(FruitsEnums fruits) {

switch (fruits) {
case APPLE:
// TODO
break;
case BANANA:
// TODO
break;
case ORANGE:
// TODO
break;
default:
throw new IllegalStateException("Unexpected value: " + fruits);
}

}

可以看到定义枚举类型带来函数类型安全性,如果定义的是常量则无法代理这种效果



2.2 枚举能够提供更多信息


枚举在本质上还是一个类,它能够定义属性和方法,我们可以在枚举类中定义想要的方法、或者通过属性扩展枚举提供的基础信息。


比如我们做web开发时最常见的HttpStatus,在springframework框架中就被定义成了枚举类,它不仅包含了Http响应码,还能包含描述状态。


public enum HttpStatus {

OK(200, "OK"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error");

private final int value;
private final String reasonPhrase;

private HttpStatus(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}

}

2.3 通过函数提供更多服务


此外HttpStatus它内部还嵌套了Series枚举类,这个类可以协助HttpStatus枚举类,通过statusCode / 100的模判断当前的枚举状态是is1xxInformational、is2xxSuccessful、is3xxRedirection、is4xxClientError、is5xxServerError等等。


public static enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);

private final int value;

private Series(int value) {
this.value = value;
}

public int value() {
return this.value;
}

public static HttpStatus.Series valueOf(HttpStatus status) {
return valueOf(status.value);
}

public static HttpStatus.Series valueOf(int statusCode) {
HttpStatus.Series series = resolve(statusCode);
if (series == null) {
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
} else {
return series;
}
}

@Nullable
public static HttpStatus.Series resolve(int statusCode) {
int seriesCode = statusCode / 100;
HttpStatus.Series[] var2 = values();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
HttpStatus.Series series = var2[var4];
if (series.value == seriesCode) {
return series;
}
}

return null;
}
}

2.4 获取所有定义的类型


所有的枚举类会自动产生一个values()方法,它能返回当前定义枚举类的数组集,因此可以很方便的遍历怎么枚举类定义的所有枚举。比如我们简单改造一下MeetEnums枚举类:


public enum MeetEnums {

BEEF("牛肉"),
PORK("猪肉"),
FISH("鱼肉");

String name;

public String getName() {
return name;
}

MeetEnums(String name) {
this.name = name;
}

public static MeetEnums getMeetEnumsByName(String name) {
MeetEnums[] values = values();
Optional<MeetEnums> optional = Stream.of(values).filter(v -> v.getName().equals(name)).findAny();
return optional.isPresent() ? optional.get() : null;
}

}

总之枚举类相比常量来说有太多的优点,它能使得代码更加整洁美观、安全性强、功能强大。虽然大部分情况下,枚举类的选择是由于常量定义的,但是也并不是任何时候都一定要把常量定义成枚举;具体情况大家就可以自己去斟酌啦!


作者:李子捌
链接:https://juejin.cn/post/7025394182210453540
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

前言 初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层...
继续阅读 »

前言


初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层,代码的阅读体验非常糟糕,而且如果不小心删除了一个括号要找半天才对应得上。当然,通过 VSCode 彩虹括号(Rainbow Brackets)这个插件能够一定程度上解决括号对称查找得问题,但是代码的可维护性、阅读体验还是很差。自然而然,大家会想到拆分。拆分有两种方式,一种是使用返回Widget 的函数,另一种是使用 StatelessWidget,那这两种该如何选择呢?


image.png


拆分原则


在关于这个问题的讨论上,2年前 StackOverflow 有一个经典的回答:使用函数和使用类来构建可复用得组件有什么区别?,大家可以去看看。其中提到得一个关键因素是 Flutter 框架能够检测组件树的类对象,从而提高复用性。而对于私有的方法来说 Flutter 在更新的时候并不知道该如何处理。


image.png


答主也对比了使用类和函数的优劣势。使用类构建的方式:



  • 支持性能优化,比如使用 const 构造方法,更细颗粒度的刷新;

  • 两个不同的布局切换时,能够正确地销毁对应得资源。这个我们在上篇讲 StatefulWidget 的时候有介绍过。

  • 保证正确的方式进行热重载,而使用函数可能破坏热重载。

  • 在 Widget Inspector 中可以查看得到,从而可以方便我们定位和调试问题。

  • 更友好的错误提示。当组件树出现错误时,框架会给出当前构建得组件名称,而如果使用函数的话则得不到清晰得名词。

  • 可以使用 key 提高性能。

  • 可以使用 context 提供的方法(函数式组件除非显示地传递 context)。


使用函数构建组件唯一的优势就是代码量会更少(这可以通过 functional_widget 插件解决,functional_widget 是一个通过注解将和函数式组件构建方式自动转换为类组件的代码生成插件)。


示例对比


下面我们看一段没有拆分的代码,这个仅仅是示例代码,没有任何实际意义。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}

括号有点多,对吧,一眼看过去都懵圈了 —— 这也是很多初次接触 Flutter 的人吐槽地方,可以说让不少人直接放弃了! 最直接的方式就是将部分代码抽离成为一个私有方法,比如像下面这样。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
_buildNonsenseWidget(),
],
);
}
}

将深度嵌套的组件代码单独抽成了一个返回 Widget 的私有方法,看起来确实让代码简洁不少。
那么问题就解决了吗?我们来看一下当状态改变的时候会发生什么。
我们知道,当状态变量_counter改变后,Flutter 会调用 build 方法刷新组件。这会导致 _buildNonsenseWidget 这个方法在刷新的时候每次都会被调用,意味着每次都会创建新的组件来替换旧的组件,即便两个组件没有任何改变。而事实上,我们应该只重建那些变化的组件,从而提高性能。
现在再来看使用类组件的方式,实际上有代码模板的情况下,编写一个 StatelessWidget 非常简单。使用类组件后的代码如下所示。代码确实会比函数的方式多,但是实际上大部分不需要我们手敲。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),

// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}

class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();

@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}

这里注意,以为这个_NonsenseWidget 在组件得声明周期不会改变,因此使用了 const 的构造方法。这样在刷新过程中,就不会重新构建了!关于 const 可以参考之前的两篇文章。


关于 StatefulWidget,你不得不知道的原理和要点!


解密 Flutter 的 const 关键字


总结


相比使用函数构建复用的组件代码,请尽可能地使用类组件的方式,而且尽可能地将组件拆分为小一点的单元。这样一方面可以提供精确的刷新,另一方面则是可以将组件复用到其他页面中。如果你不想改变自己得习惯,那么可以考虑使用 functional_widget 这个插件来自动生成类组件。


作者:岛上码农
链接:https://juejin.cn/post/7027987302710247454
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

可折叠式标题栏

CollapsingToolbarLayout顾名思义,这是一个作用在Toolbar上的布局,但是要注意的是CollapsingToolbarLayout不能单独存在,它必须要作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是C...
继续阅读 »

CollapsingToolbarLayout

顾名思义,这是一个作用在Toolbar上的布局,但是要注意的是CollapsingToolbarLayout不能单独存在,它必须要作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout(监听子控件的事件,做出合理的响应)的子布局。所以可以得到:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appbar">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_width="match_parent"
android:layout_height="match_parent">

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

接着为了让标题栏高级一些,在CollapsingToolbarLayout中放一张图片和一个Toolbar

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appbar">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:id="@+id/fruit_image_view"/>

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

这里有一个layout_collapseMode属性,表示在折叠过程中的折叠样式,parallax表示在折叠过程中会产生错位偏移,而pin表示会始终不变。

NestedScrollView

标题栏完成之后,就是开始编写水果内容的部分了。这里使用NestedScrollView,这和ScrollView,RecyclerView一样都是可以通过滚动的方式来查看屏幕外的数据。同样的和之前的RecyclerView一样,这里也要指定一个布局行为。 由于NestedScrollView和ScrollView一样,只允许存在一个直接子布局,这里就可以嵌套一个LinearLayout作为它的直接子布局。然后在LinearLayout中放具体的内容就可以了。

    <androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.cardview.widget.CardView
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>

</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

这里添加了一个布局行为(和之前的RecyclerView一样)。 为了让之前的知识尽可能用到,这里再加一个悬浮按钮。

    <com.google.android.material.floatingactionbutton.FloatingActionButton
android:src="@drawable/ic_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"/>

这是目前的效果图在这里插入图片描述接着就是通过逻辑代码将数据给填进去了。

MainActivity→FruitActivity

由于数据是在MainActivity得到的,FruitActivity并不能得到数据,所以需要通过MainActivity将数据传输过去,这里可以用Intent来传输。

        ViewHolder viewHolder=new ViewHolder(view);
viewHolder.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(context,FruitActivity.class);
int position=viewHolder.getAdapterPosition();
Fruit fruit=FruitList.get(position);
intent.putExtra("fruitName",fruit.getName());
intent.putExtra("fruitId",fruit.getId());
context.startActivity(intent);
}
});

这里选择在每次生成ViewHolder的时候就为其中的cardView绑定点击事件,将id和name传递给FruitAcitivity。

FruitActivity进行数据处理。

       ImageView imageView=findViewById(R.id.fruit_image_view);
TextView textView=findViewById(R.id.fruit_content_text);
Toolbar toolbar=findViewById(R.id.toolbar);
//将ActionBar换成toolbar
setSupportActionBar(toolbar);
ActionBar actionBar=getSupportActionBar();
if(actionBar!=null){
actionBar.setDisplayHomeAsUpEnabled(true);
}
CollapsingToolbarLayout collapsingToolbarLayout=findViewById(R.id.collapsing_toolbar);
Intent intent=getIntent();
String name=intent.getStringExtra(fruitName);
int id=intent.getIntExtra(fruitId,0);
//用Glide来加载图片
Glide.with(this).load(id).into(imageView);
textView.setText(ExtraText(name));
//为可折叠标题栏设置标题。
collapsingToolbarLayout.setTitle(name);

充分利用状态栏空间。

为ImageView的父布局和其本身添加fitsSystemWindows属性。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>

<com.google.android.material.appbar.AppBarLayout
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appbar">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:fitsSystemWindows="true"
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:id="@+id/fruit_image_view"/>

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.cardview.widget.CardView
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>

</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:src="@drawable/ic_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

为FruitActivity自定义一个主题。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FruitActivityTheme" parent="Theme.ListView">
<item name="android:statusBarColor">
@android:color/transparent
</item>
</style>
</resources>

引入这个主题

 <activity android:name=".FruitActivity" android:theme="@style/FruitActivityTheme">
收起阅读 »

基础巩固——多线程

多线程编程Android沿用了Java的线程模型,一个Android应用在创建时会开启一个线程,常称作主线程,也叫做UI线程,如果有请求网络等耗时操作时,就需要开启子线程去处理。因此,此文对多线程进行梳理总结线程基础1. 进程与线程这两个的区分在我的另一篇文章...
继续阅读 »

多线程编程

Android沿用了Java的线程模型,一个Android应用在创建时会开启一个线程,常称作主线程,也叫做UI线程,如果有请求网络等耗时操作时,就需要开启子线程去处理。因此,此文对多线程进行梳理总结


线程基础

1. 进程与线程

这两个的区分在我的另一篇文章# Android面向面试复习-操作系统+计网篇中已经提及,简单复习一下。进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。进程可以看作程序的实体,也是线程的容器。线程是操作系统调度的最小单元,一个进程中可以创建多个线程。这些线程拥有各自的计数器,堆栈,局部变量等属性,并且能够访问共享的内存变量


2. 线程的状态

Java线程在运行的生命周期可能会处于六种不同的状态,如下

  • New:新创建状态,线程被创建,还没有调用start方法,运行之前还有一些基础工作
  • Runnable:可运行状态,一旦调用start方法,就会处于Runnable状态。处于这个状态的线程可能正在运行,也可能没有,取决于操作系统的调度
  • Blocked:阻塞状态,表示线程被锁阻塞,暂不能活动
  • Waiting:等待状态,线程暂时不活动,并且不运行任何代码,这消耗最少的资源,知道调度器重新激活这个线程
  • Timed Waiting:超时等待,可以在指定的时间自行返回
  • Terminated:终止状态,表示当前线程已经执行完毕,比如run方法正常执行退出,或者因为没有被捕获的异常而终止

3. 创建线程

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法
  • 实现Callable接口,重写call方法

4. 理解中断

当线程的run方法执行完毕,或者方法里出现没有捕获的异常时,线程就要终止。早期Java版本中有stop方法可以终止线程,现在已经被弃用。现版本用interrupt来中断线程,当一个线程调用interrupt方法时,它的中断标志位将被置为true。线程会时不时的检测这个中断标记位,以判断线程是否应该被中断,要想知道线程是否被置位,可以调用isInterrupted方法查看返回值。还可以调用静态方法interrupted来对中断标志位进行复位。但是如果一个线程被阻塞,就无法检测中断状态。如果一个线程处于阻塞状态,那么线程在检查中断标志位时若发现中断标志位为true,就会在阻塞方法调用处抛出阻塞异常,并且在抛出异常前将线程中断标志位复位,即重新设置为false。需要注意的是被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定如何响应中断。如果是比较重要的线程,则不会理会中断。而大部分情况是线程会将中断作为一个终止的请求。另外,不要在底层代码里捕获InterruptedException不做处理,这里介绍两种合适的处理方式

  1. 在catch子句中,调用Thread.currentThread().interrupt()来设置中断状态。因为在抛出异常后中断标志位会复位,让外界通过判断isInterrupted()来决定是终止还是继续下去
void test(){
try{
sleep(50);
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
  1. 更好的做法是直接抛出异常,方便调用者捕获
void test() throw InterruptedException{
sleep(50);
}

5. 安全的终止线程

上一点我们提到了中断,首先用中断来终止线程,如下

public class test{
public static void main(String[] args) throws InterruptedException {
MyRunner runner = new MyRunner();
Thread thread = new Thread(runner, "MyRunner");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
thread.interrupt();
}

public static class MyRunner implements Runnable{
private long i;

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
i++;
System.out.println("i=" + i);
}
System.out.println("stop");
}
}
}

代码里用sleep方法使得main线程沉睡10ms,留给MyRunner足够的时间来感知中断从而结束,还可以采用boolean变量来控制是否需要停止线程,如下

public class test{
public static void main(String[] args) throws InterruptedException {
MyRunner runner = new MyRunner();
Thread thread = new Thread(runner, "MyRunner");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
runner.cancel();
}

public static class MyRunner implements Runnable{
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on){
i++;
System.out.println("i=" + i);
}
System.out.println("stop");
}

public void cancel(){
on = false;
}
}
}

结果如下,两段代码是类似的

image.png

此处说明线程执行到了run方法的末尾,即将终止


线程同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取,如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况被称为竞态条件。此时如果不用同步,是无法保证数据原子性的,所以我们就需要用到锁


1. 重入锁与条件对象

synchronized关键字自动提供了锁以及相关条件。大多数需要显示锁的情况使用synchronized非常方便。但是等我们了解了重入锁和条件对象时,能更好的理解synchronized关键字。重入锁ReentrantLock是Java SE 5.0引入的,就是支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。具体结构如下

Lock mLock = new ReentrantLock();
mLock.lock();
try {

}catch (){

}finally {
mLock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是同一时刻只有一个任务访问的代码区域。一旦一个线程封锁了锁对象,其他线程都无法进入。把解锁操作放到finally区域内是十分必要的,如果因为某些异常,锁资源是必须要释放的,否则其他资源将被永久阻塞。进入临界区时,却发现在某一个条件满足之后它才能执行,这时可以用一个条件对象来管理那些已经获得了一把锁但是却不能做有用工作的线程,条件对象又被称作条件变量。通过下面例子来说明为何需条件对象。假设一个场景需要用支付宝转账,我们先写支付宝类,它的构造方法需传入支付宝账户的数量和每个账户的账户金额。

public class Alipay{
private double[] accounts;
private Lock alipayLock;
public Alipay(int n, double money){
accounts = new double[n];
alipayLock = new ReentrantLock();
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}
}

接下来实现转账,需要一个from转账方,和to接收方,amount是转账金额,如下

public void transfer(int from, int to, int amount){
alipayLock.lock();
try {
while (accounts[from] < amount){
//wait
}
}catch (){

}finally {
alipayLock.unlock();
}
}

有可能会出现转账方余额不足的情况,如果有其他线程给这个转账方再转足够的钱,就可以转账成功了,但是这个线程已经获取了锁,具有排他性,别的线程无法获取锁来进行存款操作,这时我们就需要引入对象锁。一个锁对象拥有多个相关条件对象,可以用new Condition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁,相关代码如下

public class Alipay{
private double[] accounts;
private Lock alipayLock;
private Condition condition;
public Alipay(int n, double money){
accounts = new double[n];
alipayLock = new ReentrantLock();
condition = alipayLock.newCondition();
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}

public void transfer(int from, int to, int amount) throws InterruptedException{
alipayLock.lock();
try {
while (accounts[from] < amount){
condition.await();
}
}catch (){

}finally {
alipayLock.unlock();
}
}
}

一旦一个线程调用await方法,就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的signalAll()方法时为止。当另一个线程转账给我们此前的转账方时,只重复调用singnalAll()方法,就会重新激活因为这一条件而等待的所有线程,代码如下

public void transfer(int from, int to, int amount) throws InterruptedException{
alipayLock.lock();
try {
while (accounts[from] < amount){
condition.await();
}
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
condition.signalAll();
}catch (){

}finally {
alipayLock.unlock();
}
}

当调用了signalAll时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞态实现对对象的访问,还有个方法是signal,它则是随机解除某个线程的阻塞。如果该线程仍然不能运行,则再次被阻塞,如果没有其他线程再次调用signal,那么系统就死锁了


2. 同步方法

Lock接口和Condition接口为程序设计提供了高度的锁定控制,然而大多数情况下并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。Java中每一个对象都有一个内部锁,如果一个方法用synchronized关键字修饰,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程必须获得内部的对象锁,如下

public synchronized void method(){
···
}

这段代码等价于

Lock mLock = new ReentrantLock();
public void method(){
mLock.lock();
try{
···
}finally{
mLock.unlock();
}
}

对于上面转账的例子,可以将Alipay的transfer方法声明为synchronized,而不是使用一个显示的锁。内部对象锁只有一个相关条件,wait方法将一个线程添加到等待集中,使用notifyAll或notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.await(),notifyAll等价于signalAll,所以前面例子里的transfer方法也可以这么写

public synchronized void transfer(int from, int to, int amount) throws InterruptedException{
while (accounts[from] < amount){
wait();
}
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
notifyAll();
}

在此可以看到,使用sychronized关键字来编码要简练很多,由该锁来管理那些试图进入synchronized方法的线程,由该锁中的条件来管理那些调用wait的线程


3. 同步代码块

除了调用同步方法来获得锁,还可以通过使用同步代码块,如下

synchronized(obj){
···
}

其获得了obj的锁,obj是一个对象,我们用同步代码块进行改写上面的例子

public class Alipay{
private double[] accounts;
private Object lock = new Object();
private Condition condition;
public Alipay(int n, double money){
accounts = new double[n];
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}

public synchronized void transfer(int from, int to, int amount) throws InterruptedException{
synchronized (lock){
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
}
}
}

在这里创建了一个名为lock的Object类,为的是使用Object类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用,一般实现同步最好用Java的并发包下的集合类,比如阻塞队列。如果同步方法适合自己的程序,尽量使用同步方法,这样可以减少编写代码的数量,减少出错的概率,如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition


4. volatile

有时,仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大,而volatile关键字为实例域的同步访问提供了免锁机制。如果声明一个域为volatile的话,那么编译器和虚拟机知道该域是可能被另一个线程并发更新的。当一个共享变量被volatile关键字修饰后,就具备了两个含义,一个含义是线程修改了变量的值时,变量的新值对于其他线程是立即可见的。另一个含义是禁止使用指令重排序,分为编译期重排序和运行时重排序。先来看一段代码,假设线程1先执行,2后执行,如下

//线程1
boolean stop = false;
while(!stop){
//doSomething
}

//线程2
stop = true;

这是一个线程中断的代码,但是这段代码不一定会将线程中断,虽说无法中断线程这个情况出现的概率很小,但是一旦发生便是死循环。因为每个线程都有私有的工作内存,因此线程1运行时会拷贝一份stop的值放入私有工作内存中,当线程2更改了stop的变量值并返回后,线程2突然需要做其他操作,这时就无法将更改的stop变量写入主存中,这样线程1就不知道线程2对stop变量进行了更改,因此线程1会一直执行下去。当stop用volatile修饰,线程2修改stop值时,会强制将修改的值立刻写入主存,这样使得线程1的工作内存中的stop变量缓存无效,这样线程1在此读取变量stop的值时就会去主存读取

volatile不保证原子性

另外volatile不保证原子性,可看如下代码演示

class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}

public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(() -> {
for(int j=0;j<1000;j++)
test.increase();
}).start();
}
//保证前面的线程都执行完
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(test.inc);
}
}

这段代码每次运行的结果都不一致,因为自增操作是不具备原子性的。自增操作里包含了读取原始值、加1、写入工作内存这三个子操作,也就是说这三个子操作可能被割裂执行。

volatile保证有序性

volatile关键字能禁止指令重排序,因此能保证有序性。禁止指令重排序有两层含义,其一是指代码运行到volatile变量操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还未执行。其二是进行指令优化时,在volatile变量之前的语句不能在volatile变量之后执行

正确使用volatile关键字

synchronized关键字可防止多个线程同时执行一段代码,但是这会很影响程序的执行效率。volatile关键字在有些时候会优于synchronized关键字。但是要注意volatile关键字时无法替代synchronized关键字的,因为其无法保证原子性,通常来说,使用volatile关键字需要具备以下两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

关于第一点,就是上面提到的自增自减操作。关于第二点,举个例子,包含一个不变式:下界总是小于或等于上界,代码如下

public class NumberRange {
private volatile int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}

这种方式定义的upper和lower并不能充分实现类的线程安全,如果两个线程在同一时间使用不一致的值执行setLower和setUpper的话,就会使范围处于不一致的状态。例如,如果初始状态是(0,5),同一时间内,两个线程分别调用setLower(4)和setUpper(3),虽然这两个交叉存入的值是不符合条件的,但是这两个线程都会通过用于保护不变式的检查,使得最后范围是(4,3),显然是不对的


收起阅读 »

使用DialogFragment代替Dialog

使用DialogFragment代替Dialog是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。DialogFra...
继续阅读 »

使用DialogFragment代替Dialog

是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。

DialogFragment是什么

DialogFragment从它的源码得知,它继承了Fragment,其实是一个比较特殊的Fragment。那么它相对于普通的Dialog有什么不同,谷歌又为什么推荐我们使用它呢,它相对于普通的Dialog有什么优点呢。

使用过它之后用自己的感受描述:

  • 它的生命周期很清晰,方便写复杂的逻辑
  • 它于Activity的生命周期是绑定的,Activity消失,DialogFragment也会消失。
  • 它可以很简单的控制弹窗的布局。

总结就是dialogfragment能更好的管理dialog的展示与消失,以及在屏幕旋转时的一些状态保存问题。

DialogFragment的踩坑

即使它有很多的优点,但使用不当时,仍然会有很多坑。
我遇到了很多奇奇怪怪的问题。 比如

  • Fragment already added 异常
  • 快速的显示消失,无法消失的异常

当然除此之外我们还可能有以下需求:

  • 设置对话框的大小
  • 设置弹出对话框时背景灰色或者透明

下面我们就来一一实现。

如何实现DialogFragment

重点来关注两个方法。

  • onCreateDialog 新建一个Dialog即可使用
  • onCreateView 自定义一个Dialog界面

onCreateDialog

Screenshot_2021-11-04-22-04-55-27_2a27335eaa331505125090a61677c0b2.jpg

做一个简单的对话框

public class ConfirmDialog extends DialogFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("提示")
.setMessage("确认要退出吗")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
}).create();
return dialog;
}
}

显示它

ConfirmDialog dialog = new ConfirmDialog();
dialog.show(getSupportFragmentManager(), "dialogTag");

onCreateView

使用自定义视图做一个加载框。
这里有一个非常重要的地方,我出现了 Fragment already added 的问题,意思就是重复添加了,那么为什么会出现重复添加呢,因为我最初的代码是使用isAddedisVisibility进行判断,但是当快速执行的时候,这两个方法并不准确。

正确的做法有两种。

  • 添加事务时先进行移除
beginTransaction().remove(this).commit()
  • 使用变量进行判断,不能使用isAdded
private boolean isShowFragment = false;

@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
// 解决bug:Android java.lang.IllegalStateException: Fragment already added
if (this.isShowFragment) {
return;
}
this.isShowFragment = true;
super.show(manager, tag);
}

@Override
public void dismiss() {
super.dismiss();
this.isShowFragment = false;
}

// 避免有些手机兼容性问题,isShowFragment未变成false而导致无法二次打开
@Override
public void onDestroy() {
super.onDestroy();
this.isShowFragment = false;
}

最后直接放代码,封装好的Loading框

LoadingDialog 对话框 可以看到代码中对bug的处理:在每个add事务前增加一个remove事务,防止连续的add。

public class LoadingDialog extends DialogFragment
implements DialogInterface.OnKeyListener {
/**
* 加载框提示信息 设置默认
*/
private final String hintMsg = "加载中...";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.MyDialog);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Dialog dialog = getDialog();
// 设置背景透明
if (dialog.getWindow() != null)
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
// 去掉标题
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(false);
View loadingView = inflater.inflate(R.layout.dialog_loading, container);
TextView hintTextView = loadingView.findViewById(R.id.tv_ios_loading_dialog_hint);
hintTextView.setText(hintMsg);
//不响应返回键
dialog.setOnKeyListener(this);
return loadingView;
}

@Override
public void show(FragmentManager manager, String tag) {
try {
//在每个add事务前增加一个remove事务,防止连续的add
manager.beginTransaction().remove(this).commit();
super.show(manager, tag);
} catch (Exception e) {
//同一实例使用不同的tag会异常,这里捕获一下
e.printStackTrace();
}
}

@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
// return keyCode == KeyEvent.KEYCODE_BACK;
// 允许按back键取消Loading
return false;
}

}

代理管理类

public class GlobalDialogManager {

private LoadingDialog mLoadingDialog;

private GlobalDialogManager() {
init();
}

public static GlobalDialogManager getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final GlobalDialogManager INSTANCE = new GlobalDialogManager();
}

public void init() {
if (mLoadingDialog == null) {
mLoadingDialog = new LoadingDialog();
}
}

/**
* 展示加载框
*/
public synchronized void show(FragmentManager manager) {
if (manager != null && mLoadingDialog != null) {
mLoadingDialog.show(manager, "tag");
}
}

/**
* 隐藏加载框
*/
public synchronized void dismiss(FragmentManager manager) {
if (mLoadingDialog != null && !manager.isDestroyed()) {
mLoadingDialog.dismissAllowingStateLoss();
}
}
}

使用它

if (getContext() != null)
GlobalDialogManager.getInstance().show(((Activity) getContext()).getFragmentManager());

if (getContext() != null)
GlobalDialogManager.getInstance().dismiss(((Activity) getContext()).getFragmentManager());

这里判断getContext()很有必要,避免Activity消失了,getContext为空的bug。

背景不变暗设置一个style属性行啦。

<item name="android:backgroundDimEnabled">false</item><!--activity不变暗-->

收起阅读 »

栈的实现

一、栈 💦 栈的概念及结构 栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。 栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对...
继续阅读 »

一、栈


💦 栈的概念及结构


栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对应多种出栈顺序

栈有两个经典的操作


1️⃣ 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。


2️⃣ 出栈:栈的删除操作叫做出栈。出数据也在栈顶 。


在这里插入图片描述


💦 栈的实现


这里对于栈的实现我们既可以选择数组也可以和选择链表两者的效率都差不多,但是还是建议使用数组
在这里插入图片描述


1.初始化

函数原型


在这里插入图片描述


函数实现


void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

2.插入

函数原型


在这里插入图片描述


函数实现


void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}

3.判空

函数原型


在这里插入图片描述


函数实现


bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}

4.删除

函数原型


在这里插入图片描述


函数实现


void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}

5.长度

函数原型


在这里插入图片描述


函数实现


int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}

6.栈顶

函数原型


在这里插入图片描述


函数实现


STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}

7.销毁

函数原型


在这里插入图片描述


函数实现


void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

💦 完整代码


这里需要三个文件


1️⃣ Static.h,用于函数的声明


2️⃣ Static.c,用于函数的定义


3️⃣ Test.c,用于测试函数




🧿 Stack.h

#pragma once

//头
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

//结构体
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a; //指向动态开辟的空间
int top; //栈顶
int capacicy; //容量
}ST;

//函数
//注意链表和顺序表我们写Print,但是栈不写,因为如果栈可以Print的话,就不符合后进先出了
//初始化
void StackInit(ST* ps);
//插入
void StackPush(ST* ps, STDatatype x);
//判空
bool StackEmpty(ST* ps);
//删除
void StackPop(ST* ps);
//长度
int StackSize(ST* ps);
//栈顶
STDatatype StackTop(ST* ps);
//销毁
void StackDestory(ST* ps);

🧿 Stack.c

#include"Stack.h"

void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}
void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}
bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}
void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}
int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}
STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}
void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

🧿 Test.c

#include"Stack.h"

int main()
{
ST st;
//初始化
StackInit(&st);
//插入+删除
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPop(&st);
StackPop(&st);
//长度
StackSize(&st);
//栈顶
StackTop(&st);
//销毁
StackDestory(&st);
return 0;
}

作者:跳动的bit
链接:https://juejin.cn/post/7026874977597014023
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一篇文章了解Java之网络编程

一、网络基础知识 网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。 计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。 IP地址:具有全球唯一性,...
继续阅读 »

一、网络基础知识


网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。


计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。


IP地址:具有全球唯一性,相对于internet,IP为逻辑地址。


IP地址分类:


1. A类地址


A类地址第1字节为网络地址,其它3个字节为主机地址。另外第1个字节的最高位固定为0。


A类地址范围:1.0.0.1到126.155.255.254。


A类地址中的私有地址和保留地址:


10.0.0.0到10.255.255.255是私有地址(所谓的私有地址就是在互联网上不使用,而被用在局域网络中的地址)。


127.0.0.0到127.255.255.255是保留地址,用做循环测试用的。


2. B类地址


B类地址第1字节和第2字节为网络地址,其它2个字节为主机地址。另外第1个字节的前两位固定为10。


B类地址范围:128.0.0.1到191.255.255.254。


B类地址的私有地址和保留地址


172.16.0.0到172.31.255.255是私有地址


169.254.0.0到169.254.255.255是保留地址。如果你的IP地址是自动获取IP地址,而你在网络上又没有找到可用的DHCP服务器,这时你将会从169.254.0.0到169.254.255.255中临得获得一个IP地址。


3. C类地址


C类地址第1字节、第2字节和第3个字节为网络地址,第4个个字节为主机地址。另外第1个字节的前三位固定为110。


C类地址范围:192.0.0.1到223.255.255.254。


C类地址中的私有地址:


192.168.0.0到192.168.255.255是私有地址。


4. D类地址


D类地址不分网络地址和主机地址,它的第1个字节的前四位固定为1110。


D类地址范围:224.0.0.1到239.255.255.254


Mac地址:每个网卡专用地址,也是唯一的。


端口(port):OS中可以有65536(2^16)个端口,进程通过端口交换数据。连线的时候需要输入IP也需要输入端口信息。


计算机通信实际上的主机之间的进程通信,进程的通信就需要在端口进行联系。


192.168.0.23:21


协议:为了进行网络中的数据交换(通信)而建立的规则、标准或约定。


不同层的协议是完全不同的。


网络层:寻址、路由(指如何到达地址的过程)


传输层:端口连接


TCP模型:应用层/传输层/网络层/网络接口


端口是一种抽象的软件结构,与协议相关:TCP23端口和UDT23端口为两个不同的概念。


端口应该用1024以上的端口,以下的端口都已经设定功能。


TCP/IP模型


Application


(FTP,HTTP,TELNET,POP3,SMPT)


Transport


(TCP,UDP)


Network


(IP,ICMP,ARP,RARP)


Link


(Device driver,….)


注:


IP:寻址和路由


ARP(Address Resolution Protocol)地址解析协议:将IP地址转换成Mac地址


RARP(Reflect Address Resolution Protocol)反相地址解析协议:与上相反


ICMP(Internet Control Message Protocol)检测链路连接状况。利用此协议的工具:ping , traceroute


二、TCP Socket


TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。


   1) 服务器分配一个端口号,服务器使用accept()方法等待客户端的信号,信号一到打开socket连接,从socket中取得OutputStream和InputStream。


   2) 客户端提供主机地址和端口号使用socket端口建立连接,得到OutputStream和InputStream。


TCP/IP的传输层协议


1、 建立TCP服务器端


创建一个TCP服务器端程序的步骤:


    1). 创建一个ServerSocket


    2). 从ServerSocket接受客户连接请求


    3). 创建一个服务线程处理新的连接


    4). 在服务线程中,从socket中获得I/O流


    5). 对I/O流进行读写操作,完成与客户的交互


    6). 关闭I/O流


    7). 关闭Socket


ServerSocket server = new ServerSocket(post)


Socket connection = server.accept();


ObjectInputStream put=new ObjectInputStream(connection.getInputStream());


ObjectOutputStreamo put=newObjectOutputStream(connection.getOutputStream());  


处理输入和输出流;


关闭流和socket。


2、 建立TCP客户端


创建一个TCP客户端程序的步骤:


1).创建Socket


    2). 获得I/O流


    3). 对I/O流进行读写操作


    4). 关闭I/O流


    5). 关闭Socket


Socket connection = new Socket(127.0.0.1, 7777);

ObjectInputStream input=new ObjectInputStream(connection.getInputStream());

ObjectOutputStream utput=new ObjectOutputStream(connection.getOutputStream());


处理输入和输出流;


关闭流和socket。


三、 建立UDP连接


UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。


         比较:TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输;既然有了保证可靠传输的TCP协议,为什么还要非可靠传输的UDP协议呢?主要的原因有两个。一是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。二是在许多应用中并不需要保证严格的传输可靠性,比如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。


如:http://www.tarena.com.cn:80/teacher/zhu…


协议名://机器名+端口号+文件名


2 . URL类的常见方法


一个URL对象生成后,其属性是不能被改变的,但是我们可以通过类URL所提供的方法来获取这些属性:


   public String getProtocol() 获取该URL的协议名。


   public String getHost() 获取该URL的主机名。


   public int getPort() 获取该URL的端口号,如果没有设置端口,返回-1。


   public String getFile() 获取该URL的文件名。


   public String getRef() 获取该URL在文件中的相对位置。


   public String getQuery() 获取该URL的查询信息。


   public String getPath() 获取该URL的路径


   public String getAuthority() 获取该URL的权限信息


   public String getUserInfo() 获得使用者的信息


   public String getRef() 获得该URL的锚



  1. 例子,将tarena网站首页拷贝到本机上。


import java.net.*;

import java.io.*;

import java.util.*;



public class TestURL{



         public static void main(String[] arg){



                   System.out.println("http://www.tarena.com.cn:80/index.htm===>");

                   //System.out.println(getWebContent());

                   writeWebFile(getWebContent());

             }

   

             public static String getWebContent(){

   

                       URL url = null;

                       HttpURLConnection uc = null;

                       BufferedReader br = null;

                       final int buffLen = 2048;

                       byte[] buff = new byte[buffLen];

                       String message = "";

                       String tmp = "";

                       int len = -1;



                       String urlStr = "http://www.tarena.com.cn:80/index.htm";

                      

                       try{

                             url = new URL(urlStr);

                             //连接到web资源

                             System.out.println("before openConnection ====>"+new Date());

                             uc = (HttpURLConnection)url.openConnection();

                             System.out.println("end openConnection ====>"+new Date());

                             br = new BufferedReader( new InputStreamReader(uc.getInputStream()));

                             System.out.println("end getINputStream() ====>"+new Date());



                             while( ( tmp = br.readLine())!=null){

                                    message += tmp;

                             }

                             System.out.println("end set message ====>"+new Date());



                       }catch(Exception e){e.printStackTrace();System.exit(1);}

                       finally{



                             if(br!=null){

                                    try{

                                           br.close();

                                    }catch(Exception ioe){ioe.printStackTrace();}

                             }

                    }



                    return  message;

    }

   

    public static void writeWebFile(String content){

   

             FileWriter fw = null;

             try{

                       fw = new FileWriter("index.htm");

                       fw.write(content,0,content.length());

             }catch(Exception e){

                       e.printStackTrace();

             }finally{

                       if(fw!=null){

                                try{

                                         fw.close();

                                }catch(Exception e){}

                       }

             }

            

    }

}


四、UDP socket


这种信息传输方式相当于传真,信息打包,在接受端准备纸。


特点:




  1. 基于UDP无连接协议




  2. 不保证消息的可靠传输




  3. 它们由Java技术中的DatagramSocket和DatagramPacket类支持




DatagramSocket(邮递员):对应数据报的Socket概念,不需要创建两个socket,不可使用输入输出流。


DatagramPacket(信件):数据包,是UDP下进行传输数据的单位,数据存放在字节数组中,其中包括了目标地址和端口以及传送的信息(所以不用建立点对点的连接)。


DatagramPacket的分类:


用于接收:DatagramPacket(byte[] buf,int length)


                      DatagramPacket(byte[] buf,int offset,int length)


用于发送:DatagramPacket(byte[] buf,int length, InetAddress address,int port )


                      DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port)


注:InetAddress类网址用于封装IP地址


没有构造方法,通过


InetAddress.getByAddress(byte[] addr):InetAddress


InetAddress.getByName(String host):InetAddress


等。


1、建立UDP 发送端


创建一个UDP的发送方的程序的步骤:


    1). 创建一个DatagramPacket,其中包含发送的数据和接收方的IP地址和端口


号。


    2). 创建一个DatagramSocket,其中包含了发送方的IP地址和端口号。


    3). 发送数据


    4). 关闭DatagramSocket


byte[] buf = new byte[1024];

DatagramSocket datagramSocket = new DatagramSocket(13);// set port

DatagramPacket intputPacket = new DatagramPacket (buf,buf.length);

datagramSocket.receive(inputPacket);

DatagramPacket  outputPacket = new DatagramPacket (buf,buf.length,

inetAddress,port);

datagramSocket.send(outputPacket);


没建立流所以不用断开。


2、 建立UDP 接受端


创建一个UDP的接收方的程序的步骤:


    1). 创建一个DatagramPacket,用于存储发送方发送的数据及发送方的IP地址和端口号。


    2). 创建一个DatagramSocket,其中指定了接收方的IP地址和端口号。


    3). 接收数据


    4). 关闭DatagramSocket


byte[] buf = new byte[1024];

DatagramSocket datagramSocket = new DatagramSocket();//不用设端口,因为发送的包中端口

DatagramPacket outputPacket=new DatagramPacket(

Buf, buf.length, serverAddress, serverPort);

DatagramPacket inputPacket=new DatagramPacket(buf, buf.length);

datagramSocket.receive(inputPacket);


URL类:可直接送出或读入网络上的数据。


Socket类:可以想象为两个不同主机的程序通过网络的通信信道。


Datagram类:它把数据的目的记录放在数据包中,然后直接放到网络上。


InetAddress类:用来存储internet地址的类(ip地址,域名)。



作者:zhulin1028
链接:https://juejin.cn/post/7026870482116804644
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

熬夜再战Android-Button实现selector选择器

前提 这是小空熬夜写的Android新手向系列,欢迎品尝。 selector是按钮最常用的功能,对美化控件的作用很大。 上节我们说了selector和shape联合使用,但偏向shape的介绍,今天主要说selector。 👉实践过程 我们先按照上一节的sha...
继续阅读 »

前提


这是小空熬夜写的Android新手向系列,欢迎品尝。


selector是按钮最常用的功能,对美化控件的作用很大。


上节我们说了selector和shape联合使用,但偏向shape的介绍,今天主要说selector。


👉实践过程


我们先按照上一节的shape方式创建两个shape背景

btn_selector_shape1.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- 圆角 -->
    <corners android:radius="5dp" />
    <!--填充颜色-->
    <solid android:color="#00ff00" />
</shape>

btn_selector_shape2.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!--圆角-->
    <corners android:radius="5dp" />
    <!--填充颜色-->
    <solid android:color="#0000ff" />
</shape>

接着我们在【res-drawable】右键创建个Drawable Resource File ,弹出框写文件名创建文件,设置默认【Root element】为selector。


btn_selector0.xml


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/btn_selector_shape1" android:state_pressed="true" />
    <item android:drawable="@drawable/btn_selector_shape2" android:state_window_focused="false" />
</selector>

布局中引用


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".TextActivity">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="160dp"
        android:background="@drawable/btn_selector0"
        android:text="按下变色"
        android:textColor="@color/white" />
</RelativeLayout>

我们运行下看看


image.png


image.png


但是


我们回忆下,刚才是不是创建了三个文件,按钮少的情况下还好,自定义的按钮一多,这么多文件非常不容易管理,所以我们要用另外一种写法,将所有内容放到一个文件中。


我们在刚才的btn.selector0.xml中修改:


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--这是第一种方式,利用drwable引用文件-->
    <!--<item android:drawable="@drawable/btn_selector_shape1" android:state_pressed="true" />-->
    <!--<item android:drawable="@drawable/btn_selector_shape2" android:state_pressed="false" />-->
    <!--第二种方式如下-->
    <item android:state_pressed="false">
        <shape android:shape="rectangle">
            <!-- 圆角 -->
            <corners android:radius="5dp" />
            <!--填充颜色为白色-->
            <solid android:color="#0000ff" />
        </shape>
    </item>
    <!--单击时是一个带圆角,白色背景,绿色边框的矩形-->
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <!--圆角-->
            <corners android:radius="5dp" />
            <!--填充颜色为白色-->
            <solid android:color="#00ff00" />
        </shape>
    </item>
</selector>

我们运行起来看看,哎,效果很正确啊


Selector的属性不止这两个哦:



  • state_focused 布尔值,是否获得焦点

  • state_window_focused 布尔值,是否获得窗口焦点

  • state_enabled 布尔值,控件是否可用

  • state_checkable 布尔值,控件可否被勾选

  • state_checked 布尔值,控件是否被勾选

  • state_selected 布尔值,控件是否被选择,针对有滚轮的情况

  • state_pressed 布尔值,控件是否被按下

  • state_active 布尔值,控件是否处于活动状态

  • state_singlestate_firststate_middle很少使用,知道就行。

作者:芝麻粒儿
链接:https://juejin.cn/post/7026880838784516103
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

View 事件分发机制详解

View事件传递过程先从Activity-->Window-->View。public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; ...
继续阅读 »

View事件传递过程先从Activity-->Window-->View。

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
//1
if (onInterceptTouchEvent(ev)) {
//2
consume = onTouchEvent(ev);
} else {
//3
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

这个都是事件分发的伪代码,

  • 注释1调用自己ViewGroup的onInterceptTouchEvent方法是否需要拦截此事件
  • 拦截事件调用注释2此View自己消耗调用方法onTouchEvent方法
  • 注释1不拦截这调用注释3子View的dispatchTouchEvent方法

接着我们继续分Activity的dispatchTouchEvent方法,代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//1
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//2
return onTouchEvent(ev);
}
  • 注释1 可以看出,Activity把事件传递给Window的superDispatchTouchEvent方法让Window处理事件,可看出如果Window不消费事件
  • 注释2 Activity的onTouchEvent方法。我们继续看Window是如何实现的,Window是个抽象类,具体实现者是PhoneWindow

源码如下。

//1
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
//2
return mDecor.superDispatchTouchEvent(event);
}
  • 注释1 mDecor是DecorView类的引用,DecorView继承FrameLayout
  • 注释2 调用ViewGroup的superDispatchTouchEvent方法

接着我们继续分析superDispatchTouchEvent源码,其最终调用了ViewGroup的dispatchTouchEvent方法,代码如下:

 public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//2
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//3
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
return handled;
}
  • 注释1处当事件ACTION_DOWN时进行初始化(mFirstTouchTarget、mGroupFlags重置),事件是由ACTION_DOWN开始以ACTION_UP结束,这样做的目的在于每次事件不会因为之前设置受影响
  • 注释2处 mFirstTouchTarget(后面细讲)表示子元素是否处理事件,如果处理则不为空。如果ViewGroup拦截事件,在ACTION_MOVE和ACTION_UP状态时都不会调用onInterceptTouchEvent方法且intercept为true,ViewGroup消费这些事件
  • 注释3处 子View通过ViewGroup的requestDisallowInterceptTouchEvent方法告知父容器不要拦截事件,让子View处理。 接着继续分析dispatchTouchEvent方法代码如下:
    @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//1
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//2
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//3
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//4
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
return handled;
}
  • 注释1处 倒叙遍历ViewGroup子元素,最上面一层最先遍历
  • 注释2处 判断子View是否接收事件,触摸点是否在子View上
  • 注释3处 调用了dispatchTransformedTouchEvent方法,如果返回值为true,表示有子View消费事件
  • 注释4处 调用addTouchTarget方法具体源码如下
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
//1
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//2
target.next = mFirstTouchTarget;
//3
mFirstTouchTarget = target;
return target;
}

把需要消费事件的View包装成TouchTarget类的单链表结构。

  • 注释1处 从缓存池拿TouchTarget对象
  • 注释2处 TouchTarget类的target对象作为头指针,target的next指向mFirstTouchTarget引用
  • 注释3处 将target对象赋值给mFirstTouchTarget即成为单链表的表头

mFirstTouchTarget: 存储本轮(ACTION_DOWN到ACTION_UP)需要消耗事件的子View的单链表,也是单链表表头。
我们继续分析ViewGroup类dispatchTouchEvent方法最后一部分代码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
//2
while (target != null) {
final TouchTarget next = target.next;
//3
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//4
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
return handled;
}

根据mFirstTouchTarget链表头指针,判断事件交给谁来处理。

  • 注释1处 没有子View来处理事件,则交由自己处理
  • 注释2处 while循环遍历单链表
  • 注释3处 判断是否已经消费此事件
  • 注释4处 事件分发给子View的dispatchTouchEvent方法

接着我们继续分析这个神秘的方法dispatchTransformedTouchEvent方法源码如下:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (child == null) {
//1
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//2
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
  • 注释1处 如果ViewGroup没有子元素则调用父类View的dispatchTouchEvent方法,把事件交给自己处理
  • 注释2处 调用子元素的dispatchTouchEvent方法,实现事件的分发

继续看View的dispatchTouchEvent方法源码实现:

public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
//1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
  • 注释1处 判断mOnTouchListener不为空则调用onTouch方法,则不会调用onTouchEvent方法,说明onTouch方法调用的优先级高于它
  • 注释2处调用onTouchEvent方法

继续分析onTouchEvent方法源码如下:

 public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
break;
}
return true;
}
return false;
}

只要View是CLICKABLE和LONG_CLICKABLE这个状态,都会返回true表示要消费。接着我们看看点击操作,代码如下:

public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//1
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}

我们从注释1处看出,如果设置了点击事件这个回调onClick方法,说明onTouchEvent方法优先级高于onClick方法。

以上都是所有分析View事件分发机制


作者:程序员喵大人
链接:https://juejin.cn/post/7026690811085455397
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

学习笔记-Retrofit源码解析

挖掘Retrofit:2.8.0源码。介绍Retrofit如何完成对OkHttp的封装,以及Retrofit如何支持的协程。1. BuilderRetrofit通过Retrofit.Builder创建,主要是配置各种工厂Factory。val retrofit...
继续阅读 »

挖掘Retrofit:2.8.0源码。介绍Retrofit如何完成对OkHttp的封装,以及Retrofit如何支持的协程。

1. Builder

Retrofit通过Retrofit.Builder创建,主要是配置各种工厂Factory

val retrofit = Retrofit.Builder()
.baseUrl("this is baseUrl")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()

Builder.builder()主要有五个步骤:

  1. 获取Platform对象,Platform是平台适配器,主要为了跨平台使用,类如安卓平台实现就是Android
  2. 初始化CallFactroyCallFactroy的作用是生产realCall,网络请求由他发出。默认值是OkHttpOKHttpClient
  3. 初始化Executor,默认通过Platform获取。
  4. 初始化CallAdapter.FactroyCallAdapter.Factroy生产CallAdapterCallAdapterrealCall的适配器,通过对realCall的包装,实现Executorjava8Futruerxjava等调度方法。存在多个,顺序是自定义配置-->默认配置。默认配置通过Platform获得。
  5. 初始化Converter.FactoryConverter.Factory生产ConverterConverter是数据转换器,将返回的数据转换为需要的数据,一般转换为我们要用的对象。存在多个,顺序是内置配置-->自定义配置-->默认配置。默认配置通过Platform获得。
class Builder{

Builder(Platform platform) {
this.platform = platform;
}

public Builder() {
//step 0
this(Platform.get());
}

public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}

// step1
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();
}

//step2
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
callbackExecutor = platform.defaultCallbackExecutor();
}

//step3
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

//step4
List<Converter.Factory> converterFactories = new ArrayList<>(
1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
converterFactories.add(new BuiltInConverters());
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(platform.defaultConverterFactories());

return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}
}

到这里,Retrofit对象创建完成,一个大致的结构如下图:

retrofit结构.png

2. 创建API对象

Retrofit通过定义网络请求的接口设置请求参数和返回类型,通过调用retrofit.create()创建这个接口的对象,调用这个对象的方法生成最终的网络请求。

interface MyService {
@GET("/user")
fun getUser(): Observable<Response<User>>
}

val myServiceClass: MyService = retrofit.create(MyService::class.java)

进入到create方法,可以看到是直接调用Proxy.newProxyInstance()方法创建出对象。这是标准库提供的动态代理机制,在运行时创建接口的实例对象。

Proxy.newProxyInstance()方法有三个参数:

  • classLoader: 类加载器

  • interfaces:需要实现的接口

  • InvocationHandler:代理方法

public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

为了更好的理解动态代理,可以当做动态代理自动帮我们生成一个接口的实现类。将所有的方法都通过handler.invoke()代理,如下面的代码所示。只不过动态代理是运行时生成的这个类,并且是直接生成了字节码。

interface MyService {
@GET("/user")
fun getUser(): Observable<Response<User>>

@GET("/name")
fun getName(userId: String): Observable<Response<String>>
}

//自动生成的代码示例
class SuspendServiceProxy implements SuspendService {
InvocationHandler handler;

@NonNull
@Override
public Observable<Response<User>> getUser() {
return handler.invoke(
this,
SuspendService.class.getMethod("getUser", String.class),
new Object[]{}
);
}

@NonNull
@Override
public Observable<Response<String>> getName(@NonNull String userId) {
return handler.invoke(
this,
SuspendService.class.getMethod("getName", String.class),
new Object[]{userId}
);
}
}

3. 创建请求对象

创建好API对象之后,就可以调用它的方法创建请求对象。

val observable = myServiceClass.getUser()

根据前面可以知道,这个方法代理给了InvocationHandler,在这个方法首先判断这个方法对象是不是实体对象,如果是的话就直接调用就行。

如果不是一个对象,就把调用再代理给ServiceMethod。首先调用loadServiceMethod()创建ServiceMethod对象,然后调用invoke()方法得到返回值。

public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
// 判断这个方法对象是不是实体对象,如果是的话就直接调用
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
// 判断这个方法对象是不是实体对象,如果是的话就直接调用
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}

3.1. 创建ServiceMethod对象

ServiceMethodRetrofit内部的自定义的代理方法,实际的逻辑是交给它处理。

abstract class ServiceMethod<T> {
abstract @Nullable T invoke(Object[] args);
}

loadServiceMethod()方法获取ServiceMethod对象。

方法对象Method作为Key缓存ServiceMethod对象在Retrofit中,因为创建ServiceMethod是一个耗时过程,所以弄成单例模式。

如果拿不到缓存,就调用ServiceMethod.parseAnnotations()创建一个ServiceMethod对象。

private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();


ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;

synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = ServiceMethod.parseAnnotations(this, method);
serviceMethodCache.put(method, result);
}
}
return result;
}

ServiceMethod.parseAnnotations()中只做了一件事,创建RequestFactory然后将创建ServiceMethod的工作又交给了子类HttpServiceMethod处理。

RequestFactory是接口的参数配置,通过解析接口的注解,返回值,入参及其注解等获得这些参数。

之后将解析完成的数据传递给HttpServiceMethed,由它继续创建ServiceMethod

abstract class ServiceMethod<T> {
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
...
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}

abstract @Nullable T invoke(Object[] args);
}

3.1.1. 创建RequstFactory

RequestFactory内部也是一个Builder模式。主要做了两件事:

  1. 遍历接口注解,初始化配置参数,这里读取的是POSTGET等注释。
  2. 遍历入参及其注解,将入参转换为ParameterHandler对象,将每个参数的设置配置的逻辑代理给了它处理。
final class RequestFactory {
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new RequestFactory.Builder(retrofit, method).build();
}


}
}

3.1.2.2. OkHttpCall

最终创建的网络请求对象是OkHttpCall

OkHttpCall实现了Retrofit.Call接口,这个接口与OkHttp.Call基本一致,这里只介绍它的三个方法:

  1. execute()同步发起请求并且返回请求体Response
  2. enqueue()异步发起请求,通过Callback通信,需要注意的是处理的回调也是在异步中调用的。
  3. cancel()取消请求。

查看OkHttpCall的实现,可以发现所有的Call接口方法的具体实现都是代理给了rawCall

cancel()直接代理给rawCall.cancel()

execute()代理给rawCall.execute(),将返回值交给parseResponse()转换了一次。

enqueue()代理给rawCall.enqueue(),多加了一层Callback回调,在成功回调中也是交给parseResponse()转换之后再回调给原始的Callback

也就是说OkHttpCall把所有的逻辑静态代理给了rawCall,这样做的好处是可以在对应的地方做一下额外的处理,也就是获得返回值通过parseResponse()转换数据。

fAndroid平台默认的CallBackExecutorPlatform的实现类Android中,将Runnable抛到MainHandler中,实现回调到主线程。
static final class Android extends Platform {

@Override public Executor defaultCallbackExecutor() {
return MainThreadExecutor();
}

static class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());

@Override public void execute(Runnable r) {
handler.post(r);
}
}
}
3.1.2.3.2. RxJava2CallAdapterFactory

RxJava2CallAdapterFactory只在返回类型是Observable之类的时候创建CallAdapter

public final class RxJava2CallAdapterFactory extends CallAdapter.Factory {

@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
Class<?> rawType = getRawType(returnType);

if (rawType != Observable.class) { //省略了其他类型
return null;
}

boolean isResult = false;
boolean isBody = false;
Type responseType;


Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
Class<?> rawObservableType = getRawType(observableType);
if (rawObservableType == Response.class) {
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
}
...
return new RxJava2CallAdapter(...);
}
}

RxJava2CallAdapter中的adapt()Call封装成observable返回。

final class RxJava2CallAdapter<R> implements CallAdapter<R, Object> {

@Override public Object adapt(Call<R> call) {
Observable<Response<R>> observable = new CallExecuteObservable<>(call);
...
return RxJavaPlugins.onAssembly(observable);
}
}

最后进到CallExecuteObservable,在启动的时候调用call.execute()并将结果抛给观察者。

final class CallExecuteObservable<T> extends Observable<Response<T>> {
private final Call<T> originalCall;

CallExecuteObservable(Call<T> originalCall) {
this.originalCall = originalCall;
}

@Override protected void subscribeActual(Observer<? super Response<T>> observer) {
Call<T> call = originalCall.clone();
try {
...
Response<T> response = call.execute();
observer.onNext(response);
}
}
}

3.2. 回顾

  1. Api接口实例动态代理HttpServiceMethod
  2. 网络请求的真实执行者是OkHttpClient创建的RealCall
  3. Api接口的调用到RealCall直接有两层静态代理 OkHttpCallCallAdapter
  4. OkHttpCall代理了RealCall,额外调用Converter做序列化处理。
  5. CallAdapter代理了OkHttpCall,可以在这里做扩展,将Call转换为实际的返回类型。

4. 协程实现

在看Retrofit如何实现协程之前,先梳理一下协程的基本概念。

当有一个延迟任务,后续的逻辑又需要等待这个任务执行完成返回数据,能继续执行,为了不阻塞线程,一般就需要就需要通过线程调度和传递回调来通信。在使用了协程之后,却只需要像同步代码那样书写,就可以完成这些操作。

但这并不是什么黑魔法,并不是用了协程之后不需要线程调度和传递回调,而是将这些繁琐的事进行复杂的封装并且为我们自动生成。将回调封装成Continuation,将线程的调度封装成调度器。

Continuation ,调用它的 resume 或者 resumeWithException 来返回结果或者抛出异常,跟我们所说的回调一模一样。

调度器的本质是一个协程拦截器,它拦截的对象就是Continuation,进而在其中实现回调的调度。调度器一般使用现成的,类如Dispatchers.Main,如果去挖它的源码,你会发现到了最后,还是使用的handler.post(),也跟我们所说的线程调度一模一样。

而前面有讲到Retrofit的实现很多时候需要依据返回类型做不同的处理,所以就需要了解协程是如何自动生成的回调代码和如何传递回调。写一个简单的协程接口,看一下转换后的Java代码,以及尝试在Java代码中调用协程接口。

可以看到返回值String被封装成了Continuation<String>作为入参传递,思考一下回调不也是这样实现的。

真实的返回值成了Object(用于状态机状态切换)。

//Kotlin代码
interface SuspendService {
suspend fun C(c1: Long): String
}

//字节码转化的Java代码
public interface SuspendService {
@Nullable
Object C(long var1, @NotNull Continuation var3);
}

//尝试在Java中调用suspend方法
class MMM {
SuspendService service;

public static void main(String[] args) {
MMM mmm = new MMM();
mmm.service.C(1L, new Continuation<String>() {
@NonNull
@Override
public CoroutineContext getContext() {
return null;
}

@Override
public void resumeWith(@NonNull Object o) {

}
});
}
}

接着回到动态代理那部分,因为suspend生成的代码会多加一个回调参数Continuation,那么动态代理的时候这个参数就会传入到代理的handler中。

Continuation的创建和使用十分的繁琐,最好的处理方法应该是把它再丢进一个kotlinsuspend方法中,让编译器去处理这些东西,而这个也就是Retrofit实现协程的原理。

interface SuspendService {
@GET("/user")
suspend fun getUser(): Response<User>

@GET("/name")
suspend fun getName(userId: String): Response<String>
}

//动态代理生成字节码示例
class SuspendServiceProxy implements SuspendService {
InvocationHandler handler;

@Nullable
@Override
public Object getUser(@NonNull Continuation<? super Response<User>> $completion) {
return handler.invoke(
this,
SuspendService.class.getMethod("getUser", Continuation.class),
new Object[]{$completion}
);
}

@Nullable
@Override
public Object getName(@NonNull String userId, @NonNull Continuation<? super Response<String>> $completion) {
return handler.invoke(
this,
SuspendService.class.getMethod("getName", String.class, Continuation.class),
new Object[]{userId, $completion}
);
}
}

接着再回到HttpServiceMethod,看看刚才被省略的代码。

在这里面会判断是不是suspend方法,判断的逻辑在RequestFactory中,判断的方法就是判断参数有没有Continuation对象,感兴趣可以去RequestFactory源码瞅瞅。

现在如果是suspend方法,会直接自定义一个类型adapterType,它的实际类型是Call,泛型是实际的返回类型(Response<T>里面的T)。之后将他作为返回类型去创建CallAdapter,而这里实际创建的就是DefaultCallAdapterFactoryExecutorCallbackCall。最后创建SuspendForResponse对象返回。

abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT> {

static <ResponseT, ReturnT> retrofit2.HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {

/**
* 通过判断参数是不是Continuation,标志函数是不是suspend
* 在requestFactory内部处理
*/

boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
...
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType = Utils.getParameterLowerBound(0,
(ParameterizedType) parameterTypes[parameterTypes.length - 1]);

if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
}
/**
* 自己新建一个返回类型,将实际的返回例行包装给Call
*/

adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
} else {
adapterType = method.getGenericReturnType();
}

CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
Type responseType = callAdapter.responseType();

Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);

okhttp3.Call.Factory callFactory = retrofit.callFactory;
if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else {
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
}
}
}

SuspendForResponse中,adapt()的返回类型对应到了suspend的返回类型Object。并且其中的逻辑就是解析出CallContinuation对象,然后有调用KotlinExtensions.awaitResponse(),就如之前说的,它是一个suspend方法,在代理中不处理Continuation,而是交给编译器去处理。

static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;

SuspendForResponse(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
Converter<ResponseBody, ResponseT> responseConverter,
CallAdapter<ResponseT, Call<ResponseT>> callAdapter) {
super(requestFactory, callFactory, responseConverter);
this.callAdapter = callAdapter;
}

@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);

Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
...
return KotlinExtensions.awaitResponse(call, continuation);
}
}

KotlinExtensions.awaitResponse()Call的扩展函数,扩展函数的实现是通过静态方法传入this的方法实现的,所以前面传入awaitResponse()的参数有两个,分别是Call对象和Continuation对象。

KotlinExtensions.awaitResponse()的主体是suspendCancellableCoroutine方法,suspendCancellableCoroutine运行在协程当中并且帮我们获取到当前协程的 CancellableContinuation 实例,CancellableContinuation是一个可取消的Continuation。通过调用它的 invokeOnCancellation 方法可以设置一个取消事件的回调,一旦这个回调被调用,那么意味着调用所在的协程被取消了,这时候我们也要相应的做出取消的响应,也就是把OkHttp发出去的请求给取消掉。这段建议多读几遍。

之后调用Call.enqueue()发送网络请求,在Callback中调用CancellableContinuation的 resume 或者 resumeWithException 来返回结果或者抛出异常。

这里的Callback也是经过了callAdapterOkHttpCall处理,乏了。

suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

//扩展函数示例
fun <T> Call<T>.awaitResponse(){
toString()
}

//扩展函数示例转换为Java代码
public static final void awaitResponse(@NotNull Call $this$awaitResponse) {
$this$awaitResponse.toString();
}

收起阅读 »

大厂Android高频问题:谈谈Activity的启动模式?

前言Activity可以说是Android开发面试高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Activity启动模式时,他最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!启动模式是什...
继续阅读 »

前言

Activity可以说是Android开发面试高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Activity启动模式时,他最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!

  1. 启动模式是什么?
  2. 启动模式如何设置?
  3. Activity的启动模式区别?
  4. 应用场景以及哪些注意的点?

1.activity堆栈流程以及四种启动模式

一个应用由多个Activity构成,多个Activity构成了任务,系统以栈方式进行管理任务(也就是管理多个Activity),管理方式为“先进后出”。

默认情况下,当用户点击App图标后,启动应用,这时会创建一个任务栈,并且将MAIN Activity压入栈中,作为栈底Activity。之后每启动一个Activity,就会将这个Activity压入栈中,显示处于栈顶的Activity。当用户点击“返回”键后,处于栈顶的Activity进行出栈销毁。

Android提供四种Activity的启动模式来进行入栈操作。

standard:

默认值,启动Activity都会重新创建一个Activity的实例进行入栈。此时Activity可能存在多个实例。

image.png

singleTop:

当Activity处于栈顶时,再启动此Activity,不会重新创建实例入栈,而是会使用已存在的实例。

image.png

singleTask:

与singleTop模式相似,只不过singleTop模式是只是针对栈顶的元素,而singleTask模式下,如果task栈内存在目标Activity实例,则:

  1. 将task内的对应Activity实例之上的所有Activity弹出栈。
  2. 将对应Activity置于栈顶,获得焦点。

image.png

singleInstance:

这是我们最后的一种启动模式,也是我们最恶心的一种模式:在该模式下,我们会为目标Activity分配一个新的affinity,并创建一个新的Task栈,将目标Activity放入新的Task,并让目标Activity获得焦点。新的Task有且只有这一个Activity实例。

如果已经创建过目标Activity实例,则不会创建新的Task,而是将以前创建过的Activity唤醒(对应Task设为Foreground状态)

image.png

2.启动模式如何设置?

AndroidMainfest.xml文件设置

设置的lanuchMode属性。可设置四个值: standard、singleTop、singleTask、singleInstance。若不设置默认为standard。

<activity 
android:name=".activity.MainActivity"
android:launchMode="standard"/>

Intent跳转标记Flag

FLAG_ACTIVITY_SINGLE_TOP 等价于 singleTop。位于栈顶的Activity会重用实例,调用onNewIntent函数接收intent。

Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);

FLAG_ACTIVITY_SINGLE_NEW_TASK,启动新的TASK,这个新的TASK取决于xml中设置的TaskAffinity(亲和性)属性。

首先去寻找是否存在相同亲和性的任务,如果存在,那么直接将这个Activity加入到这个任务中。若不存在,则新建一个任务来加入Activity。

FLAG_ACTIVITY_CLEAR_TOP,会将位于此Activity上方的Activity进行出栈销毁。

// singleTask的行为可使用代码表示为
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

两者区别:

  • xml设置为静态的
  • intent标记是动态的。intent标记Flag的优先级更高一些。所以当标记Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP后,尽管Activity为默认的standard模式,也同样会使用存在的实例,调用onNewIntent。

3.亲和性和多个任务并存

亲和性是指Activity设置在AndroidMainfest.xml中的taskAffinity属性。相同亲和性的Activity在同一个任务中,默认使用application的taskAffinity,也就是package name。

并不是设置taskAffinity就一定起作用,起作用是有条件的:

  • 同时设置了launchMode属性为singleTask。
  • Intent跳转时使用FLAG_ACTIVITY_NEW_TASK。
  • 同时设置allowTaskReparenting属性为true。
  • allowTaskReparenting可以使此Activity从启动任务中转移到该taskAffinity的任务中。此时需要发生Task reset(回到Home之后再进入app)才能看出效果。

不同亲和性意味着不同的任务,也就是同一个app中可以存在不同的任务,前台显示的任务的栈顶Activity为用户可见的Activity。当启动一个新的任务时,新的任务会覆盖当前任务。并且回退时,一个任务中Activity全部出栈,会将后台的任务调出,直到最后一任务的最后一个Activity出栈,app结束,回到Home。

例如: 一个应用有Main、A、B、C四个Activity,C的lanuchMode为singleTask,并且taskAffinity设置为.c,其他都为默认,那么按照启动顺序: Main->A->B->C

此时存在两个task:

  • 默认: Main->A->B (后台)
  • .c: C (前台)

由C启动A,那么此时task为:

  • 默认: Main->A->B->A (前台)

  • .c: C (后台)

按回退键:

  • 出栈顺序为: A、B、A、Main、C

4.应用场景以及需要避免的坑

  1. 新闻客户端的推送,点击打开新闻详情页,此时新闻详情页应该设置singleTop,避免用户在新闻详情页打开推送通知,使得回退出现两次详情页。

  2. 利用singleTask的特性,可以使得应用完全退出。

    • 注意:闪屏页+主页+其他的应用,可以设置主页为singleTask,因为闪屏页展示完就finish掉,栈底存在主页,用户点击回退键可以直接关闭应用。

    • 坑:避免启动MAIN的MainActivity设置为singleTask,这样当用户点击HOME,再重新启动应用时,将始终展示MainActivity,并且此时MainActivity走onNewIntent方法。

  3. singleInstance使用比较少,系统应用比如打电话可使用singleInstance。

  4. singleTop、singleTask、singleInstance在使用已存在的Activity实例时,都将走onNewIntent方法。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 小新聊Android 』,不定期分享原创知识
  3. 同时可以期待后续文章ing🚀

收起阅读 »

当我们讨论Android重建机制时在讨论什么?

前言Android应用有一个常常被忽略的问题,但问题出现时你又不得不面对。比如Activity横竖屏转换时Fragment重影应用长时间处于后台,并用户重新切到前台时,Activity显示异样或者需要等待一段时间才能显示内容这类问题都与Activity的恢复重...
继续阅读 »

前言

Android应用有一个常常被忽略的问题,但问题出现时你又不得不面对。比如

  • Activity横竖屏转换时Fragment重影

  • 应用长时间处于后台,并用户重新切到前台时,Activity显示异样或者需要等待一段时间才能显示内容

这类问题都与Activity的恢复重建机制相关,如果你想知道怎么解决这类问题,以及Activity恢复重建机制内部原理。这篇文或许能够帮到你。

1) 什么时候会重建

并不是任何Activity的销毁行为都会触发Activity数据的保存**。只有销毁行为是被系统发起的并且今后有可能恢复的时候才会触发**。

1.1)不会触发重建机制

  • 按返回按键。比如,A Actvivity启动B Activity,BActivity中返回不会调用BActivity的OnSaveInstanceState()方法
  • 最近使用屏幕中滑动关闭 Activity。
  • 从设置中【停止】应用
  • 完成某种“完成”操作,开发者通过调用Activity #finish()方法完成

1.2)有可能会触发重建机制的

触发恢复重建机制就是两大类

  • 系统回收内存时kill掉。
  • 横竖屏切换和语言切换等配置发生变化时kill重建。(可以通过 Activity #isChangingConfigurations()方法判断是否为配置更改发生的)。

当由系统发起而非人为手动关闭Activity的时候,Activity有可能在未来的某个时机恢复重建。Android系统提供了两套机制,用以保存和恢复界面状态。 这两套机制我个人分别给其取名为 Save-Restore InstanceState机制RetainNonConfiguration机制

2)Save-Restore InstanceState机制

1635401794(1).jpg

  • Save-Restore InstanceState机制的初衷是保存界面的一些瞬时状态,比如ListView滑动的位置、ViewPager的position一些TextView的文本内容。保证用户进入新建Activity的时候能尽快的看到界面状态的恢复。
  • Save-Restore InstanceState是一些比较轻量级的数据,因为保存过程需要经历数据的序列化和反序列化。

对于开发者时机操作层面来说,Save-Restore InstanceState机制的核心就是Activity中 onSaveInstanceState() 、onCreate()和onRestoreInstanceState()这三个回调方法。

2.1) onSaveInstanceState()

#Activity
protected void onSaveInstanceState(@NonNull Bundle outState) {
//A、整个view树中的view相关信息有机会保存到整个bundle中
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
if (mAutoFillResetNeeded) {
outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
getAutofillManager().onSaveInstanceState(outState);
}
dispatchActivitySaveInstanceState(outState);
}
  • onSaveInstanceState(outState)的被调用的条件:非手动杀死Activity而是系统kill同时可能会在未来重建时。
  • 在Activity被系统kill时会调用onSaveInstanceState(outState)方法,允许开发者把一些今后重建时需要的一些状态数据存储到outState里面;这个方法的的触发时机是在onStop之前 (Android P开始会在onDestory之前执行)。
  • 默认地,代码A处,onSaveInstance方法会通过window依次调用整个view树的各个view的onSaveInstanceState()方法,view树的每个符合条件的view都有机会存储一些状态。需要注意的是:需要保存view状态的view需要有id作为标识存储在Bundle整个数据结构中。也就是说当view没有id的时候是保存不成功的。

2.2)onCreate(Bundle savedInstanceState)方法

被系统销毁又重建的Activity onCreate(Bundle savedInstanceState)回调方法中savedInstanceState的方法参数不为null。可以在这个位置取出被系统杀死之前保存的一些状态信息用来构建Activity。

2.3)onRestoreInstanceState(Bundle savedInstanceState)

  • 如果Activity是被系统重建的,会触发onRestoreInstanceState(savedInstanceState)方法,开发者可以在savedInstanceState中取出之前被系统销毁时存储的数据,用以在新Activity中恢复状态。

  • onRestoreInstanceState调用时机是在onStart()之后被调用

  • 默认地,onRestoreInstanceState方法会通过 mWindow.restoreHierarchyState()方法把之前保存的view状态信息分发出去,用以恢复view的状态。

protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//window view树有一次机会恢复销毁之前的状态
mWindow.restoreHierarchyState(windowState);
}
}
}

3) 配置改变发生的重建

image.png

当如横竖屏的切换、语言切换等配置发生改变时也会触发Activity的重建。这种由配置发生改变而导致的Activity重建除了会触发Save-Restore InstanceState机制之外也会触发RetainNonConfigurationInstance机制

RetainNonConfigurationInstance机制的核心是Activity中onRetainNonConfigurationInstance()和 getLastNonConfigurationInstance()这两个回调方法也会回调onRetainNonConfigurationInstance()方法,

3.1) onRetainNonConfigurationInstance()

  • 用于保存配置发生前的数据,这个数据理论上是没有结构和大小限制的甚至可以把旧Activity本身保存其中。

  • 触发时机会在onStop之前

public Object onRetainNonConfigurationInstance() {
return null;
}

3.2)getLastNonConfigurationInstance()

  • 当Activity 中getLastNonConfigurationInstance()方法返回值不为空的时候,说明当前这个Activity是因为配置发生变化重建而来的,可以使用这个返回值做一些Activity状态恢复的操作。

public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}

4)两种机制有什么不同

  • 数据上:
    • 通过Save-Restore InstanceState方式保存和恢复界面时只是一些简单的瞬时数据。究其原因这个机制是讲保存的数据传递到了系统进程,在恢复的时候又从系统进程传递到应用进程,数据经历序列化和反序列化,而这些操作又是在主线程。
    • RetainInstance方式数据可以传递一些大数据甚至可以传递Activity本身。究其原因这个机制保存数据还是在当前应用进程,不会经历数据的序列化和反序列化。
  • 触发条件上
    • Save-Restore InstanceState只要Activity有可能被系统kill就会调用onSaveInstance()方法,只要Activity被重建就会在onCreate()方法中传入instanceState数据和调用onRestoreInstaceState()方法。
    • RetaineInstance方式只在系统配置发生变化的时候才生效。

5)AndroidX 做了什么

坊间流传Jetpack中Viewmodel会比Activity的生命周期长,是怎么回事?

阅读这个章节之前如果你对ViewModel的创建比较了解读起来可能会省力些

5.1) 横竖屏切换的时候

在androidx.activity:activity包下的ComponentActivity中关键点,

code 5.5.1
static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}
  • 引入NonConfigurationInstances类,这个类主要有两个属性,分别用来保存自定义数据和viewModelStore.

    code 5.5.2
    public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
    NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) { viewModelStore = nc.viewModelStore; }
    }
    if (viewModelStore == null && custom == null) {
    return null;
    }
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
    }
  • 重写了onRetainNonConfigurationInstance()方法并把方法设置为了final。onRetainNonConfigurationInstance()方法内部创建NonConfigurationInstances对象nci,把viewModelStore存放到nci,同时收集onRetainCustomNonConfigurationInstance()方法的返回值存在nci里

  • 开发者可重写onRetainCustomNonConfigurationInstance()这个方法返回需要保存的数据。

    code 5.5.3
    public ComponentActivity() {
    ...
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    mContextAwareHelper.clearAvailableContext()
    if (!isChangingConfigurations()) { //不在更改配置状态
    getViewModelStore().clear(); //1
    }
    }
    }
    });
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    ensureViewModelStore();
    getLifecycle().removeObserver(this);
    }
    });
    void ensureViewModelStore() { //在NonConfiguration中取出viewModleStore
    if (mViewModelStore == null) {
    NonConfigurationInstances nc =
    (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) {
    mViewModelStore = nc.viewModelStore;
    }
    if (mViewModelStore == null) {
    mViewModelStore = new ViewModelStore();
    }
    }
    }
  • viewmodel中横竖屏转换,横竖屏切换等配置发生变化导致的重建时,新Activity中可听过ensureViewModelStore()方法获取从旧Activity传递过来viewmodelstore,这样就实现了横竖屏切换的时候viewmodel不丢失。

  • 另外值得注意的点是,Activity onDestory的时候,会通过**isChangingConfigurations()**方法判断activity是否处于配置变化状态,如果不是就会将viewmodelstores清空掉。

这套方式解决了一个问题当Activity由横竖屏转换等配置原因发生变化导致Activity重建的时候,会将旧Activity的viewModelStore传给新Activity。如果不是由于配置发生变化导致的Activity重建会清除掉viewModelStore

那有什么方式能解决非配置变化导致Activity重建时保存ViewModel的数据呢?

5.2)非配置变化引起的Activity重建对ViewModel的保存

结论先行,因配置变化引起的Activity重建可以将ViewModleStore保存,在新Activity中可以直接获取旧Activity中的ViewModel。而对于非配置变化引起的Activity重建不能直接将ViewModelStore对象传递给新Activity。AndroidX中是将ViewModel的数据保存到Bundle中,给Bundle分配一个Key,这样ViewModel的保存和恢复就可以通过Save-Restore Stated Instance机制实现。

稍微展开下实现细节

5.2.1) ViewModel销毁时数据保存
  • 数据保存还是通过Activity Save-Restore StateInstace机制,Activity发起saveStatedInstance()时通过调用注册到SavedStateRegistry上SavedStateProvider的saveState()方法获取到对应的Bundle当然最终存储到saveStatedInstance(outBundle)的outBundle中。
  • 通常情况下,ViewModel中会有LiveData,SavedStateHandle中也支持LiveData中数据的保存,SavingStateLiveData继承MutableLiveData复写setValue()方法,设置到LiveData的数据都会保存到mRegular中一份这样实现LiveData数据的保存。
public final class SavedStateHandle {
..
final Map<String, Object> mRegular;
final Map<String, SavedStateProvider> mSavedStateProviders = new HashMap<>();
private final Map<String, SavingStateLiveData<?>> mLiveDatas = new HashMap<>();

private final SavedStateProvider mSavedStateProvider = new SavedStateProvider() {
@SuppressWarnings("unchecked")
@NonNull
@Override
public Bundle saveState() {
...
Set<String> keySet = mRegular.keySet();
ArrayList keys = new ArrayList(keySet.size());
ArrayList value = new ArrayList(keys.size());
for (String key : keySet) {
keys.add(key);
value.add(mRegular.get(key));
}
Bundle res = new Bundle();
//把mRegular保存的数据存放到Bundle中返回
res.putParcelableArrayList("keys", keys);
res.putParcelableArrayList("values", value);
return res;
}
}
5.2.2) ViewModel重建时恢复
  • 带恢复功能的Viewmodel是通过SavedStateViewModelFactory创建,当Activity重建时,会在Activity的onCreate(Bundle data)带后旧Activity存的数据,这bundle中可以取出旧ViewModel的SavedStateHandle对象并以此为构造参数构建ViewModel。这样新建ViewModel就有了旧ViewModel的数据,数据是通过SavedStateHandle对象为介质进行传递的,ViewModel中可以使用对应的key恢复ViewModel的基本数据类型和可序列化的数据类型。

  • 所以,在具备保存-恢复数据特性的ViewModle中获取数据时使用SavedStateHandle对象上的 get(@NonNull String key)方法。获取LiveData()时使用 MutableLiveData getLiveData(String key)方法。内部方法实现是通过key在mRegular中获取到对应的值,再用值作为LiveData初始值创建LiveData。

5.2.3)其他
  • ViewModle中也会存在非序列化的数据(继承了Parcelable或Serializable)或者不能被Bundle存储的对象,如果要保存恢复这些数据怎么实现呢? Lifecycle 2.3.0-alpha03 开始允许设置自定义的SavedStateProvider这样我们可以把非序列化的数据转化成可序列化的数据保存到Bundle中,实现非序列化的数据的保存和恢复。

  • ViewModel的数据保存和恢复虽然逻辑相对比较简单,但是里面涉及到的类和细节比较繁杂这个章节只是说明了一下实现的核心思想,如果大家想了解内部更多的实现细节,今后可以另开一篇展开聊。

6) 最后

  • Android销毁重建机制常常会被开发者忽略,进而造成App线上出现非预期问题甚至crash。开发阶段我们可以通过 开发者模式 ->不保留活动选项,尽早的暴露相关的问题。
  • 关于此类问题的解决时至今日,我们已经有比较完备的工具箱,如果比较熟悉这些工具内部的实现原理机制,在使用这些工具的时候会更得心应手。

不足处批评指正,望不吝点赞


收起阅读 »

Room & Kotlin 符号的处理

Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。 注解处理器非常强大,但它们会增加构建时间。这对于用 Java...
继续阅读 »

Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。


注解处理器非常强大,但它们会增加构建时间。这对于用 Java 写的代码来说通常是可以接受的,但对于 Kotlin 而言,编译时间消耗会非常明显,这是因为 Kotlin 没有一个内置的注解处理管道。相反,它通过 Kotlin 代码生成了存根 Java 代码来支持注解处理器,然后将其输送到 Java 编译器中进行处理。


由于并不是所有 Kotlin 源代码中的内容都能用 Java 表示,因此有些信息会在这种转换中丢失。同样,Kotlin 是一种多平台语言,但 KAPT 只在面向 Java 字节码的情况下生效。


认识 Kotlin 符号处理


随着注解处理器在 Android 上的广泛使用,KAPT 成为了编译时的性能瓶颈。为了解决这个问题,Google Kotlin 编译器团队开始研究一个替代方案,来为 Kotlin 提供一流的注解处理支持。当这个项目诞生之初,我们非常激动,因为它将帮助 Room 更好地支持 Kotlin。从 Room 2.4 开始,它对 KSP 有了实验性的支持,我们发现编译速度提高了 2 倍,特别是在全量编译的情况下。


本文内容重点不在注解的处理、Room 或者 KSP。而在于重点介绍我们在为 Room 添加 KSP 支持时所面临的挑战和所做的权衡。为了理解本文您并不需要了解 Room 或者 KSP,但必须熟悉注解处理。



注意: 我们在 KSP 发布稳定版之前就开始使用它了。因此,尚不确定之前做的一些决策是否适用于现在。



本篇文章旨在让注解处理器的作者们在为项目添加 KSP 支持前,充分了解需要注意的问题。


Room 工作原理简介


Room 的注解处理分为两个步骤。有一些 "Processor" 类,它们遍历用户的代码,验证并提取必要的信息到 "值对象" 中。这些值对象被送到 "Writer" 类中,这些类将它们转换为代码。和其他诸多的注解处理器一样,Room 非常依赖 Auto-Commonjavax.lang.model 包 (Java 注解处理 API 包) 中频繁引用的类。


为了支持 KSP,我们有三种选择:



  1. 复制 JavaAP 和 KSP 的每个 "Processor" 类,它们会有相同的值对象作为输出,我们可以将其输入到 Writer 中;

  2. 在 KSP/Java AP 之上创建一个抽象层,以便处理器拥有一个基于该抽象层的实现;

  3. 用 KSP 代替 JavaAP,并要求开发者也使用 KSP 来处理 Java 代码。


选项 C 实际上是不可行的,因为它会对 Java 用户造成严重的干扰。随着 Room 使用数量的增加,这种破坏性的改变是不可能的。在 "A" 和 "B" 两者之间,我们决定选择 "B",因为处理器具有相当数量的业务逻辑,将其分解并非易事。


认识 X-Processing


在 JavaAP 和 KSP 上创建一个通用的抽象并非易事。Kotlin 和 Java 可以互操作,但模式却不相同,例如,Kotlin 中特殊类的类型如 Kotlin 的值类或者 Java 中的静态方法。此外,Java 类中有字段和方法,而 Kotlin 中有属性和函数。


我们决定实现 "Room 需要什么",而不是尝试去追求完美的抽象。从字面意思来看,在 Room 中找到导入了 javax.lang.model 的每一个文件,并将其移动到 X-Processing 的抽象中。这样一来,TypeElement 变成了 XTypeElementExecutableElemen 变成了 XExecutableElemen 等等。


遗憾的是,javax.lang.model API 在 Room 中的应用非常广泛。一次性创建所有这些 X 类,会给审阅者带来非常严重的心理负担。因此,我们需要找到一种方法来迭代这一实现。


另一方面,我们需要证明这是可行的。所以我们首先对其做了 原型 设计,一旦验证这是一个合理的选择,我们就用他们自己的测试 逐一重新实现了所有 X 类


关于我说的实现 "Room 需要什么",有一个很好的例子,我们可以在关于类的字段 更改 中看到。当 Room 处理一个类的字段时,它总是对其所有的字段感兴趣,包括父类中的字段。所以我们在创建相应的 X-Processing API 时,添加了获取所有字段的能力。


interface XTypeElement {
fun getAllFieldsIncludingPrivateSupers(): List<XVariableElement>
}

如果我们正在设计一个通用库,这样可能永远不会通过 API 审查。但因为我们的目标只是 Room,并且它已经有一个与 TypeElement 具有相同功能的辅助方法,所以复制它可以减少项目的风险。


一旦我们有了基本的 X-Processing API 和它们的测试方法,下一步就是让 Room 来调用这个抽象。这也是 "实现 Room 所需要的东西" 获得良好回报的地方。Room 在 javax.lang.model API 上已经拥有了用于基本功能的扩展函数/属性 (例如获取 TypeElement 的方法)。我们首先更新了这些扩展,使其看起来与 X-Processing API 类似,然后在 1 CL 中将 Room 迁移到 X-Processing。


改进 API 可用性


保留类似 JavaAP 的 API 并不意味着我们不能改进任何东西。在将 Room 迁移到 X-Processing 之后,我们又实现了一系列的 API 改进。


例如,Room 多次调用 MoreElement/MoreTypes,以便在不同的 javax.lang.model 类型 (例如 MoreElements.asType) 之间进行转换。相关调用通常如下所示:


val element: Element ...
if (MoreElements.isType(element)) {
val typeElement:TypeElement = MoreElements.asType(element)
}

我们把所有的调用放到了 Kotlin contracts 中,这样一来就可以写成:


val element: XElement ...
if (element.isTypeElement()) {
// 编译器识别到元素是一个 XTypeElement
}

另一个很好的例子是在一个 TypeElement 中找寻方法。通常在 JavaAP 中,您需要调用 ElementFilter 类来获取 TypeElement 中的方法。与此相反,我们直接将其设为 XTypeElement 中的一个属性。


// 前
val methods = ElementFilter.methodsIn(typeElement.enclosedElements)
// 后
val methods = typeElement.declaredMethods

最后一个例子,这也可能是我最喜欢的例子之一,就是可分配性。在 JavaAP 中,如果您要检查给定的 TypeMirror 是否可以由另一个 TypeMirror 赋值,则需要调用 Types.isAssignable


val type1: TypeMirror ...
val type2: TypeMirror ...
if (typeUtils.isAssignable(type1, type2)) {
...
}

这段代码真的很难读懂,因为您甚至无法猜到它是否验证了类型 1 可以由类型 2 指定,亦或是完全相反的结果。我们已经有一个扩展函数如下:


fun TypeMirror.isAssignableFrom(
types: Types,
otherType: TypeMirror
): Boolean

在 X-Processing 中,我们能够将其转换为 XType 上的常规函数,如下方所示:


interface XType {
fun isAssignableFrom(other: XType): Boolean
}

为 X-Processing 实现 KSP 后端


这些 X-Processing 接口每个都有自己的测试套件。我们编写它们并非是用来测试 AutoCommon 或者 JavaAP 的,相反,编写它们是为了在有了它们的 KSP 实现时,我们就可以运行测试用例来验证它是否符合 Room 的预期。


由于最初的 X-Processing API 是按照 avax.lang.model 建模,它们并非每次都适用于 KSP,所以我们也改进了这些 API,以便在需要时为 Kotlin 提供更好的支持。


这样产生了一个新问题。现有的 Room 代码库是为了处理 Java 源代码而写的。当应用是由 Kotlin 编写时,Room 只能识别该 Kotlin 在 Java 存根中的样子。我们决定在 X-Processing 的 KSP 实现中保持类似行为。


例如,Kotlin 中的 suspend 函数在编译时生成如下签名:


// kotlin
suspend fun foo(bar:Bar):Baz
// java
Object foo(bar:Bar, Continuation<? extends Baz>)

为保持相同的行为,KSP 中的 XMethodElement 实现为 suspend 方法合成了一个新参数,以及新的返回类型。(KspMethodElement.kt)



注意: 这样做效果很好,因为 Room 生成的是 Java 代码,即使在 KSP 中也是如此。当我们添加对 Kotlin 代码生成的支持时,可能会引起一些变化。



另一个例子与属性有关。Kotlin 属性也可能具有基于其签名的合成 getter/setter (访问器)。由于 Room 期望找到这些访问器作为方法 (参见: KspTypeElement.kt),因此 XTypeElement 实现了这些合成方法。



注意 : 我们已有计划更改 XTypeElement API 以提供属性而非字段,因为这才是 Room 真正想要获取的内容。正如您现在猜到的那样,我们决定 "暂时" 不这样做来减少 Room 的修改。希望有一天我们能够做到这一点,当我们这样做时,XTypeElement 的 JavaAP 实现将会把方法和字段作为属性捆绑在一起。



在为 X-Processing 添加 KSP 实现时,最后一个有趣的问题是 API 耦合。这些处理器的 API 经常相互访问,因此如果不实现 XField / XMethod,就不能在 KSP 中实现 XTypeElement,而 XField / XMethod 本身又引用了 XType 等等。在添加这些 KSP 实现的同时,我们为它们的实现部分写了单独的测试用例。当 KSP 的实现变得更加完整时,我们逐渐通过 KSP 后端启动全部的 X-Processing 测试。


需要注意的是,在此阶段我们只在 X-Processing 项目中运行测试,所以即使我们知道测试的内容没问题,我们也无法保证所有的 Room 测试都能通过 (也称之为单元测试 vs 集成测试)。我们需要通过一种方法来使用 KSP 后端运行所有的 Room 测试,"X-Processing-Testing" 就应运而生。


认识 X-Processing-Testing


注解处理器的编写包含 20% 的处理器代码和 80% 的测试代码。您需要考虑到各种可能的开发者错误,并确保如实报告错误消息。为了编写这些测试,Room 已经提供一个辅助方法如下:


 

runTest 在底层使用了 Google Compile Testing 库,并允许我们简单地对处理器进行单元测试。它合成了一个 Java 注解处理器并在其中调用了处理器提供的 process 方法。


val entitySource : JavaFileObject //示例 @Entity 注释类
val result = runTest(entitySource) { invocation ->
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
}
// 断言结果是否有误,警告等

糟糕的是,Google Compile Testing 仅支持 Java 源代码。为了测试 Kotlin 我们需要另一个库,幸运的是有 Kotlin Compile Testing,它允许我们编写针对 Kotlin 的测试,而且我们为该库贡献了对 KSP 支持。



注意 : 我们后来用 内部实现 替换了 Kotlin Compile Testing,以简化 AndroidX Repo 中的 Kotlin/KSP 更新。我们还添加了更好的断言 API,这需要我们对 KCT 执行 API 不兼容的修改操作。



作为能让 KSP 运行所有测试的最后一步,我们创建了以下测试 API:


fun runProcessorTest(
sources: List<Source>,
handler: (XTestInvocation) -> Unit
): Unit

这个和原始版本之间的主要区别在于,它同时通过 KSP 和 JavaAP (或 KAPT,取决于来源) 运行测试。因为它多次运行测试且 KSP 和 JavaAP 两者的判断结果不同,因此无法返回单个结果。


因此,我们想到了一个办法:


fun XTestInvocation.assertCompilationResult(
assertion: (XCompilationResultSubject) -> Unit
}

每次编译后,它都会调用结果断言 (如果没有失败提示,则检查编译是否成功)。我们把每个 Room 测试重构为如下所示:


val entitySource : Source //示例 @Entity 注释类
runProcessorTest(listOf(entitySource)) { invocation ->
// 该代码块运行两次,一次使用 JavaAP/KAPT,一次使用 KSP
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
invocation.assertCompilationResult {
// 结果被断言为是否有 error,warning 等
hasWarningContaining("...")
}
}

接下来的事情就很简单了。将每个 Room 的编译测试迁移到新的 API,一旦发现新的 KSP / X-Processing 错误,就会上报,然后实施临时解决方案;这一动作反复进行。由于 KSP 正在大力开发中,我们确实遇到了很多 bug。每一次我们都会上报 bug,从 Room 源链接到它,然后继续前进 (或者进行修复)。每当 KSP 发布之后,我们都会搜索代码库来找到已修复的问题,删除临时解决方案并启动测试。


一旦编译测试覆盖情况较好,我们在下一步就会使用 KSP 运行 Room 的 集成测试。这些是实际的 Android 测试应用,也会在运行时测试其行为。幸运的是,Android 支持 Gradle 变体,因此使用 KSP 和 KAPT 来运行我们 Kotlin 集成测试 便相当容易。


下一步


将 KSP 支持添加到 Room 只是第一步。现在,我们需要更新 Room 来使用它。例如,Room 中的所有类型检查都忽略了 nullability,因为 javax.lang.modelTypeMirror 并不理解 nullability。因此,当调用您的 Kotlin 代码时,Room 有时会在运行时触发 NullPointerException。有了 KSP,这些检查现在可在 Room 中创建新的 KSP bug (例如 b/193437407)。我们已经添加了一些临时解决方案,但理想情况下,我们仍希望 改进 Room 以正确处理这些情况。


同样,即使我们支持 KSP,Room 仍然只生成 Java 代码。这种限制使我们无法添加对某些 Kotlin 特性的支持,比如 Value Classes。希望在将来,我们还能对生成 Kotlin 代码提供一些支持,以便在 Room 中为 Kotlin 提供一流的支持。接下来,也许更多 :)。


我能在我的项目上使用 X-Processing 吗?


答案是还不能;至少与您使用任何其他 Jetpack 库的方式不同。如前文所述,我们只实现了 Room 需要的部分。编写一个真正的 Jetpack 库有很大的投入,比如文档、API 稳定性、Codelabs 等,我们无法承担这些工作。话虽如此,Dagger 和 Airbnb (ParisDeeplinkDispatch) 都开始用 X-Processing 来支持 KSP (并贡献了他们需要的东西🙏)。也许有一天我们会把它从 Room 中分解出来。从技术层面上讲,您仍然可以像使用 Google Maven 库 一样使用它,但是没有 API 保证可以这样做,因此您绝对应该使用 shade 技术。


总结


我们为 Room 添加了 KSP 支持,这并非易事但绝对值得。如果您在维护注解处理器,请添加对 KSP 的支持,以提供更好的 Kotlin 开发者体验。


特别感谢 Zac SweersEli Hart 审校这篇文章的早期版本,他们同时也是优秀的 KSP 贡献者。


更多资源



欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!


作者:Android_开发者
链接:https://juejin.cn/post/7026558615997661214
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

坏了!面试官问我垃圾回收机制

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛 面试官:当时感觉再讲下去那我可能就得加班了 面试官:今天有点空了,继续聊聊「堆」那块吧 候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Ed...
继续阅读 »

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛


面试官:当时感觉再讲下去那我可能就得加班了


面试官今天有点空了,继续聊聊「堆」那块吧


候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「survivor」区又分为「From Survivor」和「To Survivor」区



候选者:说到这里,我就想聊聊Java的垃圾回收机制了


面试官:那你开始你的表演吧


候选者:我们使用Java的时候,会创建很多对象,但我们未曾「手动」将这些对象进行清除


候选者:而如果用C/C++语言的时候,用完是需要自己free(释放)掉的


候选者:那为什么在写Java的时候不用我们自己手动释放”垃圾”呢?原因很简单,JVM帮我们做了(自动回收垃圾)


面试官:嗯…


候选者:我个人对垃圾的定义:只要对象不再被使用了,那我们就认为该对象就是垃圾,对象所占用的空间就可以被回收



面试官那是怎么判断对象不再被使用的呢?


候选者:常用的算法有两个「引用计数法」和「可达性分析法」


候选者:引用计数法思路很简单:当对象被引用则+1,但对象引用失败则-1。当计数器为0时,说明对象不再被引用,可以被可回收


候选者:引用计数法最明显的缺点就是:如果对象存在循环依赖,那就无法定位该对象是否应该被回收(A依赖B,B依赖A)


面试官:嗯…


候选者:另一种就是可达性分析法:它从「GC Roots」开始向下搜索,当对象到「GC Roots」都没有任何引用相连时,说明对象是不可用的,可以被回收



候选者:「GC Roots」是一组必须「活跃」的引用。从「GC Root」出发,程序通过直接引用或者间接引用,能够找到可能正在被使用的对象


面试官还是不太懂,那「GC Roots」一般是什么?你说它是一组活跃的引用,能不能举个例子,太抽象了。


候选者:比如我们上次不是聊到JVM内存结构中的虚拟机栈吗,虚拟机栈里不是有栈帧吗,栈帧不是有局部变量吗?局部变量不就存储着引用嘛。


候选者:那如果栈帧位于虚拟机栈的栈顶,是不是就可以说明这个栈帧是活跃的(换言之,是线程正在被调用的)


候选者:既然是线程正在调用的,那栈帧里的指向「堆」的对象引用,是不是一定是「活跃」的引用?


候选者:所以,当前活跃的栈帧指向堆里的对象引用就可以是「GC Roots」


面试官:嗯…


候选者:当然了,能作为「GC Roots」也不单单只有上面那一小块


候选者:比如类的静态变量引用是「GC Roots」,被「Java本地方法」所引用的对象也是「GC Roots」等等…



候选者:回到理解的重点:「GC Roots」是一组必须「活跃」的「引用」,只要跟「GC Roots」没有直接或者间接引用相连,那就是垃圾


候选者:JVM用的就是「可达性分析算法」来判断对象是否垃圾


面试官:懂了


候选者:垃圾回收的第一步就是「标记」,标记哪些没有被「GC Roots」引用的对象



候选者:标记完之后,我们就可以选择直接「清除」,只要不被「GC Roots」关联的,都可以干掉


候选者:过程非常简单粗暴,但也存在很明显的问题


候选者:直接清除会有「内存碎片」的问题:可能我有10M的空余内存,但程序申请9M内存空间却申请不下来(10M的内存空间是垃圾清除后的,不连续的)



候选者:那解决「内存碎片」的问题也比较简单粗暴,「标记」完,不直接「清除」。


候选者:我把「标记」存活的对象「复制」到另一块空间,复制完了之后,直接把原有的整块空间给干掉!这样就没有内存碎片的问题了


候选者:这种做法缺点又很明显:内存利用率低,得有一块新的区域给我复制(移动)过去


面试官:嗯…


候选者:还有一种「折中」的办法,我未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动


候选者:把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛


候选者:这种专业的术语就叫做「整理」



候选者:扯了这么久,我们把思维再次回到「堆」中吧


候选者:经过研究表明:大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间


候选者:又由于「垃圾回收」是会导致「stop the world」(应用停止访问)


候选者:理解「stop the world」应该很简单吧:回收垃圾的时候,程序是有短暂的时间不能正常继续运作啊。不然JVM在回收的时候,用户线程还继续分配修改引用,JVM怎么搞(:


候选者:为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率


候选者:在很多的垃圾收集器上都会在「物理」或者「逻辑」上,把这两类对象进行区分,死得快的对象所占的区域叫做「年轻代」,活得久的对象所占的区域叫做「老年代」



候选者:但也不是所有的「垃圾收集器」都会有,只不过我们现在线上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。


候选者:所以,你可以看到我的「堆」是画了「年轻代」和「老年代」


候选者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是没有分代的概念的(:


候选者:只不过我为了好说明现状,ZGC的话有空我们再聊


面试官:嗯…好吧


候选者:在前面更前面提到了垃圾回收的过程,其实就对应着几种「垃圾回收算法」,分别是:


候选者:标记清除算法、标记复制算法和标记整理算法【「标记」「清除」「复制」「整理」】


候选者:经过上面的铺垫之后,这几种算法应该还是比较好理解的



候选者:「分代」和「垃圾回收算法」都搞明白了之后,我们就可以看下在JDK8生产环境及以下常见的垃圾回收器了


候选者:「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew


候选者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS


候选者:看着垃圾收集器有很多,其实还是非常好理解的。Serial是单线程的,Parallel是多线程


候选者:这些垃圾收集器实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)


候选者:CMS是「JDK8之前」是比较新的垃圾收集器,它的特点是能够尽可能减少「stop the world」时间。在垃圾回收时让用户线程和 GC 线程能够并发执行!



候选者:又可以发现的是,「年轻代」的垃圾收集器使用的都是「标记复制算法」


候选者:所以在「堆内存」划分中,将年轻代划分出Survivor区(Survivor From 和Survivor To),目的就是为了有一块完整的内存空间供垃圾回收器进行拷贝(移动)


候选者:而新的对象则放入Eden区


候选者:我下面重新画下「堆内存」的图,因为它们的大小是有默认的比例的



候选者:图我已经画好了,应该就不用我再说明了


面试官我还想问问,就是,新创建的对象一般是在「新生代」嘛,那在什么时候会到「老年代」中呢?


候选者:嗯,我认为简单可以分为两种情况:


候选者:1. 如果对象太大了,就会直接进入老年代(对象创建时就很大 || Survivor区没办法存下该对象)


候选者:2. 如果对象太老了,那就会晋升至老年代(每发生一次Minor GC ,存活的对象年龄+1,达到默认值15则晋升老年代 || 动态对象年龄判定 可以进入老年代)



面试官既然你又提到了Minor GC,那Minor GC 什么时候会触发呢?


候选者:当Eden区空间不足时,就会触发Minor GC


面试官:Minor GC 在我的理解就是「年轻代」的GC,你前面又提到了「GC Roots」嘛


面试官那在「年轻代」GC的时候,从GC Roots出发,那不也会扫描到「老年代」的对象吗?那那那..不就相当于全堆扫描吗?


候选者:这JVM里也有解决办法的。


候选者:HotSpot 虚拟机「老的GC」(G1以下)是要求整个GC堆在连续的地址空间上。


候选者:所以会有一条分界线(一侧是老年代,另一侧是年轻代),所以可以通过「地址」就可以判断对象在哪个分代上


候选者:当做Minor GC的时候,从GC Roots出发,如果发现「老年代」的对象,那就不往下走了(Minor GC对老年代的区域毫无兴趣)



面试官但又有个问题,那如果「年轻代」的对象被「老年代」引用了呢?(老年代对象持有年轻代对象的引用),那时候肯定是不能回收掉「年轻代」的对象的


候选者:HotSpot虚拟机下 有「card table」(卡表)来避免全局扫描「老年代」对象


候选者:「堆内存」的每一小块区域形成「卡页」,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为「脏页」


候选者:那知道了「卡表」之后,就很好办了。每次Minor GC 的时候只需要去「卡表」找到「脏页」,找到后加入至GC Root,而不用去遍历整个「老年代」的对象了。



面试官:嗯嗯嗯,还可以的啊,要不继续聊聊CMS?


候选者:这面试快一个小时了吧,我图也画了这么多了。下次?下次吧?有点儿累了


本文总结



  • 什么是垃圾:只要对象不再被使用,那即是垃圾

  • 如何判断为垃圾:可达性分析算法和引用计算算法,JVM使用的是可达性分析算法

  • 什么是GC Roots:GC Roots是一组必须活跃的引用,跟GC Roots无关联的引用即是垃圾,可被回收

  • 常见的垃圾回收算法:标记清除、标记复制、标记整理

  • 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率。

  • Minor GC:当Eden区满了则触发,从GC Roots往下遍历,年轻代GC不关心老年代对象

  • 什么是card table【卡表】:空间换时间(类似bitmap),能够避免扫描老年代的所有对应进而顺利进行Minor GC (案例:老年代对象持有年轻代对象引用)

  • 堆内存占比:年轻代占堆内存1/3,老年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代2/10(其中From 和To 各站1/10)

作者:Java3y
链接:https://juejin.cn/post/7026504718771814431
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

flutter 数字键盘、自定义键盘

有些特殊的场景 会遇到使用数字键盘的特殊场景,例如输入金额、数量 number_keypan.dart =》文件内容如下: import 'package:flutter/material.dart'; /// <summary> /// ...
继续阅读 »

有些特殊的场景 会遇到使用数字键盘的特殊场景,例如输入金额、数量


image.png


number_keypan.dart =》文件内容如下:


import 'package:flutter/material.dart';

/// <summary>
/// todo: 数字键盘
/// author:zwb
/// dateTime:2021/7/19 10:25
/// filePath:lib/widgets/number_keypan.dart
/// desc: 示例
/// <summary>
// OverlayEntry overlayEntry;
// TextEditingController controller = TextEditingController();
//
// numberKeypan(
// initialization: (v){
// /// 初始化
// overlayEntry = v;
// /// 唤起键盘
// openKeypan(context: context);
// },
// onDel: (){
// delCursor(textEditingController: controller);
// },
// onTap: (v){
// /// 更新输入框的值
// controller.text += v;
// /// 保持光标
// lastCursor(textEditingController: controller);
// },
// );
OverlayEntry overlayEntry;
NumberKeypan({@required Function(OverlayEntry) initialization,@required Function(String) onTap,Function onCommit,Function onDel,}){
overlayEntry = OverlayEntry(builder: (context) {
List<String> list = ['1','2','3','4','5','6','7','8','9','','0','删除'];
return new Positioned(
bottom: 0,
child: new Material(
child: new Container(
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
color: Colors.grey[200],
child: Row(
children: [
Expanded(
child: Wrap(
alignment: WrapAlignment.spaceBetween,
children: List.generate(list.length, (index) {
return Material(
color: Colors.white,
child: Ink(
child: InkWell(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200],width: 0.25),
),
alignment: Alignment.center,
height: 50,
width: (MediaQuery.of(context).size.width) / 3,
child: Text("${list[index]}",style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold),),
),
onTap: index == 11 ? onDel : (){
if(list[index] != "" && list[index] !="删除"){
onTap(list[index]);
}
},
),
color: Colors.white,
),
);
}),
),
),
// Column(
// children: [
// SizedBox(
// width: 60,
// height: 50 * 1.5,
// child: MaterialButton(
// onPressed: onDel ?? (){},
// child: Text("删除",style: TextStyle(color: Colors.black,fontWeight: FontWeight.bold)),
// color: Colors.grey[100],
// elevation: 0,
// padding: EdgeInsets.all(0),),
// ),
// SizedBox(
// width: 60,
// height: 50 * 2.5,
// child: MaterialButton(
// onPressed: (){
// disKeypan();
// if(onCommit != null ) onCommit();
// },
// child: Text("确认",style: TextStyle(color: Colors.white,fontWeight: FontWeight.bold),),
// color: Colors.blue,
// elevation: 0,
// padding: EdgeInsets.all(0),
// ),
// ),
// ],
// ),
],
),
),
));
});
initialization(overlayEntry);
}

/// <summary>
/// todo: 保持光标在最后
/// author: zwb
/// date: 2021/7/19 11:43
/// param: 参数
/// return: void
/// <summary>
///
lastCursor({@required TextEditingController textEditingController}){
/// 保持光标在最后
final length = textEditingController.text.length;
textEditingController.selection = TextSelection(baseOffset:length , extentOffset:length);
}

/// <summary>
/// todo: 自定义键盘的删除事件
/// author: zwb
/// date: 2021/7/19 11:45
/// param: 参数
/// return: void
/// <summary>
///
delCursor({@required TextEditingController textEditingController}){
if(textEditingController != null && textEditingController.value.text != "") textEditingController.text = textEditingController.text.substring(0,textEditingController.text.length - 1);
}

/// <summary>
/// todo: 打开键盘
/// author: zwb
/// date: 2021/7/19 12:04
/// param: 参数
/// return: void
/// <summary>
///
openKeypan({BuildContext context}){
Overlay.of(context).insert(overlayEntry);
}

/// <summary>
/// todo: 销毁键盘
/// author: zwb
/// date: 2021/7/19 12:03
/// param: 参数
/// return: void
/// <summary>
///
disKeypan(){
if(overlayEntry!=null) overlayEntry.remove();
}

作者:win工藤新一
链接:https://juejin.cn/post/7026291564758450183
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【GC算法几人知?】一、前置知识积累

GC
本篇是接下来算法的前置知识,毕竟搞懂算法逻辑的基础,是搞懂概念 结构 对象 组成: 头:保存对象的一些基本信息,比如大小,种类等,他的地址也代表对象的地址,类似于数组的首地址 域:对象中可以访问的部分,里面可以有各种数据,也可以有指向其他对象的指针(指向其...
继续阅读 »

本篇是接下来算法的前置知识,毕竟搞懂算法逻辑的基础,是搞懂概念


结构


对象


在这里插入图片描述


组成:



  • 头:保存对象的一些基本信息,比如大小,种类等,他的地址也代表对象的地址,类似于数组的首地址

  • 域:对象中可以访问的部分,里面可以有各种数据,也可以有指向其他对象的指针(指向其他对象的头)


分类



  • 活动对象:能被mutator引用的对象(后面会讲),可以理解为能被引用的对象

  • 非活动对象:不能被mutator引用的对象,这种对象就是将被GC的对象,称为垃圾


mutator


这是一种动作,作用是改变GC中对象的引用关系,可以类比为new操作,new就是新建一个对象,mutator可以申请内存,为new对象做准备,也可以修改对象的域中指针的方向


其他结构



  • 堆:执行程序时存放对象的空间

  • 根:指向对象的指针的起点

  • 分块:当mutator时,从堆中分出去的一块内存

  • 分配:从堆中选出一个分块给mutator的方法


算法评价


如何判定一个GC算法是好的呢?有以下几个方面



  • 吞吐量throughput:单位时间内的处理能力
    计算方法是:heap_size/GC的时间
    比如


在这里插入图片描述


上图中的throughput=堆的大小/(A+B+C),A,B,C为三次GC



  • 最大暂停时间:因GC而暂停mutator的最大时间


从上图看出,当GC触发时,mutator将会暂停,所以也可以理解为单次GC所需要的最大时间,图中B最长,所以最大暂停时间是B



  • 堆使用效率


有两方面,
一是对象的头,对象中,头越大,信息越多,越方便找到他,但是效率会降低,因为头大了,对象大小不变的话,所能生成的对象数量就会减少


二是利用率,如果算法越好,对堆的利用率越高当然好,但是相应的GC会越困难,类比hash算法虽然可以通过映射使得数组空间得以最大利用,但是因此数组排列很不规律。在堆中也是一样,类似的对象或许分布堆中各地,很难去全部找出



  • 访问局部性
    某些对象由于有较强相关性,会一起生成,一起毁灭,比如有boyfriend就会有girlfriend,这类对象最好放在相近的地方,好生成,好清除


所以,我们的GC算法追求的是较大的吞吐量,较小的最大暂停时间,合适的利用率,以及最大限度的局部性


现在你已经掌握的学习GC的所有前置知识啦,一起来学习GC算法吧


从本文开始,将持续更新GC算法,GC算法是面试java必问的知识,同时,在c,c++这种需要手动GC的语言,更是需要掌握的算法,一起加油吧!


作者:小松漫步
链接:https://juejin.cn/post/7026286102583459847
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Rxjava - 自己动手实现Rxjava

先看看大致实现的样式:Observable.create(new ObservableOnSubscribe() { @Override public void subscribe(ObservableEmitter emitter) thro...
继续阅读 »

先看看大致实现的样式:

Observable.create(new ObservableOnSubscribe() {
@Override
public void subscribe(ObservableEmitter emitter) throws Exception {
emitter.onNext(1);
emitter.onComplete();
}
}).map(new Function(){

@Override
public String apply(Integer integer) {
return integer + "arrom";
}
}).subscribe(new Observer(){

@Override
public void onSubscribe(Disposable d) {
Log.d("arrom", "onSubscribe 成功");
}

@Override
public void onNext(String s) {
Log.d("arrom", "onSubscribe===" + s);
}

@Override
public void onError(Throwable throwable) {
Log.d("arrom", "onError");
}

@Override
public void onComplete() {
Log.d("arrom", "onComplete");
}
});

被观察者

/**
* 被观察者
*/
public abstract class Observable implements ObserverbleSource{


/**
* 创建操作符号
* @param source
* @param
* @return
*/
public static Observable create(ObservableOnSubscribe source){

return new ObservableCreate(source);
}
@Override
public void subscribe(Observer observer) {

subscribeActual(observer);

}

protected abstract void subscribeActual(Observer observer);


public Observable map(Function function){
return new ObservableMap(this,function);
}

}

观察者

public interface Observer {

void onSubscribe(Disposable d);

void onNext(T t);

void onError(Throwable throwable);

void onComplete();

}

订阅

public interface ObserverbleSource {

//订阅
void subscribe(Observer observer);

}

发射器

public interface ObservableOnSubscribe {

/**
* 为每一个订阅的观察者调用
* @param observableEmitter
* @throws Exception
*/
void subscribe(ObservableEmitter observableEmitter) throws Exception;
}
public interface ObservableEmitter extends Emitter{
}
/**
* 发射器
*/
public interface Emitter {

//发出正常值信号
void onNext(T value);

//发出一个throwable异常信号
void onError(Throwable throwable);

//发出完成的信号
void onComplete();
}

订阅方法的实现

public class ObservableCreate extends Observable {

final ObservableOnSubscribe source;

public ObservableCreate(ObservableOnSubscribe source) {
this.source = source;
}


@Override
protected void subscribeActual(Observer observer) {
CreateEmitter parent = new CreateEmitter(observer);
observer.onSubscribe(parent);//通知观察者订阅成功
try {
source.subscribe(parent);
} catch (Exception e) {
e.printStackTrace();
parent.onError(e);
}
}

static final class CreateEmitter implements ObservableEmitter ,Disposable{

final Observer observer;

private boolean flag;

public CreateEmitter(Observer observer) {
this.observer = observer;
}

@Override
public void disposa(boolean flag) {
this.flag = flag;
}

@Override
public boolean isDisposad() {
return flag;
}

@Override
public void onNext(T value) {
if (!flag){
observer.onNext(value);
}
}

@Override
public void onError(Throwable throwable) {
if (!flag){
observer.onError(throwable);
}
}

@Override
public void onComplete() {
if (!flag){
observer.onComplete();
}
}
}

}

Disposable

public interface Disposable {

void disposa(boolean flag);


boolean isDisposad();

}

create操作符大致就这个几个类。转换操作和这个有点类似只是有一些不一眼的地方

被观察者

/**
* 被观察者
* @param
* @param
*/
public abstract class AbstractObservableWithUpstream extends Observable {

protected final ObserverbleSource source;

public AbstractObservableWithUpstream(ObserverbleSource source) {
this.source = source;
}

}

观察者

/**
* 观察者
* @param
* @param
*/
public abstract class BaseFuseableObserver implements Observer, Disposable {

//观察者
protected final Observer actual;

protected Disposable disposable;

public BaseFuseableObserver(Observer actual) {
this.actual = actual;
}

@Override
public void disposa(boolean flag) {
disposable.disposa(flag);
}

@Override
public boolean isDisposad() {
return disposable.isDisposad();
}

@Override
public void onSubscribe(Disposable d) {
this.disposable = d;
actual.onSubscribe(d);
}


@Override
public void onError(Throwable throwable) {
actual.onError(throwable);
}

@Override
public void onComplete() {
actual.onComplete();
}
}
public class ObservableMap extends AbstractObservableWithUpstream {

Function function;


public ObservableMap(ObserverbleSource source,Function function){
super(source);
this.function = function;
}

@Override
protected void subscribeActual(Observer observer) {
source.subscribe(new MapObserver<>(observer,function));
}


static final class MapObserver extends BaseFuseableObserver{

final Function mapper;

public MapObserver(Observer actual,Function mapper) {
super(actual);
this.mapper = mapper;
}


@Override
public void onNext(T t) {
U u = mapper.apply(t);
actual.onNext(u);
}

}


}

转换函数

public interface Function {
/**
* 转换
* @param t
* @return
*/
R apply(T t);

}

自己撸完一遍之后感觉其实没有那么绕。


收起阅读 »

RxJava的并发实现

我们在开发App过程中,常常遇见这种需求,例如首页,仅一个界面就要请求3个甚至更多的接口,更变态的是这些接口必须按顺序请求,来以此展示返回结果,那么这样我们就无法用普通的并发去同时请求接口了,因为我们无法预知各个接口的请求完成时间,普通的也是最简单的办法就是依...
继续阅读 »

我们在开发App过程中,常常遇见这种需求,例如首页,仅一个界面就要请求3个甚至更多的接口,更变态的是这些接口必须按顺序请求,来以此展示返回结果,那么这样我们就无法用普通的并发去同时请求接口了,因为我们无法预知各个接口的请求完成时间,普通的也是最简单的办法就是依次请求接口了,A接口请求完成->B接口请求完成->C接口...简单粗暴有木有?并且在加载效率上(接口请求时间)会差很多,那么有没有更优雅的办法去解决这种需求呢?那必须有,利用RxJava的Observable.zip方法即可实现并发请求!

假如ApiService中有两个接口:

    @GET("test1")
Observable<HttpResult<TestModel1>> test1(@QueryMap HashMap<String, String> options);

@GET("test2")
Observable<HttpResult<TestModel2>> test2(@QueryMap HashMap<String, String> options);

HttpResult为自定义数据结构:

public class HttpResult<T> {

public int status;

public String msg;

public T data;

}

TestModel1和TestModel2则分别为两个返回的数据结构!

接口封装后的请求方法: test1:

    Observable o1 = Observable.create((ObservableOnSubscribe<TestModel1>) emitter ->
//接口请求
ApiUtil.getInstance()
.getApiService()
.test1()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<HttpResult<TestModel1>>() {

@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(HttpResult<TestModel1> httpResult) {
emitter.onNext(httpResult.data);
emitter.onComplete();
}

@Override
public void onError(Throwable e) {
emitter.onNext(null);
emitter.onComplete();
}

@Override
public void onComplete() {

}
}));

注意: ObservableOnSubscribe的参数是o1 中emitter要传递的参数类型,也就是你接口得到的数据类型:TestModel1!

test2:

 Observable o2 = Observable.create((ObservableOnSubscribe<TestModel2>) emitter ->
//接口请求
ApiUtil.getInstance()
.getApiService()
.test2()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<HttpResult<TestModel2>>() {

@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(HttpResult<TestModel2> httpResult) {
emitter.onNext(httpResult.data);
emitter.onComplete();
}

@Override
public void onError(Throwable e) {
emitter.onNext(null);
emitter.onComplete();
}

@Override
public void onComplete() {

}
}));

两个接口请求,得到两个Observable:o1和o2!

合并:

   Observable.zip(o1, o2, new BiFunction<Object, Object, Object>() {
@Override
public Object apply(Object o, Object o2) throws Exception {
TestModel1 t1 = (TestModel1) o;//o1得到的结果
TestModel2 t2 = (TestModel2) o2;//o2得到的结果
FinalData f=new FinalData();//最终结果合并
f.t1=t1;
f.t2=t2;
return f;
}
}).subscribeOn(Schedulers.io()).subscribe(o -> {
FinalData f=(FinalData)o;//获取最终结果
//处理数据...
});

注意: BiFunction中的3个Obj参数,前两个对应接口返回数据类型,最后一个对应apply方法返回的数据类型(最终结果)!

如果是3个或以上接口,那么合并时可以根据接口数量使用Function3,Function4...

   Observable.zip(o1, o2,o3, new Function3<Object, Object, Object,Object>() {
@Override
public Object apply(Object o, Object o2,Object o3) throws Exception {

}
}).subscribeOn(Schedulers.io()).subscribe(o -> {

});

除了zip操作符,rxjava还提供了concat,merge,join等其它合并操作符,但它们又各有不同,有兴趣的可以去多了解一下!

收起阅读 »

Android线程思考

在编程中我们经常遇到多线程相关的问题,记得刚工作的时候对线程没有太多概念,只知道new Thread()run函数中是新的线程,函数多调用几层,特别是一些别人的回调函数中,就忽略了线程引起的并发问题,产生了并发修改异常的崩溃。今天总结一些线程相关的知识。线程基...
继续阅读 »


在编程中我们经常遇到多线程相关的问题,记得刚工作的时候对线程没有太多概念,只知道new Thread()run函数中是新的线程,函数多调用几层,特别是一些别人的回调函数中,就忽略了线程引起的并发问题,产生了并发修改异常的崩溃。今天总结一些线程相关的知识。

线程基础

线程创建

Java创建线程的两种方式:

  1. new Thread(){}.start();
  2. new Thread(new Runnable(){}).start();

线程生命周期

5ca3282b6c02e745.jpg

新建-就绪-运行-阻塞-死亡。

线程同步

Syncronized关键字

  1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

线程同步手段

  • AsyncTask

  • runOnUiThread

  • Handler

  • View.post(Runnable r)

线程池

什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。 如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

为什么要使用线程池?

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。(我们可以把创建和销毁的线程的过程去掉)

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。

如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。 一个线程池包括以下四个基本组成部分:

  1. 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
  2. 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  4. 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池有什么作用?

线程池作用就是限制系统中执行线程的数量

  1. 提高效率 创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。
  2. 方便管理 可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建100个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

线程池原理

Java通过Executors提供四种线程池

  • CachedThreadPool():可缓存线程池。如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。比较适合处理执行时间比较小的任务
  • FixedThreadPool():定长线程池。可控制线程最大并发数,超出的线程会在队列中等待。可以用于已知并发压力的情况下,对线程数做限制。
  • ScheduledThreadPool():定时线程池。支持定时及周期性任务执行。适用于需要多个后台线程执行周期任务的场景
  • SingleThreadExecutor():单线程化的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。可以用于需要保证顺序执行的场景,并且只有一个线程在执行

使用ThreadPoolExecutor自定义的线程池

阿里巴巴Java开发手册,明确指出不允许使用上述Executors静态工厂构建线程池 原因如下:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,同时Executors返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool:允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
  2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

ThreadPoolExecutor创建

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

  private static ExecutorService executor = new ThreadPoolExecutor(10, 10,      60L, TimeUnit.SECONDS,      new ArrayBlockingQueue(10));   

或者是使用开源类库:开源类库,如apache和guava等。

ThreadPoolExecutor的执行流程

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务。
  2. 线程数量达到了corePools,则将任务移入队列等待。
  3. 队列已满,新建线程(非核心线程)执行任务。
  4. 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常(拒绝策略)
  5. 新建线程->达到核心数->加入队列->新建线程(非核心)->达到最大数->触发拒绝策略

ThreadPoolExecutor参数说明

  1. corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
  2. maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列,则maximumPoolSize不起作用,因为无法提交至核心线程池的线程会一直持续地放入workQueue(工作队列)中。
  3. keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
  4. allowCoreThreadTimeout:默认情况下超过keepAliveTime的时候,核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。
  5. unit:可以指定keepAliveTime的时间单位。
  6. workQueue
    • ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。需要指定队列大小。
    • LinkedBlockingQueue若指定大小则和ArrayBlockingQueue类似,若不指定大小则默认能存储Integer.MAX_VALUE个任务,相当于无界队列,此时maximumPoolSize值其实是无意义的。此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
    • SynchronousQueue同步阻塞队列,当有任务添加进来后,必须有线程从队列中取出,当前线程才会被释放,newCachedThreadPool就使用这种队列。
    • PriorityBlockingQueue 一个具有优先级的无限阻塞队列。
    • RejectedExecutionHandler:线程数和队列都满的情况下,线程池会执行的拒绝策略,有四个(也可以使用自定义的策略)。
    • AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略。
    • DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式。
    • DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行。
    • CallerRunPolicy:拒绝新任务进入,如果该线程池还没被关闭,那么这个新的任务在执行线程中被调用。
    • Executors和ThreadPoolExecutor创建线程的区别

如何向线程池中提交任务

可以通过execute()或submit()两个方法向线程池提交任务。

  • execute()方法没有返回值,所以无法判断任务知否被线程池执行成功。
  • submit()方法返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值。

如何关闭线程池

可以通过shutdown()或shutdownNow()方法来关闭线程池。

  • shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
  • shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

初始化线程池时线程数的选择

  • 如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。
  • 如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,更多的线程数也只能增加上下文切换,不能增加CPU利用率。

上述只是一个基本思想,如果真的需要精确的控制,还是需要上线以后观察线程池中线程数量跟队列的情况来定。

线程优先级

Linux中,使用nice value(以下成为nice值)来设定一个进程的优先级,系统任务调度器根据nice值合理安排调度。

nice的取值范围为-20到19。 通常情况下,nice的默认值为0。视具体操作系统而定。 nice的值越大,进程的优先级就越低,获得CPU调用的机会越少,nice值越小,进程的优先级则越高,获得CPU调用的机会越多。 一个nice值为-20的进程优先级最高,nice值为19的进程优先级最低。 父进程fork出来的子进程nice值与父进程相同。父进程renice,子进程nice值不会随之改变。

由于Android基于Linux Kernel,在Android中也存在nice值。但是一般情况下我们无法控制,原因如下:

Android系统并不像其他Linux发行版那样便捷地使用nice命令操作。 renice需要root权限,一般应用无法实现。

Android中的线程优先级别目前规定了如下,了解了进程优先级与nice值的关系,那么线程优先级与值之间的关系也就更加容易理解。

  • THREAD_PRIORITY_DEFAULT,默认的线程优先级,值为0。
  • THREAD_PRIORITY_LOWEST,最低的线程级别,值为19。
  • THREAD_PRIORITY_BACKGROUND 后台线程建议设置这个优先级,值为10。
  • THREAD_PRIORITY_FOREGROUND 用户正在交互的UI线程,代码中无法设置该优先级,系统会按照情况调整到该优先级,值为-2。
  • THREAD_PRIORITY_DISPLAY 也是与UI交互相关的优先级界别,但是要比THREAD_PRIORITY_FOREGROUND优先,代码中无法设置,由系统按照情况调整,值为-4。
  • THREAD_PRIORITY_URGENT_DISPLAY 显示线程的最高级别,用来处理绘制画面和检索输入事件,代码中无法设置成该优先级。值为-8。 THREAD_PRIORITY_AUDIO 声音线程的标准级别,代码中无法设置为该优先级,值为 -16。
  • THREAD_PRIORITY_URGENT_AUDIO 声音线程的最高级别,优先程度较THREAD_PRIORITY_AUDIO要高。代码中无法设置为该优先级。值为-19。
  • THREAD_PRIORITY_MORE_FAVORABLE 相对THREAD_PRIORITY_DEFAULT稍微优先,值为-1。
  • THREAD_PRIORITY_LESS_FAVORABLE 相对THREAD_PRIORITY_DEFAULT稍微落后一些,值为1。

使用Android API为线程设置优先级也很简单,只需要在线程执行时调用android.os.Process.setThreadPriority方法即可。这种在线程运行时进行修改优先级,效果类似renice。

Android应用程序包含线程

我们创建一个只有一个页面一个按钮的android应用,启动时会产生几个线程呢?这些线程分别是做什么?

我们可以想到的有:

  • 主线程

  • 6.0开始有了渲染线程

  • gc线程 回收守护线程, 回收监控线程

  • binder线程池 4个线程

  • JVM agent *2

看看通过AndroidStudio profile看到的:

image.png

像Profile Saver猜测是性能检测工具注入的。其它的我们可以带着问题从framework中寻找。

之前做电视项目的时候遇到了录音丢帧问题,最后定位到是因为CPU打满,录音线程被阻塞引起。为了解决问题首先想到的是提升录音线程优先级,但是不管调用Android哪个录音API系统都会为应用分配一个AudioRecorder线程,我们无法修改这个线程的优先级,而且AudioRecorder线程本身优先级就是-19,已经很高了。所以后续的优化思路只能是整个APP层面性能优化。

线程注意事项

我们不管是在写代码还是阅读别人代码时,要经常思考所看的方法是运行在哪个线程,避免多线程并发引起的问题。在我们做架构设计或者SDK设计时要考虑对外暴露的接口的线程安全性。

总结

本文总结了线程的基础知识,以及线程池,线程优先级相关的东西,并且介绍了一个最简单APP所包含的线程及作用。

收起阅读 »

Kotlin - Compose 编程思想

Kotlin - Compose Compose 编程思想 Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而...
继续阅读 »

Kotlin - Compose 

Compose 编程思想 

Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。此术语需要一些解释说明,它的含义对应用设计非常重要。

声明性编程范式

长期以来,Android 视图层次结构一直可以表示为界面微件树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新      以显示当前数据。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变微件的内部状态。

手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以意外的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护复杂性会随着需要更新的视图数量而增长。

在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。

重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减轻这一成本,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。

简单的可组合函数 

使用 Compose,您可以通过定义一组接受数据而发出界面元素的可组合函数来构建界面。

关于此函数,有几点值得注意:

- 此函数带有 @Composable 注释。所有可组合函数都必须带有此注释;此注释可告知 Compose 编译器:此函数旨在将数据转换为界面。 - 此函数接受数据。可组合函数可以接受一些参数,这些参数可让应用逻辑描述界面。 - 此函数可以在界面中显示文本。为此,它会调用 Text() 可组合函数,该函数实际上会创建文本界面元素。可组合函数通过调用其他可组合函数来发出界面层次结构。 - 此函数不会返回任何内容。发出界面的 Compose 函数不需要返回任何内容,因为它们描述所需的屏幕状态,而不是构造界面微件。 - 此函数快速、幂等且没有副作用。     - 使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对 random() 的调用。     - 此函数描述界面而没有任何副作用,如修改属性或全局变量

    一般来说,出于重组部分所述的原因,所有可组合函数都应使用这些属性来编写。

示例         

    @Composable
    private fun PreTitle(){
        MdcTheme(this, readColors = true) {
            Title(title = titleString)
        }
    }
    
    @Composable
    private fun Title(title: String) {
        Text(
            text = title,
            style = MaterialTheme.typography.h5
        )
    }
复制代码

声明性范式转变

在许多面向对象的命令式界面工具包中,您可以通过实例化微件树来初始化界面。您通常通过膨胀 XML 布局文件来实现此目的。每个微件都维护自己的内部状态,并且提供 getter 和 setter 方法,允许应用逻辑与微件进行交互。

在 Compose 的声明性方法中,微件相对无状态,并且不提供 setter 或 getter 函数。实际上,微件不会以对象形式提供。您可以通过调用带有不同参数的同一可组合函数来更新界面。这使得向架构模式(如 ViewModel)提供状态变得很容易,如应用架构指南中所述。然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。

动态内容

由于可组合函数是用 Kotlin 而不是 XML 编写的,因此它们可以像其他任何 Kotlin 代码一样动态

重组

在命令式界面模型中,如需更改某个微件,您可以在该微件上调用 setter 以更改其内部状态。在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 -- 系统会根据需要使用新数据重新绘制函数发出的微件。Compose 框架可以智能地仅重组已更改的组件。

重组是指在输入更改时再次调用可组合函数的过程。当函数的输入更改时,会发生这种情况。当 Compose 根据新输入重组时,它仅调用可能已更改的函数或 lambda,而跳过其余函数或 lambda。通过跳过所有未更改参数的函数或 lambda,Compose 可以高效地重组。

切勿依赖于执行可组合函数所产生的附带效应,因为可能会跳过函数的重组。如果您这样做,用户可能会在您的应用中遇到奇怪且不可预测的行为。附带效应是指对应用的其余部分可见的任何更改。例如,以下操作全部都是危险的附带效应:          - 写入共享对象的属性 - 更新 ViewModel 中的可观察项 - 更新共享偏好设置

可组合函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。可组合函数应快速执行,以避免在播放动画期间出现卡顿。如果您需要执行成本高昂的操作(例如从共享偏好设置读取数据),请在后台协程中执行,并将值结果作为参数传递给可组合函数。

注意的事项

可组合函数可以按任何顺序执行 

如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。

可组合函数可以并行运行

Compose 可以通过并行运行可组合函数来优化重组。这样一来,Compose 就可以利用多个核心,并以较低的优先级运行可组合函数(不在屏幕上)。

这种优化意味着,可组合函数可能会在后台线程池中执行。如果某个可组合函数对 ViewModel 调用一个函数,则 Compose 可能会同时从多个线程调用该函数。

为了确保应用正常运行,所有可组合函数都不应有附带效应,而应通过始终在界面线程上执行的 onClick 等回调触发附带效应。

调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免使用修改可组合 lambda 中的变量的代码,既因为此类代码并非线程安全代码,又因为它是可组合 lambda 不允许的附带效应。

重组会跳过尽可能多的内容

如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行界面树中在其上面或下面的任何可组合项。每个可组合函数和 lambda 都可以自行重组。

同样,执行所有可组合函数或 lambda 都应该没有附带效应。当您需要执行附带效应时,应通过回调触发。

重组是乐观的操作

只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。

取消重组后,Compose 会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组成操作,也会应用该附带效应。这可能会导致应用状态不一致。

确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。

可组合函数可能会非常频繁地运行

在某些情况下,可能会针对界面动画的每一帧运行一个可组合函数。如果该函数执行成本高昂的操作(例如从设备存储空间读取数据),可能会导致界面卡顿。

例如,如果您的微件尝试读取设备设置,它可能会在一秒内读取这些设置数百次,这会对应用的性能造成灾难性的影响。

如果您的可组合函数需要数据,它应为相应的数据定义参数。然后,您可以将成本高昂的工作移至组成操作线程之外的其他线程,并使用 mutableStateOf 或 LiveData 将相应的数据传递给 Compose。

收起阅读 »

面试官问我JVM内存结构,我真的是

jvm
面试官:今天来聊聊JVM的内存结构吧? 候选者:嗯,好的 候选者:前几次面试的时候也提到了:class文件会被类加载器装载至JVM中,并且JVM会负责程序「运行时」的「内存管理」 候选者:而JVM的内存结构,往往指的就是JVM定义的「运行时数据区域」 候选者:...
继续阅读 »

面试官今天来聊聊JVM的内存结构吧?


候选者:嗯,好的


候选者:前几次面试的时候也提到了:class文件会被类加载器装载至JVM中,并且JVM会负责程序「运行时」的「内存管理」


候选者:而JVM的内存结构,往往指的就是JVM定义的「运行时数据区域」


候选者:简单来说就分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈


候选者:要值得注意的是:这是JVM「规范」的分区概念,到具体的实现落地,不同的厂商实现可能是有所区别的。



面试官嗯,顺便讲下你这图上每个区域的内容吧。


候选者:好的,那我就先从「程序计数器」开始讲起吧。


候选者:Java是多线程的语言,我们知道假设线程数大于CPU数,就很有可能有「线程切换」现象,切换意味着「中断」和「恢复」,那自然就需要有一块区域来保存「当前线程的执行信息」


候选者:所以,程序计数器就是用于记录各个线程执行的字节码的地址(分支、循环、跳转、异常、线程恢复等都依赖于计数器)


面试官:好的,理解了。


候选者:那接下来我就说下「虚拟机栈」吧


候选者:每个线程在创建的时候都会创建一个「虚拟机栈」,每次方法调用都会创建一个「栈帧」。每个「栈帧」会包含几块内容:局部变量表、操作数栈、动态连接和返回地址



候选者:了解了「虚拟机栈」的组成后,也不难猜出它的作用了:它保存方法了局部变量、部分变量的计算并参与了方法的调用和返回。


面试官:ok,了解了


候选者:下面就说下「本地方法栈」吧


候选者:本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的「本地方法」指的是「非Java方法」,一般本地方法是使用C语言实现的。


面试官:嗯…


候选者:嗯,说完了「本地方法栈」、「虚拟机栈」和「程序计数器」,哦,下面还有「方法区」和「堆」


候选者:那我先说「方法区」吧


候选者:前面提到了运行时数据区这个「分区」是JVM的「规范」,具体的落地实现,不同的虚拟机厂商可能是不一样的


候选者:所以「方法区」也只是 JVM 中规范的一部分而已。


候选者:在HotSpot虚拟机,就会常常提到「永久代」这个词。HotSpot虚拟机在「JDK8前」用「永久代」实现了「方法区」,而很多其他厂商的虚拟机其实是没有「永久代」的概念的。



候选者:我们下面的内容就都用HotSpot虚拟机来说明好了。


候选者:在JDK8中,已经用「元空间」来替代了「永久代」作为「方法区」的实现了


面试官:嗯…


候选者:方法区主要是用来存放已被虚拟机加载的「类相关信息」:包括类信息、常量池


候选者:类信息又包括了类的版本、字段、方法、接口和父类等信息。


候选者:常量池又可以分「静态常量池」和「运行时常量池」


候选者:静态常量池主要存储的是「字面量」以及「符号引用」等信息,静态常量池也包括了我们说的「字符串常量池」。


候选者:「运行时常量池」存储的是「类加载」时生成的「直接引用」等信息。



面试官:嗯…


候选者:又值得注意的是:从「逻辑分区」的角度而言「常量池」是属于「方法区」的


候选者:但自从在「JDK7」以后,就已经把「运行时常量池」和「静态常量池」转移到了「堆」内存中进行存储(对于「物理分区」来说「运行时常量池」和「静态常量池』就属于堆)


面试官:嗯,这信息量有点多


面试官我想问下,你说从「JDK8」已经把「方法区」的实现从「永久代」变成「元空间」,有什么区别?


候选者:最主要的区别就是:「元空间」存储不在虚拟机中,而是使用本地内存,JVM 不会再出现方法区的内存溢出,以往「永久代」经常因为内存不够用导致跑出OOM异常。


候选者:按JDK8版本,总结起来其实就相当于:「类信息」是存储在「元空间」的(也有人把「类信息」这块叫做「类信息常量池」,主要是叫法不同,意思到位就好)


候选者:而「常量池」用JDK7开始,从「物理存储」角度上就在「堆中」,这是没有变化的。



面试官:嗯,我听懂了


面试官最后来讲讲「堆」这块区域吧


候选者:嗯,「堆」是线程共享的区域,几乎类的实例和数组分配的内存都来自于它


候选者:「堆」被划分为「新生代」和「老年代」,「新生代」又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成


候选者:不多BB,我也画图吧



候选者:将「堆内存」分开了几块区域,主要跟「内存回收」有关(垃圾回收机制)


面试官:那垃圾回收这块等下次吧,这个延伸下去又很多东西了


面试官你要不先讲讲JVM内存结构和Java内存模型有啥区别吧?


候选者:他们俩没有啥直接关联,其实两次面试过后,应该你就有感觉了


候选者:Java内存模型是跟「并发」相关的,它是为了屏蔽底层细节而提出的规范,希望在上层(Java层面上)在操作内存时在不同的平台上也有相同的效果


候选者:Java内存结构(又称为运行时数据区域),它描述着当我们的class文件加载至虚拟机后,各个分区的「逻辑结构」是如何的,每个分区承担着什么作用。


面试官:了解了


今日总结:JVM内存结构组成(JVM内存结构又称为「运行时数据区域」。主要有五部分组成:虚拟机栈、本地方法栈、程序计数器、方法区和堆。其中方法区和堆是线程共享的。虚拟机栈、本地方法栈以及程序计数器是线程隔离的)



作者:Java3y
链接:https://juejin.cn/post/7025763374566735886
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

大力作业灯APP编译内存治理

背景 随着作业灯业务的蓬勃发展,大力客户端的编译情况劣化越来越严重。sync一次项目需要长达五分钟,本地编译耗时也极长,还会经常出现GC over limit 错误,严重影响开发效率。CI编译时长经常超过20分钟,严重影响合码效率。 以上劣化已经严重影响到日常...
继续阅读 »

背景


随着作业灯业务的蓬勃发展,大力客户端的编译情况劣化越来越严重。sync一次项目需要长达五分钟,本地编译耗时也极长,还会经常出现GC over limit 错误,严重影响开发效率。CI编译时长经常超过20分钟,严重影响合码效率。


以上劣化已经严重影响到日常研发工作,急切需要改善。


前期调研


针对上述情况,我们首先对本地编译情况做了具体的调研。


本地首次全量编译,耗时10分钟。接着不做任何修改,尝试第二次增量编译,耗时长达15分钟。第三次增量编译直接报GC over limit 错误。如果每次编译过后,清理掉Java进程,就不会有这个问题。查看每次编译的Java进程,内存都是打满到8G。


本地sync一次项目,耗时长达9分钟。第二次sync,10分钟后才结束。第三次直接报GC over limit 错误。同样的,每次sync后清理掉Java进程,就不会有卡死问题。


同时发现,由于本地Java进程占用内存过多,导致电脑会有明显的发热以及卡顿现象,非常影响开发体验。


思路分析


首先,我们可以从本地编译中,很容易观察到内存泄漏情况。每次sync之后,内存占用都是成倍增加,这说明其中存在很严重的内存泄漏问题。解决这些问题,可以有效缓解本地编译问题。


内存治理


variantFilter 过滤多余configuration


一开始我们只猜测是某些插件有内存泄漏问题,具体是哪些插件,为什么会有内存泄漏,我们也没什么思路。 在前期调研时,发现sync占用内存极多,且内存泄漏严重(多次sync会卡死报错)。决定先从sync场景入手,来进行内存治理。



后续复盘发现,这是一个非常正确的决定。当我们处理问题没有思路时,最好是找到一个简单的场景去深入分析。这里的重点就是找到sync这个简单的场景,相比较build编译,sync任务更为简单,更好给我们去复现问题。



首先我们需要知道,sync过后,内存占用情况。使用 VisualVM 获取sync实时的内存情况。这里是用 VisualVM 对新创建的Java进程进行实时监控,这个Java进程也就是gradle创建的deamon进程。


1


查看sync过程中内存变化情况,最终sync完成后,堆内存打满到8G,实际占用内存高达6.3G。dump出来hprof文件,我们使用MAT来分析当前内存情况。


分析dump文件,看Top Consumers中,6.3G的内存中,DefaultConfiguration_Decorated实例占了83%。我也不知道这个现象是不是正常的,这时候正好看到公司一篇文档中介绍如何解决编译中OOM问题,文档中提到,configuration数量由 模块数 * flavor 数 * buildType 数 * 每个 variant 对应的 Configurations 数决定。


我们项目中有三个flavor(其中有一个是最近新增的,这也能解释为什么劣化这么严重),主仓中有80+module,再加上debug、release两个buildType,Android Studio sync时会加载所有flavor以及buildType情况,这样可以在build variants中给我们提供所有选项。这也导致我们项目一次sync configuration内存占用高达5G。


这里我们可以参考Android官网关于variantFilter的使用,将当前flavor之外另外两个flavor给屏蔽掉。这样可以减少sync和开发过程中,内存占用,也可以减少configuration的时间。在项目的build.gradle 下面增加如下代码:


if (!project.rootProject.ext.BuildContext.isCI) {

// 本地开发减少flavor的configuration配置

afterEvaluate {

if (it.plugins.hasPlugin('com.android.library') || it.plugins.hasPlugin('com.android.application')) {

def flavorName = DEFAULT_FLAVOR_NAME

def mBuildType = DEFAULT_BUILD_TYPE

boolean needIgnore = false

for(String s : gradle.startParameter.taskNames){

s = s.toLowerCase()

println("variantFilter taskName = ${s}")

//当涉及到组件升级或者组件检查时,不使用variantFilter

if(s.contains("publish") || s.contains("checkchanged")){

needIgnore = false

}

if(s.contains("release")){

mBuildType = "release"

}

if(s.contains("flavor1")){

flavorName = "flavor1"

break

}else if(s.contains("flavor2")){

flavorName = "flavor2"

break

}else if(s.contains("flavor3")){

flavorName = "flavor3"

break

}

}

if(needIgnore){

println("variantFilter flavorName = ${flavorName},mBuildType = ${mBuildType}")

android {

variantFilter { variant ->

def names = variant.flavors*.name

if (!names.empty && !names.contains(flavorName)) {

setIgnore(true)

println("ignore variant ${names}")

}

def buildType = variant.getBuildType().getName()

if (buildType != mBuildType) {

setIgnore(true)

println("ignore variant ${buildType}")

}

}

}

}

}

}

}

在gradle.properties中设置默认的flavor和build type,在开发过程中如果需要切换flavor,可以在此切换。


# flavor default setting

DEFAULT_FLAVOR_NAME = flavor1

DEFAULT_BUILD_TYPE = debug

过滤之后,我们查看一下sync时内存情况:



堆大小5.5G,内存实际占用3.2G 。我们通过添加variant 过滤,减少3G的内存占用。


sync内存泄漏治理


上面一个过滤就减少了3G的内存占用,这是一个好消息。我们开始继续排查多次sync会GC over limit问题。这时候尝试再次sync,内存又增加了3.2G,内存占用直接翻倍。


这时候有两个猜想:


1.上一次sync时的内存占用,没有成功回收,形成内存泄漏。


2.第二次sync本应该使用第一次sync的缓存,由于某些原因,它没有复用反而自行创建新的缓存。


这时候我们还是先dump heap,来分析一下堆内存情况。这里直接抓取两次sync后的内存情况,看看是哪里有泄漏。




可以看到,两次sync后,其中configuration增加了一倍。到底是什么原因呢?其实这时候,我还是不太会使用这个软件,去搜索了MAT的正确使用方式,发现其中leak suspects功能会自动帮我们找出内存中可能存在的内存泄漏情况。



Leak suspects提示有两个内存泄漏可疑点,这里针对问题a,发现是defaultConfiguration_Decorated都是被 seer中的dependencyManager引用。到这个时候,我还是不确定是内存泄漏,还是内存没有复用导致的问题。其实后面复盘发现,MAT已经很明确给出了内存泄漏的建议,这时候问题应该已经很明朗了。但还是由于对gradle sync机制不够了解,仍然身处迷雾中。


这时候查看了一下上面VisitableURLClassLoader的path2GC(也就是查看它到GCroots的引用链),发现是build scan包中的一个线程对其有引用,导致其内存泄漏。而且在sync两次后这个线程从一个变成了两个!



通过这一步分析,我们可以确定,这就是泄漏问题。GCRoot来自我们接入的公司插件。找插件的维护人解决上述问题后,再次连续执行两次sync,内存还是翻倍了。


这时候我也学会如何使用MAT来分析内存泄漏问题了,直接查看一次sync之后的hprof文件。查看leak suspects,第一个问题变成了ActionRunningListener,第二个问题是configuration。



第二个的内存泄漏是大头,总共都有1.1G,而第一个只有280M,我们先分析第一个。



我们可以看到这里有两个相同的listener对象,我们直接看其中一个listener的path2GC,找到内存泄漏的GCROOTS。



这里可以发现,GCROOT来自另一个插件中的KVJsonHelper类。查看了一下它的源码,KVJsonHelper里面使用了一个static 变量引用了gradle。


这时候我也想搞清楚,为什么这里会是内存泄漏。我们两次sync,都使用的同一个gradle进程,静态变量在一个进程中,不是只会存在一个吗?查了相关资料,也阅读了公司相应文档,总算找到了原因。


gradle对象在每次编译或者sync都会重新创建(不会也不需要缓存),而这个重新创建,是会创建新的classloader,那么gradle对象也就不一样了。原有的gradle是一个GCroots,其中引用到了ActionRunningListener, 导致内存泄漏。这里涉及到gradle的类加载机制,具体原理可以查看gradle相关文档,这里就不赘述了。


找相关同学说明上下文,协助我们解决了这个问题。这时候再次sync发现内存还是翻倍。


看来这种问题还不少。接下来的问题排查与上面相似,就不赘述了。我们后续相继排查出另外几个中都有同样的问题。都是有GCROOT直接或者间接引用了gradle,导致gradle对象无法被回收。而gradle对象以及它所引用的对象,高达3G。


这里其实还有个小插曲,当我们解决完所有内存泄漏后,再次sync,发现内存还是翻倍。这时候准备dump heap出来分析时,发现内存占用都被回收了。原来VisualVM 的dump功能会先执行FULL GC ,而我们项目sync完成后也会执行full GC,但是由于mbox插件会在sync之后执行一次buildSrc ,导致这次fullGC没有回收成功,等插件任务执行完后,没有后续GC操作,所以内存依然存在。



这时候,内存泄漏已经完全解决了。我们总共帮助5个插件解决了内存泄漏问题,将本地内存占用从3G降低到了100M 。这时候还有一个遗留问题,为什么GC过后,实际内存占用100M,而堆大小还是6G呢?这就需要下面的,gradle JVM调优了。


Gradle JVM 调优


sync时的内存确实降下去了,但是build编译时间还是很长,CI上release编译也被同学疯狂吐槽太慢了。这该如何是好?查看了一下这时候CI上编译时长,都超过20Mins了。



挑了一个时间长的编译任务,看了看其中的耗时task。



整体编译时长24分钟,R8任务占了18mins。这时候点到内存分析,GC时间竟然逼近12mins。都占了整体时长的一半了。



查看了一下内存情况,发现到编译后期,内存几乎打满,导致一直GC。



看来是我们设置的8G最大堆内存不够用,决定将其增加到16G。在gradle.properties中,修改gradle进程Java堆的最大值。


org.gradle.jvmargs=-Xmx16384M -XX:MaxPermSize=8192m -Dkotlin.daemon.jvm.options="-Xmx8192M" 

上面参数将gradle进程内存最大值增加到16G,kotlin gradle进程内存最大值8G。本地尝试了一下,发现编译速度确实快了很多。在CI上编译release包,编译时间从之前的20分钟,缩减到了10分钟,大大超出了我们的预期。


主要原因是,我们编译时间有大部分都消耗在GC时间上(占比百分之50+),我们提升了进程内存的最大值,GC时间大大降低,编译时间也就相应降低。


这时候发现一个新的问题,我们编译过程中,随着内存占用的增多,堆越来越大,后面一度到达13G 。但是当编译完成后,内存被回收到1G,堆还是13G ,有12G的空余内存。这不是浪费空间吗?



这个问题跟上面sync的遗留问题相似,我们开始尝试减少空余空间的比例。给gradle的进程增加新参数:-XX:MaxHeapFreeRatio=60 -XX:MinHeapFreeRatio=40,设置这两个参数,是用来控制堆中空闲内存的最大比例和最小比例的。


其实上面图中,就是设置过这个参数的测试结果。并不可行。这是为什么呢?在这个问题上排查了很久,搜到一些答案是说,现在GC并不会实时去更改堆的内存大小。


那这个空余内存,该怎么处理呢?这里我做了多种尝试,发现gradle对自己的deamon进程已经做过很好的优化了。我所尝试的新增参数做优化,可能适得其反。


这个时候转换思路,我们不需要在意是否有这么多空余内存占用,我们只需要确保,这个Java进程不会影响到我们日常电脑使用就OK。


deamon进程有一个参数可以设置保活时间,这个保活时间的意义是,当进程超过这个时间还没有新的任务时,会自动结束。保活时间默认3个小时,这里我们可以将其设置为一个小时,避免因为长时间占用电脑内存,影响其他工作。


优化结果


至此,我们的内存治理就告一段落了。



  • 我们治理了项目编译过程中的内存泄漏问题,多次编译内存占用只会缓慢上升,彻底杜绝了GC over limit 导致的编译错误。同时也将sync时间,从8分钟优化到1.5分钟,提升了本地研发效率。

  • 我们提升了项目gradle进程内存占用最大值,将编译过程中GC占用时间从50% 降低到了5%,将CI编译时间从20分钟缩减到10分钟,大大提升了研发合码效率。


内存治理,效果十分显著,既解决了本地编译的难题,也提升了CI编译速度。


总结


通过上述内容,我们总结了如下几条经验:



  1. 在多flavor项目中,我们可以通过使用variantFilter过滤非必须的variant资源,降低编译过程中内存占用。

  2. 我们在写gradle插件时,也应该注意,不要直接使用静态变量引用gradle对象,避免不必要的内存泄漏。

  3. 合理配置项目gradle daemon进程阈值,减少项目编译过程中,GC时长占用比例。

作者:字节大力智能
链接:https://juejin.cn/post/7026141134376140830
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

我该如何给Flutter webview添加透明背景?

为何写这篇文章 承接以上前言,我之所以写这篇文章,是因为我改的是Flutter官方的插件webview_flutter。Flutter官方的插件,全部是在一个gitHub仓库上维护的,各个库之间又相互关联。【见下图】 所以改功能其实是其次,如何在这庞大的Fl...
继续阅读 »

为何写这篇文章


承接以上前言,我之所以写这篇文章,是因为我改的是Flutter官方的插件webview_flutter。Flutter官方的插件,全部是在一个gitHub仓库上维护的,各个库之间又相互关联。【见下图】
Flutter官方插件库


所以改功能其实是其次,如何在这庞大的Flutter插件库里面单独修改你所需的插件,并且自行维护起来,这才是重点!!!我觉得是很有必要分享给大家的。


需求背景


这次改动的原因是Flutter webview的背景永远是白色,而我们的主题又是黑色的。这样H5嵌入的时候,需要H5的同事去设置黑色背景,同时网页还没加载出来时还会出现白色背景,体验极差。 如果能把webview的背景设置为透明,H5不需要设置背景色,从可维护性和体验都会有所提高,因此需求应运而生。


实现步骤



  1. 从github把plugin仓库clone下来,然后用Android studio单独打开webview文件夹,可以看到包含了三个Plugin。


2.png
2. 更改主插件依赖项


进入主插件webview目录,看到yaml依赖的还是pub上面的插件,所以我们改动其他目录下的源码,yaml根本就没有依赖到,是不会生效的。


flutter:
plugin:
platforms:
android:
default_package: webview_flutter_android
ios:
default_package: webview_flutter_wkwebview

dependencies:
flutter:
sdk: flutter
webview_flutter_platform_interface: ^1.0.0
webview_flutter_android: ^2.0.13
webview_flutter_wkwebview: ^2.0.13

所以我要先把yaml的依赖改为相对路径,这样我们对代码的改动才会生效。改完flutter pub get,跟着源码走下去,都能进入本地的源文件,good👍🏻
3.png



  1. 开始改代码


在webview的build方法中,可以看到通过WebView.platform.build传入构造的参数,然后判断平台返回对应视图。


/// webview_flutter/webview_flutter/lib/src/webview.dart
@override
Widget build(BuildContext context) {
return WebView.platform.build(
context: context,
onWebViewPlatformCreated: _onWebViewPlatformCreated,
webViewPlatformCallbacksHandler: _platformCallbacksHandler,
javascriptChannelRegistry: _javascriptChannelRegistry,
gestureRecognizers: widget.gestureRecognizers,
creationParams: _creationParamsfromWidget(widget),
);
}

/// 根据设备类型返回对应的视图
static WebViewPlatform get platform {
if (_platform == null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
_platform = AndroidWebView();
break;
case TargetPlatform.iOS:
_platform = CupertinoWebView();
break;
default:
throw UnsupportedError(
"Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one");
}
}
return _platform!;
}

/// 设置参数,这里我加多了一个transparentBackground参数,是bool类型的
CreationParams _creationParamsfromWidget(WebView widget) {
return CreationParams(
initialUrl: widget.initialUrl,
webSettings: _webSettingsFromWidget(widget),
javascriptChannelNames: _extractChannelNames(widget.javascriptChannels),
userAgent: widget.userAgent,
autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy,
transparentBackground: widget.transparentBackground,
);
}

以Android为例,来到webview_flutter_android的webview_android.dart中。可以看到通过AndroidView引入原生视图,通过标识 'plugins.flutter.io/webview' 进行匹配。


/// webview_flutter/webview_flutter_android/lib/webview_android.dart
return GestureDetector(
onLongPress: () {},
excludeFromSemantics: true,
child: AndroidView(
viewType: 'plugins.flutter.io/webview',
onPlatformViewCreated: (int id) {
if (onWebViewPlatformCreated == null) {
return;
}
onWebViewPlatformCreated(MethodChannelWebViewPlatform(
id,
webViewPlatformCallbacksHandler,
javascriptChannelRegistry,
));
},
gestureRecognizers: gestureRecognizers,
layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl,
creationParams:
MethodChannelWebViewPlatform.creationParamsToMap(creationParams),
creationParamsCodec: const StandardMessageCodec(),
),
);

之后进入android目录,找到对应的FlutterWebView文件,通过获取methodcCannel传入的params来判断是否需要启用透明背景,如果为true,则设置backgroundColor为透明。
wecom-temp-83ef5fa160fe25fafb861bd53bb2c344.png



Ps:这里可以看出,原生插件的编写很简单。只要通过methodChannel进行通信,Flutter层传入params,原生层获取参数后进行解析。注意数据传输都是转为字符串类型的,甚至很多时候都是用json字符串流转。
同时这里也返回了原生视图,通过PlatformView绑定标识,返回对应PlatformViewFactory的视图,标识同样也是字符串类型。
所以是非常傻瓜式的,Flutter只不过提供了一个通信桥梁来实现跨平台,原生代码还是得自己写,从这个角度来看,Flutter真的跨平台了吗?积极开源的社区对于Flutter而已,必要性何其重呢?值得我们深思!



ios端也一样,核心代码就是修改wkWebview的背景色为透明


/// webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
NSNumber* transparentEnabled = args[@"transparentBackground"];
NSLog(@"transparentBackground >>> %i", [transparentEnabled boolValue]);
// 设置背景色为透明
if([transparentEnabled boolValue]){
NSLog(@"开始设置背景色");
_webView.opaque = NO;
_webView.backgroundColor = UIColor.clearColor;
}

重点:如何引入到项目中使用并且独立维护


功能实现完,要考虑如何引入到我们的项目中使用。以往个人开发的插件一般都是通过git仓库引入,所以我创建新的GitHub仓库,把改好的webview目录整个上传上去。加入项目中flutter pub get后惊喜的发现,失败了!

原因是:根目录下没有yaml文件,对于Flutter来说是一个不合格的Plugin。 所以我们需要指定插件的根目录,通过path来指定到webview文件夹下。


 # webview组件
webview_flutter:
git:
url: git://github.com/~~~/webview_flutter_enable_transparent.git
ref: main
path: webview_flutter # 指定路径

继续get,再次惊喜!!!
原因是:相对路径 ../XXXX 找不到对应的插件,这合理吗?
很合理,因为Flutter必须保证你的包是最小的,你既然指定了依赖的库,那我就只会下载对应的库,并不是整个git下载下来。所以我们用相对路径的时候,根本找不到根目录下其他的插件。
解决方法,webview的yaml文件中所依赖的插件也需要用git引入


dependencies:
flutter:
sdk: flutter
webview_flutter_platform_interface:
git:
url: https://github.com/~~~/webview_flutter_enable_transparent.git
ref: main
path: webview_flutter_platform_interface
webview_flutter_android:
git:
url: https://github.com/~~~/webview_flutter_enable_transparent.git
ref: main
path: webview_flutter_android
webview_flutter_wkwebview:
git:
url: https://github.com/~~~/webview_flutter_enable_transparent.git
ref: main
path: webview_flutter_wkwebview

再次get,运行起来了,非常完美!👌🏻


WebView(
initialUrl: "xxxxx",
javascriptMode: JavascriptMode.unrestricted,
transparentBackground: true,
)

写在最后


这次对于插件的更改,其实功能并不难。但是对于Flutter官方插件的更改,以及如果放到自己的git上进行维护,我认为这次确实让我学到了不少。
同理,自己公司的Plugin,是否用类似Flutter官方的插件管理方式来管理,会更加的合理? 笔者认为这是必须要的,赶紧创建一个仓库,按照上面的方式,组建内部插件库之旅吧!


我们一起学习、进步!!!


作者:Karl_wei
链接:https://juejin.cn/post/7025276399636185096
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter 主流状态管理框架 provider get 分析与思考

Flutter 中状态管理是一个经久不衰的话题,当下市面上也有诸如 provider 、get 、fish_redux 等框架。自接触 flutter 开发以来,我大致经历了无状态管理 、简单的状态抽象,再到目前使用的是公司内部一个类似 provider 的解...
继续阅读 »

Flutter 中状态管理是一个经久不衰的话题,当下市面上也有诸如 providergetfish_redux 等框架。自接触 flutter 开发以来,我大致经历了无状态管理 、简单的状态抽象,再到目前使用的是公司内部一个类似 provider 的解决方案。加上最近看到 张风捷特烈对状态管理的看法与理解小呆呆666
Flutter 对状态管理的认知与思考 。 我也结合过往经验和大家分享下我对于状态管理的看法和主流框架(如 provider、get )的思路分析,以及我在实践过程中踩过的一些坑。




一、为什么需要状态管理:解决响应式开发带来的问题


首先,为什么 flutter 开发中需要状态管理?在我看来,本质是因为 Flutter 响应式 的构建带来的一系列问题。传统原生开发采用 控制式 的构建,这是两种完全不同的思路,所以我们没有在原生开发中听到过状态管理一说。


1、「响应式」 VS 「控制式」分析


那么怎么理解「响应式」和「控制式」?这里我们还是用最简单的计数器例子分析:


计数器.gif


如图,点击右下角按钮,显示的文本数字加一。
这个非常简单的功能在控制式的构建模式下,应该是这么思考。


image.png


当右下角按钮点中时,拿到中间 TextView 的对象,手动设置其展示的文本。代码如下:


/// 展示的数量
private int mCount = 0;
/// 中间展示数字的 TextView
private TextView mTvContent;

/// 右下角按钮调用的方案
private void increase() {
mCount++;
mTvContent.setText(mCount);
}

而在 flutter 中,我们只需要 _counter++ ,之后调用 setState((){})即可。setState 会刷新整个页面,使的中间展示的值不断变化。


image.png


这是两种完全不同的开发思路,控制式的思路下,开发者需要拿到每一个 View 的实例处理显示
而响应式的思路下,我们只需要处理状态(数据)以及状态对应的展示(Widget)即可,剩余的都交给了 setState()。所以有这么一种说法


UI = f(state)

上面的例子中,state 就是 _counter 的值,调用 setState 驱动 f (build 方法)生成新的 UI。


那么「响应式」开发有哪些 优点 以及 问题 呢?


2、响应式开发的优点:让开发者摆脱组件的繁琐控制,聚焦于状态处理


响应式开发最大的优点我认为是 让开发者摆脱组件的繁琐控制,聚焦于状态处理。 在习惯 flutter 开发之后,我切回原生最大的感受是,对于 View 的控制太麻烦了,尤其是多个组件之间如果有相互关联的时候,你需要考处理的东西非常爆炸。而在 flutter 中我们只需要处理好状态即可(复杂度在于状态 -> UI 的映射,也就是 Widget 的构建)。


举个例子,假如你现在是一家公司的 CEO,你制定了公司员工的工作计划。控制式的开发下,你需要推动每一个员工(View)完成他们的任务。


image.png
如果你的员工越来越多,或者员工之间的任务有关联,可想而知你的工作量会有多大。



这种情况下 不是你当上了老板,而是你在为所有的员工(View)打工。



一张图来说,控制式的开发就是


image.png


这时候你琢磨,你都已经当上 CEO,干嘛还要处理这种细枝末节的小事,所以响应式开发来了。


响应式开发下,你只需要处理好每个员工的计划(状态),只等你一声令下(setState),每个员工(Widget)便会自己按照计划展示(build),让你着实体会到了 CEO 的乐趣。


image.png


一张图来说,响应式开发就是


image.png


如 jetpack compose,swift 等技术的最新发展,也是朝着「响应式」的方向前进,恋猫de小郭 也聊过。学会 flutter 之后离 compose 也不远了。


响应式开发那么优秀,它会存在哪些问题呢?


3、响应式开发存在的问题:状态管理解决的目标


我一开始接触 flutter 的时候,并没有接触状态管理,而是使用最原始的「响应式」开发。过程中遇到了很多问题,总结下来主要的有三个



逻辑和页面 UI 耦合,导致无法复用/单元测试,修改混乱等



一开始所有代码都是直接写到 widget 中,这就导致 widget 文件臃肿,并且一些通用逻辑,例如网络请求与页面状态、分页等,不同页面重复的写(CV)。这个问题在原生上同样存在,所以后面也衍生了诸如 MVP 之类的思路去解决。



难以跨组件(跨页面)访问数据



跨组件通信可以分为两种,「1、父组件访问子组件」和「2、子组件访问父组件」。第一种可以借助 Notification 机制实现,而第二种在没有接触到 element 树的时候,我使用 callback。如果遇到 widget 嵌套两层左右,就能体会到是何等的酸爽。


这个问题也同样体现在访问数据上,比如有两个页面,他们中的筛选项数据是共享,并没有一个很优雅的机制去解决这种跨页面的数据访问。



无法轻松的控制刷新范围(页面 setState 的变化会导致全局页面的变化)



最后一个问题也是上面提到的优点,很多场景我们只是部分状态的修改,例如按钮的颜色。但是整个页面的 setState 会使的其他不需要变化的地方也进行重建(build),我之前也总结过 原来我一直在错误的使用 setState()?


在我看来,Flutter 中状态管理框架的核心在于这三个问题的解决思路。下面一起看看一些主流的框架比如 provider、get 是如何解决?




二、provider、get 状态管理框架设计分析:如何解决上面三个问题?


1、逻辑和页面 UI 耦合


传统的原生开发同样存在这个问题,Activity 也存在爆炸的可能,所以有 MVP 框架进行解耦。简单来说就是将 View 中的逻辑代码抽离到 Presenter 层,View 只负责视图的构建。


image.png


这也是 flutter 中几乎所有状态管理框架的解决思路,上面的 Presenter 你可以认为是 get 中的 GetxController、provider 中的 ChangeNotifier,bloc 中的 Bloc。值得一提的是,具体做法上 flutter 和原生 MVP 框架有所不同。


我们知道在传统 MVP 模型中,逻辑处理收敛到 Presenter 中,View 专注 UI 构建,一般 View 和 Presenter 以接口定义自身行为(action),相互持有接口进行调用 (也有省事儿直接持有对象的)。


image.png


但 Flutter 中不太适合这么做,从 Presenter → View 关系上 View 在 Flutter 中对应 Widget,而 Widget 的生命周期外部是无感知的,直接拿 Widget 实例并不是好的做法。原生中有 View.setBackground 的方法,但是 flutter 中你不会去定义和调用 Widget.xxx。这一点在 flutte 中我们一般结合着局部刷新组件做 Presenter(例如 ValueListenable) -> View(ValueListenableBuilder) 的控制。


而在从 View → Presenter 的关系上,Widget 可以确实可以直接持有 Presenter,但是这样又会带来难以数据通信的问题。这一点不同状态管理框架的解决思路不一样,从实现上他们可以分为两大类,一类是 provider,bloc 这种,基于 Flutter 树机制,另一类是 get 这种通过 依赖注入 实现。下面具体看看:


A、Provider、Bloc 依赖树机制的思路


首先需要简单了解一下 Flutter 树机制是怎么回事。


我们在 flutter 中通过嵌套各种 widget,构成了一个 Widget 树。如果这时有一个节点 WidgetB 想要获取 WidgetA 中 定义的 name 属性,该怎么做?


image.png


Flutter 在 BuildContext 类中为我们提供了方法进行向上和向下的查找


abstract class BuildContext { 
///查找父节点中的T类型的State
T findAncestorStateOfType();
///遍历子元素的element对象
void visitChildElements(ElementVisitor visitor);
///查找父节点中的T类型的 InheritedWidget 例如 MediaQuery 等
T dependOnInheritedWidgetOfExactType({ Object aspect })
...... }

这个 BuildContext 对应我们在每个 Widget 的 build(context) 方法中的 context。你可以把 context 当做树中的一个实体节点。借助 findAncestorStateOfType 方法,我们可以一层一层向上的访问到 WidgetA,获取到 name 属性。


image.png


调用的 findAncestorStateOfType() 方法,会一级一级父节点的向上查找,很显然,查找快慢取决于树的深度,时间复杂度为 O(logn)。而数据共享的场景在 Flutter 中非常常见,比如主题,比如用户信息等,为了更快的访问速度,Flutter 中提供了 dependOnInheritedWidgetOfExactType() 方法,它会将 InheritedWidget 存储到 Map 中,这样子节点的查找的时间复杂变成了 O(1)。不过这两种方法本质上都是通过树机制实现,他们都需要借助 「context」


看到这里相信你应该差不多明白了,bloc、provider 正是借助这种树机制,完成了 View -> Presenter 的获取。所以每次用 Provider 的时候你都会调用 Provider.of(context)


image.png


这么做有啥好处么?显然,所有 Provider 以下的 Widget 节点,都可以通过自身的 context 访问到 Provider 中的 Presenter,这很好的解决了跨组件的通信问题,但依赖 context 我们在实践中也遇到了一些问题,我会在下一篇文章介绍。更多 View 与 Presenter 之间交互的规范设计,我非常推荐 Flutter 对状态管理的认知与思考


多提一嘴,看到这种 .of(context) 的做法你有没有很眼熟?没错,Flutter 中路由也是基于这个机制,有关路由你可看看我之前写过的 如何理解 Flutter 路由源码设计,Flutter 树机制可以看看 Widget、Element、Render是如何形成树结构? 这一系列。


B、get 通过依赖注入的方式


树机制很不错,但他依赖于 context,这一点有时很让人抓狂。get 通过依赖注入的方式,实现了对 Presenter 层的获取。简单来说,就是将 Presenter 存到一个单例的 Map 中,这样在任何地方都能随时访问。


image.png


全局单例存储一定要考虑到 Presenter 的回收,不然很有可能引起内存泄漏。使用 get 要么你手动在页面 dispose 的时候做 delete 操作,要么你使用 GetBuilder ,其实它里面也是在 dispose 去做了释放。


@override
void dispose() {
super.dispose();
if (widget.autoRemove && GetInstance().isRegistered(tag: widget.tag)) {
// 移除 Presenter 实例
GetInstance().delete(tag: widget.tag);
}
}

你可能在想,为什么使用 Provider 的时候不需要考虑这个问题?


这是因为一般页面级别的 Provider 总是跟随 PageRoute。随着页面的退出,整树中的节点都被会回收,所以可以理解为系统机制为我们解决了这个问题。


image.png


当然如果你的 Provider 层级特别高,比如在 MaterialApp 一级,这时你存储的 Presenter 也往往是一些全局的逻辑,它们的生命周期往往跟随整个 App。


2、难以跨组件(跨页面)访问数据


两类状态管理方案都能支持跨组件访问数据,在 provider 中我们通过 context 。


而跨页面访问数就像上图所说,一般 Provider 的存储节点是跟随页面,要想实现跨页面访问那么 Provider 的存储节点需要放在一个更高的位置,但同样需要注意回收的处理。而 get 因为是全局单例,无论是跨页面或者跨组件,都没有任何依赖。


3、无法轻松的控制刷新范围


这一点解法其实很多,比如系统提供的 StreamChangeNotifierValueListenable 等等。他们本质上都是通过建立 View 与数据的绑定机制,当数据发生变化的时候,响应的组件随着变化,避免额外的构建。


/// 声明可能变化的数据
ValueNotifier _statusNotifier;

ValueListenableBuilder(
// 建立与 _statusNotifier 的绑定关系
valueListenable: _statusNotifier,
builder: (c, data, _) {
return Text('$data');
})

/// 数据变化驱动 ValueListenableBuilder 局部刷新
_statusNotifier.value += 1;

这里提一点,一开始在看 get 的 Obx 组件使用时真的惊艳到了我。


class Home extends StatelessWidget {
var count = 0.obs;
@override
Widget build(context) => Scaffold(
body: Center(
child: Obx(() => Text("$count")),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => count ++,
));
}

关键代码就三行


/// 申明变量
var count = 0.obs;
/// 响应组件
Obx(() => Text("$count"))
/// 变量修改,同步 Obx 变化
count ++

What?这么简单? Obx 怎么知道他应该观察哪个变量呢?


这个机制的实现,总结下来有两点:1、变量置为变成可观察态 2、响应组件与变量建立联系。我们简单看看:



1、变量可观察态



观察他的变量申明方式,你会发现 count 不是普通的 int 类型,而是 0.obsobs 这个扩展方法会返回一个 RxInt 类型的对象,这种对象核心在于他的 getset 方法。


T get value {
if (RxInterface.proxy != null) {
RxInterface.proxy!.addListener(subject);
}
return _value;
}

set value(T val) {
// ** 省略非关键代码 **
_value = val;
subject.add(_value);
}


我们可以把这个 RxInt 类型的对象想象成一个大小姐,里面的 subject大小姐的丫鬟,每个大小姐只有一个丫鬟RxInterface.proxy 是一个静态变量,还没出现过咱们暂时把他当做小黑就行。


image.png


get value 方法中我们可以看到,每次调用 RxInt 的 get 方法时,小黑都会去关注我们的丫鬟动态。


set value 时,大小姐都会通知丫鬟。


所以小黑到底是谁?



2、响应组件与变量建立联系



真相只有一个,小黑就是我们的 Obx 组件,查看 Obx 内部代码可以看到:


@override
Widget build(BuildContext context) => notifyChilds;

Widget get notifyChilds {
// 先暂时把 RxInterface.proxy 的值存起来,build 完恢复
final observer = RxInterface.proxy;
// 每个 Obx 都有一个 _observer 对象
RxInterface.proxy = _observer;
final result = widget.build();
RxInterface.proxy = observer;
return result;
}

Obx 在调用 build 方法时,会返回 notifyChilds,这个 get 方法中将 _observer 赋给了 RxInterface.proxy_observer 和 Obx 我们认为他是一个 渣男 就行。


有了上面的认知,现在我们捋一遍整个过程


      body: Center(
child: Obx(() => Text("$count")),
),

首先,在页面的 build 方法中返回了 Obx 组件,这个时候,也就是我们的渣男登场了,现在他就是小黑


image.png


在 Obx 组件内调返回了 Text("$count")),其中 $count 其实翻译为 count.toString(),这个方法被 RxInt 重写 ,他会调用 value.toString()


@override
String toString() => value.toString();

所以 $count 等价于 count.value.toString()。还记得我们上面说过 get 方法调用的时候,小黑会去关注丫鬟么,所以现在变成了


image.png


这一天,大小姐心情大好,直接 count++,仔细一看,原来 count++ 也被重写了,调用了 value =


RxInt operator +(int other) {
value = value + other;
return this;
}

上面咱提过大小姐的 set 方法时,她会通知丫鬟。而渣男时刻注意着丫鬟,一看到丫鬟发生了变化,渣男不得快速响应,马上就来谄媚了。


image.png


整个流程可以按照上面方式理解,好,为什么我们说 Obx 是个渣男呢。因为只要是在他 build 阶段,所有调用过 get 方式的 Rx 变量他都可以观察。也就是说只要其中任意一个变量调用 set value 都会触发他的重建。


正经版可以看看 Flutter GetX深度剖析 | 我们终将走出自己的路(万字图文)


总的来说,这个设计确实还蛮巧妙的。Obx 在 build 阶段会间接观察所有里面调用过 get value 方法的 Rx 类型变量。但这会带来一个问题,必须在 build 阶段显式调用 get value,否则无法建立绑定关系。


但像 LsitView 一类的组件,子节点 build 是在在 layout 过程中进行,如果你没有提前调用 get value 这时就会产生错误。例如下方代码


Center(
child: Obx(() => ListView.builder(
itemBuilder: (i, c) => Text('${count}'),
itemCount: 10,
)),
),

image.png


当然,get 中还提供了 GetBuilder 处理局部刷新,其他的问题我们留着下一期进行分析。


作者:Nayuta
链接:https://juejin.cn/post/7025426116965826574
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

flutter 优秀dio网络拦截可视化 (IMGEEK首发)

flutter_interceptor flutter dio 拦截器 库源码:github.com/smartbackme… 开始集成 dependencies: flutter_interceptor: ^0.0.1 dio添加拦截器 _dio.int...
继续阅读 »

flutter_interceptor


flutter dio 拦截器


库源码:github.com/smartbackme…


开始集成


dependencies:
flutter_interceptor: ^0.0.1

dio添加拦截器


_dio.interceptors.add(UiNetInterceptor())

页面插入浮动窗体


Overlay.of(context)?.insert(InterceptorDraggable());

功能介绍:
1、请求可视化
2、可以复制请求内容


集成后的效果如图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7025889846870671367/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

聊聊ViewPager2实现原理以及缓存复用机制

1. 前言众所周知ViewPager2是ViewPager的替代版本。它解决了ViewPager的一些痛点,包括支持right-to-left布局,支持垂直方向滑动,支持可修改的Fragment集合等。ViewPager2内部是使用RecyclerView来实...
继续阅读 »

1. 前言

众所周知ViewPager2是ViewPager的替代版本。它解决了ViewPager的一些痛点,包括支持right-to-left布局,支持垂直方向滑动,支持可修改的Fragment集合等。ViewPager2内部是使用RecyclerView来实现的。

所以它继承了RecyclerView的优势,包含但不限于以下

  1. 支持横向和垂直方向布局
  2. 支持嵌套滑动
  3. 支持ItemPrefetch(预加载)功能
  4. 支持三级缓存

ViewPager2相对于RecyclerView,它又扩展出了以下功能

  1. 支持屏蔽用户触摸功能setUserInputEnabled
  2. 支持模拟拖拽功能fakeDragBy
  3. 支持离屏显示功能setOffscreenPageLimit
  4. 支持显示Fragment的适配器FragmentStateAdapter

如果熟悉RecyclerView,那么上手ViewPager2将会非常简单。可以简单把ViewPager2想象成每个ItemView都是全屏的RecyclerView。本文将重点讲解ViewPager2的离屏显示功能和基于FragmentStateAdapter的缓存机制。

2. 回顾RecyclerView缓存机制

本章节,简单回顾下RecyclerView缓存机制。RecyclerView有三级缓存,简单起见,这里只介绍mViewCaches和mRecyclerPool两种缓存池。更多关于RecyclerView的缓存原理,请移步公众号相关文章。

  1. mViewCaches:该缓存离UI更近,效率更高,它的特点是只要position能对应上,就可以直接复用ViewHolder,无需重新绑定,该缓存池是用队列实现的,先进先出,默认大小为2,如果RecyclerView开启了预抓取功能,则缓存池大小为2+预抓取个数,默认预抓取个数为1。所以默认开启预抓取缓存池大小为3。

  2. mRecyclerPool:该缓存池理UI最远,效率比mViewCaches低,回收到该缓存池的ViewHolder会将数据解绑,当复用该ViewHolder时,需要重新绑定数据。它的数据结构是类似HashMap。key为itemType,value是数组,value存储ViewHolder,数组默认大小为5,最多每种itemType的ViewHolder可以存储5个。

3. offscreenPageLimit原理

//androidx.viewpager2:ViewPager2:1.0.0@aar
//ViewPager2.java
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
throw new IllegalArgumentException(
"Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
}
mOffscreenPageLimit = limit;
mRecyclerView.requestLayout();
}

调用setOffscreenPageLimit方法就可以为ViewPager2设置离屏显示的个数,默认值为-1。如果设置不当,会抛异常。我们看到该方法,只是给mOffscreenPageLimit赋值。为什么就能实现离屏显示功能呢?如下代码

//androidx.viewpager2:ViewPager2:1.0.0@aar
//ViewPager2$LinearLayoutManagerImpl
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}

以水平滑动ViewPager2为例:getPageSize()表示ViewPager2的宽度,离屏的空间大小为getPageSize() * pageLimit。extraLayoutSpace[0]表示左边的大小,extraLayoutSpace[1]表示右边的大小。

假设设置offscreenPageLimit为1,简单讲,Android系统会默认把画布宽度增加到3倍。左右两边各有一个离屏ViewPager2的宽度。

4. FragmentStateAdapter原理以及缓存机制

4.1 简单使用

FragmentStateAdapter继承自RecyclerView.Adapter。它有一个抽象方法,createFragment()。它能将Fragment与ViewPager2完美结合。

public abstract class FragmentStateAdapter extends
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
public abstract Fragment createFragment(int position);
}

使用FragmentStateAdapter非常简单,Demo如下

class ViewPager2WithFragmentsActivity : AppCompatActivity() {
private lateinit var mViewPager2: ViewPager2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view_view_pager2)
mViewPager2 = findViewById(R.id.viewPager2)
(mViewPager2.getChildAt(0) as RecyclerView).layoutManager?.apply {
// isItemPrefetchEnabled = false
}
mViewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
mViewPager2.adapter = MyAdapter(this)
// mViewPager2.offscreenPageLimit = 1
}

inner class MyAdapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return 100
}

override fun createFragment(position: Int): Fragment {
return MyFragment("Item $position")
}

}

class MyFragment(val text: String) : Fragment() {
init {
println("MyFragment $text")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var view = layoutInflater.inflate(R.layout.view_item_view_pager_snap, container)
view.findViewById<TextView>(R.id.text_view).text = text
return view;
}
}
}

4.2 原理

首先FragmentStateAdapter对应的ViewHolder定义如下,它只是返回一个简单的带有id的FrameLayout。由此可以看出,FragmentStateAdapter并不复用Fragment,它仅仅是复用FrameLayout而已。

public final class FragmentViewHolder extends ViewHolder {
private FragmentViewHolder(@NonNull FrameLayout container) {
super(container);
}

@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}

@NonNull FrameLayout getContainer() {
return (FrameLayout) itemView;
}
}

然后介绍FragmentStateAdapter中两个非常重要的数据结构:

final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();

private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();

  1. mFragments:是position与Fragment的映射表。随着position的增长,Fragment是会不断的新建出来的。 Fragment可以被缓存起来,当它被回收后无法重复使用。

Fragment什么时候会被回收掉呢?

  1. mItemIdToViewHolder:是position与ViewHolder的Id的映射表。由于ViewHolder是RecyclerView缓存机制的载体。所以随着position的增长,ViewHolder并不会像Fragment那样不断的新建出来,而是会充分利用RecyclerView的复用机制。所以如下图,position 4处打上了一个大大的问号,具体的值是不确定的,它由缓存的大小以及离屏个数共同决定的。

接下来我们讲解onViewRecycled()。当ViewHolder从mViewCaches缓存中移出到mRecyclerPool缓存中时会调用该方法

@Override
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
}

该方法的作用是,当ViewHolder回收到RecyclerPool中时,将ViewHolder相关的信息从上面两张表中移除。

举例 当ViewHolder1发生回收时,position 0对应的信息从两张表中删除

最后讲解onBindViewHolder方法

@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
final long itemId = holder.getItemId();
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null && boundItemId != itemId) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}

mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
ensureFragment(position);

/** Special case when {@link RecyclerView} decides to keep the {@link container}
* attached to the window, but not to the view hierarchy (i.e. parent is null) */
final FrameLayout container = holder.getContainer();
if (ViewCompat.isAttachedToWindow(container)) {
if (container.getParent() != null) {
throw new IllegalStateException("Design assumption violated.");
}
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
placeFragmentInViewHolder(holder);
}
}
});
}

gcFragments();
}

该方法可以分成3个部分:

  1. 检查该复用的ViewHolder在两张表中是否还有残留的数据,如果有,将它从两张表中移除掉。
  2. 新建Fragment,并将ViewHolder与Fragment和position的信息注册到两张表中
  3. 在合适的时机把Fragment展示在ViewPager2上。

大概的脉络就是这样,为了避免文章冗余,其它的细支且也蛮重要的方法就没有列出来

5. 案例讲解回收机制

5.1 默认情况

默认情况:offscreenPageLimit = -1,开启预抓取功能

因为开启了预抓取,所以mViewCaches大小为3。

  1. 刚开始进入ViewPager2,没有触发Touch事件,不会触发预抓取,所以只有Fragment1
  2. 滑动到Fragment2,会触发Fragment3预抓取,由于offscreenPageLimit = -1,所以只有Fragment2会展示在ViewPager2上,1和3进入mViewCaches缓存中
  3. 滑动到Fragment3。1、2、4进入mViewCaches缓存中
  4. 滑动到Fragment4。2、3、5进入mViewCaches缓存中,由于缓存数量为3,所以1被挤出到mRecyclerPool缓存中,同时把Fragment1从mFragments中移除掉
  5. 滑动到Fragment5。Fragment6会复用Fragment1对应的ViewHolder。3、4、6进入mViewCaches缓存中,2被挤出到mRecyclerPool缓存中

5.2 offscreenPageLimit=1

offscreenPageLimit=1,所以ViewPager2一下子能展示3屏Fragment,左右各显示一屏

  1. Fragment1左边没有数据,所以屏幕只有1和2
  2. 1、2、3显示在屏幕上,同时预抓取4放入mViewCaches
  3. 2、3、4显示在屏幕上,1和5放入mViewCaches
  4. 3、4、5显示在屏幕上,1、2、6放入mViewCaches
  5. 4、5、6显示在屏幕上,2、3、7放入mViewCaches,1被回收到mRecyclerPool缓存中。Fragment1同时从mFragments中删除掉
收起阅读 »

ViewModel-Flow-LiveData,我们还是好朋友

在Android应用程序中加载UI数据可能是一个挑战。各种屏幕的生命周期需要被考虑在内,还有配置的变化导致Activity的破坏和重新创建。当用户在一个应用程序中进一步或后退,从一个应用程序切换到另一个应用程序,或者设备屏幕被锁定或解锁时,应用程序的各个屏幕会...
继续阅读 »

在Android应用程序中加载UI数据可能是一个挑战。各种屏幕的生命周期需要被考虑在内,还有配置的变化导致Activity的破坏和重新创建。

当用户在一个应用程序中进一步或后退,从一个应用程序切换到另一个应用程序,或者设备屏幕被锁定或解锁时,应用程序的各个屏幕会在互动和隐藏之间不断切换。每个组件都需要公平竞争,只有在给了资源的情况下才执行积极的工作。

配置变化发生在不同的场合:当改变设备方向、将应用程序切换到多窗口模式或调整其窗口大小、切换到黑暗或光明模式、改变默认区域或字体大小等等。

Goals of efficiency

为了在Activities和Fragments中实现高效的数据加载,从而获得最佳的用户体验,应该考虑以下几点。

  • 缓存:已经成功加载并且仍然有效的数据应该立即交付,而不是第二次加载。特别是,当一个现有的Activity或Fragment再次变得可见时,或在一个Activity因配置改变而被重新创建后。
  • 避免后台工作:当一个Activity或Fragment变得不可见时(从STARTED移动到STOPPED状态),任何正在进行的加载工作应该暂停或取消,以节省资源。这对于像位置更新或任何类型的定期刷新这样的无休止的数据流尤其重要。
  • 在配置改变期间不中断工作:这是第二个目标的例外。在配置变更期间,一个Activity被一个新的实例所取代,同时保留其状态,所以当旧的实例被摧毁时,取消正在进行的工作,在新的实例被创建时立即重新启动,会产生副作用。

Today: ViewModel and LiveData

为了帮助开发者以可管理的复杂度的代码实现这些目标,谷歌在2017年以ViewModel和LiveData的形式发布了第一个架构组件库。这是在Kotlin被引入为开发Android应用程序的推荐编程语言之前。

ViewModel是跨越配置变化而保留的对象。它们对于实现目标#1和#3很有用:在配置变化期间,加载操作可以不间断地在其中运行,而产生的数据可以缓存在其中,并与当前连接到它的一个或多个Fragment/Activity共享。

LiveData是一个简单的可观察数据持有者类,也是生命周期感知的。只有当观察者的生命周期至少处于STARTED(可见)状态时,新的值才会被派发给观察者,而且观察者会自动取消注册,这对于避免内存泄漏很方便。LiveData对于实现目标#1和#2很有用:它缓存了它所持有的数据的最新值,并且该值会自动派发给新的观察者。另外,当在STARTED状态下没有更多的注册观察者时,它会得到通知,这可以避免执行不必要的后台工作。

A graph illustrating the ViewModel Scope in relation to the Activity lifecycle

如果你是一个有经验的Android开发者,你可能已经知道所有这些了。但有必要回顾一下这些功能,以便与Flow的功能进行比较。

LiveData + Coroutines

与RxJava等反应式流解决方案相比,LiveData本身是相当有限的。

  • 它只处理与主线程之间的数据传递,把管理后台线程的重任留给了开发者。值得注意的是,map()操作符在主线程上执行其转换功能,不能用于执行I/O操作或重型CPU工作。在这种情况下,需要使用switchMap()操作符,并结合在后台线程上手动启动异步操作,即使只有一个值需要在主线程上发布回来。
  • LiveData只提供了3个转换操作:map()、switchMap()和distinctUntilChanged()。如果需要更多,你必须自己使用MediatorLiveData来实现它们。

为了帮助克服这些限制,Jetpack库还提供了从LiveData到其他技术的桥梁,如RxJava或Kotlin的coroutines。

在我看来,最简单、最优雅的桥梁是androidx.lifecycle:lifecycle-livedata-ktx Gradle依赖项提供的LiveData coroutine builder函数。这个函数类似于Kotlin Coroutines库中的flow {} builder函数,可以将一个coroutine巧妙地包装成一个LiveData实例。

val result: LiveData<Result> = liveData {
val data = someSuspendingFunction()
emit(data)
}
  • 你可以使用coroutine和coroutine上下文的所有功能,以同步的方式编写异步代码,不需要回调,根据需要在线程之间自动切换。
  • 通过从coroutine中调用emit()或emitSource()挂起函数,将新值派发给主线程上的LiveData观察者。
  • coroutine使用一个特殊的范围和生命周期与LiveData实例相联系。当LiveData变得不活跃时(在STARTED状态下不再有观察者),coroutine将自动被取消,这样就可以在不做任何额外工作的情况下达到目标2。
  • 在LiveData变得不活跃之后,coroutine的取消实际上将被延迟5秒,以便优雅地处理配置变化:如果一个新的Activity立即取代了旧的Activity,并且LiveData在超时之前再次变得活跃,那么取消将不会发生,并且可以避免不必要的重启成本(目标#3)。
  • 如果用户回到屏幕上,并且LiveData再次变得活跃,那么coroutine将自动重启,但前提是它在完成之前被取消了。一旦该程序完成,它就不会再重启,这样就可以避免在输入没有变化的情况下两次加载相同的数据(目标1)。

结论:通过使用LiveData coroutines构建器,你可以用最简单的代码获得默认的最佳行为。

如果资源库提供了以Flow形式返回数值流的函数,而不是暂停返回单一数值的函数,那该怎么办?也可以通过使用asLiveData()扩展函数将其转换为LiveData并利用上述所有特性。

val result: LiveData<Result> = someFunctionReturningFlow().asLiveData()

在SDK里,asLiveData()还使用了LiveData coroutines builder来创建一个简单的coroutine,在LiveData处于活动状态时对Flow进行collect操作。

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}

但是,让我们暂停一下--究竟什么是Flow,是否可以用它来完全替代LiveData?

Introducing Kotlin’s Flow

Charlie Chaplin turning his back on his wife labeled LiveData to look at an attractive woman labeled Flow

Flow是Kotlin的Coroutines库在2019年推出的一个类,它代表了一个异步计算的数据流。它的概念类似于RxJava的Observables,但基于coroutines,有一个更简单的API。

起初,只有冷流可用——无状态的流,每次观察者开始在coroutine的范围内collect他们的值时,都会按需创建。每个观察者得到它自己的值序列,它们不被共享。

后来,新的热流子类型SharedFlow和StateFlow被添加,并在Coroutines库的1.4.0版本中作为稳定的API毕业。

SharedFlow允许发布被广播给所有观察者的值。它可以管理一个可选的重放缓存和/或缓冲区,并且基本上取代了所有被废弃的BroadcastChannel API。

StateFlow是SharedFlow的一个专门和优化的子类,它只存储和重放最新的值。听起来很熟悉?

StateFlow和LiveData有很多共同点。

  • 它们是可观察类
  • 它们存储并向任何数量的观察者广播最新的值
  • 它们迫使你尽早捕获异常:LiveData回调中未捕获的异常会停止应用程序。热流中未捕获的异常会结束流,即使使用.catch()操作符,也不可能重新启动它。

但是它们也有重要的区别。

  • MutableStateFlow需要一个初始值,MutableLiveData不需要(注意:MutableSharedFlow(replay = 1)可以用来模拟一个没有初始值的MutableStateFlow,但是它的实现效率有点低
  • StateFlow总是使用Any.equals()进行比较来过滤相同值的重复,而LiveData则不会,除非与distinctUntilChanged()操作符相结合(注:SharedFlow也可以用来防止这种行为)。
  • StateFlow不是生命周期感知的。然而,一个Flow可以从一个生命周期感知的coroutine中collect,这需要更多的代码来设置,而不需要使用LiveData(更多细节见下文)。
  • LiveData使用版本管理来跟踪哪个值已经被派发到哪个观察者。这可以避免在回到STARTED状态时,将相同的值分派给同一个观察者两次。
  • StateFlow没有版本控制。每次一个coroutinecollect一个Flow,它都被认为是一个新的观察者,并且将总是首先接收最新的值。这可能会导致执行重复的工作,我们将在下面的案例研究中看到。

Observing LiveData vs Collecting Flow

从Fragment的一个Activity中观察一个LiveData实例是很直接的。

viewModel.results.observe(viewLifecycleOwner) { data ->
displayResult(data)
}

这是一个一次性的操作,LiveData负责将流与观察者的生命周期同步起来。

对于Flow来说,相应的操作被称为collect,collect需要通过一个协程来完成。因为Flow本身不具有生命周期意识,所以与生命周期同步的责任被转移到collectFlow的coroutine上。

要创建一个生命周期感知的coroutine,在一个Activity/Fragment处于STARTED状态时collect一个Flow,并在Activity/Fragment被销毁时自动取消collect,可以使用以下代码。

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.result.collect { data ->
displayResult(data)
}
}

但是这段代码有一个主要的限制:它只能在没有通道或缓冲区支持的冷流中正常工作。这样的流只由collect它的coroutine驱动:当Activity/Fragment移动到STOPPED状态时,coroutine将暂停,Flow producer也将暂停,在coroutine恢复之前不会发生其他事情。

然而,还有其他类型的流。

  • 热流,它总是处于活动状态,并将把结果分派给所有当前的观察者(包括暂停的观察者)。
  • 基于回调的或基于通道的冷流,当collect开始时订阅一个Activity的数据源,只有当collect被取消(不暂停)时才停止订阅。

对于这些情况,即使Flow collect的coroutine被暂停,底层的Flow生产者也会保持活跃,在后台缓冲新的结果。资源被浪费了,目标#2被错过了。

Forrest Gump on a bench saying “Life is like a box of chocolates, you never know which kind of Flow you’re going to collect.”

需要实现一种更安全的方式来collect任何类型的流。当Activity/Fragment变得不可见时,执行collect的coroutine必须被取消,并在它再次变得可见时重新启动,这与LiveData coroutine builder的做法完全一样。为此,在lifecycle:lifecycle-runtime-ktx:2.4.0中引入了新的API(在写这篇文章时仍处于alpha状态)。

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.result.collect { data ->
displayResult(data)
}
}
}

或者说是。

viewLifecycleOwner.lifecycleScope.launch {
viewModel.result
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { data ->
displayResult(data)
}
}

正如你所看到的,为了达到同样的安全和效率水平,用LiveData观察Activity或Fragment的结果更简单。

你可以在Manuel Vivo的文章《以更安全的方式从Android UIscollect流量》中了解更多关于这些新的API。

Replacing LiveData with StateFlow in ViewModels

让我们回到ViewModel。我们确立了这是一种使用LiveData异步获取数据的简单而有效的方法。

val result: LiveData<Result> = liveData {
val data = someSuspendingFunction()
emit(data)
}

我们怎样才能用StateFlow代替LiveData达到同样的效果?Jose Alcérreca写了一个很长的迁移指南来帮助回答这个问题。长话短说,对于上述用例,等效的代码是。

val result: Flow<Result> = flow {
val data = someSuspendingFunction()
emit(data)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = Result.Loading
)

stateIn()操作符将我们的冷流转换为热流,能够在多个观察者之间共享一个结果。由于SharingStarted.WhileSubscribed(5000L)的存在,热流在第一个观察者订阅时被懒散地启动,并在最后一个观察者退订后5秒被取消,这样可以避免在后台做不必要的工作,同时也考虑到了配置变化。此外,一旦上游流到达终点,它就不会被共享的coroutine自动重启,所以我们避免做两次相同的工作。

看起来我们成功地实现了我们的3个目标,并使用更复杂一点的代码复制了几乎与LiveData相同的行为。

但是仍然有一个小的关键区别:每次一个Activity/Fragment再次变得可见时,一个新的流集合将开始,StateFlow总是通过立即向观察者提供最新的结果来启动流。即使这个结果在之前的集合中已经被传递给了同一个Activity/Fragment。因为与LiveData不同,StateFlow不支持版本控制,每一个流程集合都被认为是一个全新的观察者。

这有问题吗?对于这个简单的用例,并没有:一个Activity或Fragment可以只是执行一个额外的检查,以避免更新视图,如果数据没有改变。

viewLifecycleOwner.lifecycleScope.launch {
viewModel.result
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.distinctUntilChanged()
.collect { data ->
displayResult(data)
}
}

但在更复杂的、真实的使用案例中可能会出现问题,我们将在下一节看到。

Using StateFlow as trigger in a ViewModel

一个常见的情况是使用基于触发器的方法在ViewModel中加载数据:每次触发器的值被更新时,数据就会被刷新。使用MutableLiveData,效果非常好。

class MyViewModel(repository: MyRepository) : ViewModel() {
private val trigger = MutableLiveData<String>()

fun setQuery(query: String) {
trigger.value = query
}

val results: LiveData<SearchResult>
= trigger.switchMap { query ->
liveData {
emit(repository.search(query))
}
}
}
  • 在刷新时,switchMap()操作符会将观察者连接到一个新的底层LiveData源,替换掉旧的。而且,由于上述例子使用了LiveData的coroutine构建器,先前的LiveData源将在与观察者断开连接的5秒后自动取消其相关的coroutine。在过时的值上工作可以通过一个小的延迟来避免。
  • 因为LiveData有版本控制,MutableLiveData触发器将只向switchMap()操作符分派一次新值,只要至少有一个活跃的观察者。之后,当观察者变得不活跃和再次活跃时,最新的底层LiveData源的工作就会在它停止的地方继续进行。

这段代码足够简单,并且达到了所有效率的目标。

现在让我们看看是否可以用MutableStateFlow代替MutableLiveData来实现同样的逻辑。

天真的方法:

class MyViewModel(repository: MyRepository) : ViewModel() {
private val trigger = MutableStateFlow("")

fun setQuery(query: String) {
trigger.value = query
}

val results: Flow<SearchResult> = trigger.mapLatest { query ->
repository.search(query)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = SearchResult.EMPTY
)
}

MutableLiveData和MutableStateFlow的API非常接近,触发代码看起来几乎相同。最大的区别是mapLatest()转换函数的使用,对于单个返回值,它相当于LiveData的switchMap()(对于多个返回值,应该使用flatMapLatest())。

mapLatest()的工作原理与map()类似,但不是依次对所有输入值完全执行转换,而是立即消耗输入值,在一个单独的coroutine中异步执行转换。当一个新的值在上游流程中发出时,如果之前的值的转换循环程序仍在运行,它将被立即取消,一个新的循环程序将被启动以取代它。这样一来,就可以避免在过时的值上工作。

到目前为止还不错。然而,这段代码的主要问题来了:因为StateFlow不支持版本控制,当流程集合重新启动时,触发器将重新发送最新的值。每当Activity/Fragment在不可见超过5秒后再次变得可见时就会发生这种情况。

Britney Spears singing “Oops!… I emit again”

而当触发器再次发出相同的值时,mapLatest()转换将再次运行,用相同的参数再次冲击存储库,尽管结果已经被传递和缓存了!

目标1被错过了:仍然有效的数据不应该被第二次加载。

Preventing re-emission of the latest trigger value

接下来想到的问题是:我们是否应该防止这种重新加载,以及如何防止?StateFlow已经处理了从流程集合中扣除值的问题,而distinctUntilChanged()操作符对其他类型的流程也做了同样的处理。但是没有标准的操作符来重复同一流程的多个集合的值,因为流程集合应该是独立的。这是与LiveData的一个主要区别。

在使用stateIn()操作符的多个观察者之间共享Flow的特定情况下,发射的值将被缓存,并且在任何给定的时间,最多只有一个collect源Flow的coroutine。看起来很有诱惑力的是,黑掉一些运算符函数,这些运算符函数会记住以前collect的最新值,以便在新的collect开始时能够跳过它。

// Don't do this at home (or at work)
fun <T> Flow<T>.rememberLatest(): Flow<T> {
var latest: Any? = NULL
return flow {
collectIndexed { index, value ->
if (index != 0 || value !== latest) {
emit(value)
latest = value
}
}
}
}

备注:一位细心的读者注意到,同样的行为可以通过将MutableStateFlow替换成Channel(capacity = CONFLATED),然后用receiveAsFlow()将其变成一个Flow来实现。通道永远不会重新释放值。

不幸的是,上面的逻辑是有缺陷的,当下游的流转换在完成之前被取消时,将不能按预期的那样工作。

代码假设在emit(value)返回后,该值已经被处理,如果流程集合重新开始,就不应该再被发射,但这只有在使用无缓冲的Flow操作符时才是真的。像mapLatest()这样的操作符是有缓冲的,在这种情况下,emit(value)会立即返回,而转换是异步执行的。这意味着没有办法知道一个值何时被下游的流完全处理。如果流collect在异步转换的中间被取消,我们仍然需要在流collect重新启动时重新发射最新的值,以便恢复该转换,否则该值将丢失。

TL; DR:在ViewModel中使用StateFlow作为触发器会导致每次Activity/Fragment再次变得可见时的重复工作,并且没有简单的方法来避免它。

这就是为什么在ViewModel中使用LiveData作为触发器时,LiveData要优于StateFlow,尽管在Google的 "Advanced coroutines with Kotlin Flow "代码实验室中没有提到这些差异,这意味着Flow的实现方式与LiveData的实现方式完全相同。事实并非如此。

Conclusion

以下是我基于上述演示的建议。

  • 在你的Android UI层和ViewModels中继续使用LiveData,特别是用于触发器。尽可能地使用它来暴露数据,以便在Activities和Fragments中消耗:它将使你的代码既简单又高效。
  • LiveData coroutine builder函数是你的朋友,在许多情况下可以取代ViewModels中的Flows。
  • 当你需要时,你仍然可以使用Flow运算符的力量,然后将产生的Flow转换为LiveData。
  • Flow比LiveData更适用于应用程序的所有其他层,如存储库或数据源,因为它不依赖于Android特定的生命周期,而且更容易测试。

现在你知道了,如果你还想完全 "随波逐流",将LiveData从你的Android UI层中铲除,你愿意做哪些取舍了。

收起阅读 »

android Compose中沉浸式设计和导航栏的处理

Material Design风格的顶部和底部导航栏Compose中Material Design风格的设计我们的做法如下:1、使用Scafoold作为页面的顶级,Scafoold中承载topbar和bottombar分别作为顶部导航栏和底部导航栏。2、调用W...
继续阅读 »

Material Design风格的顶部和底部导航栏

Compose中Material Design风格的设计我们的做法如下:

1、使用Scafoold作为页面的顶级,Scafoold中承载topbar和bottombar分别作为顶部导航栏和底部导航栏。

2、调用WindowCompat.setDecorFitsSystemWindows(window, false)方法让我们的布局超出状态栏和底部导航栏的位置 3、使用ProvideWindowInsets包裹布局,使我们可以获取到状态栏和底部导航栏的高度(不包裹无法获取状态栏和底部导航栏高度) 4、手动处理顶部和底部导航栏让页面适应屏幕

界面设计

TopBar设计

实现方式

因为使用WindowCompat.setDecorFitsSystemWindows(window, false)设置后页面布局顶到了状态栏的上面,因为我们需要用一个Spacer来填充状态栏,让我们的布局看起来正常点

代码

如下是封装的状态栏方法

@Composable
fun TopBarView(title: String, callback: () -> Unit) {
Column {
Spacer(
modifier = Modifier
.statusBarsHeight()//设置状态栏高度
.fillMaxWidth()
)
TopAppBar(title = {
Text(title)
}, navigationIcon = {
IconButton(onClick = {
callback()
}) {
Icon(Icons.Filled.ArrowBack, "")
}
})
}
}

处理状态栏前后的ui状态

处理前:

 处理后:

 结论是经过我们的处理后解决了状态栏的遮挡

BottomBar设计

实现方式

因为使用ProvideWindowInsets包裹后底部导航栏顶到了底部,所以需要填充一个底部导航栏高度的Spacer。

代码

bottomBar = {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(statusbarColor),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
){
Text(text = "首页")
Text(text = "通讯录")
Text(text = "朋友圈")
Text(text = "我的")

}
Spacer(modifier = Modifier.navigationBarsHeight())
}
}

处理状态栏前后的ui状态

处理前:

处理后:

结论是经过我们的处理后解决了底部导航栏的遮挡问题

状态栏和底部导航栏颜色的处理

状态栏和底部导航栏颜色设置

依赖

   implementation "com.google.accompanist:accompanist-insets:0.16.0"
implementation "com.google.accompanist:accompanist-systemuicontroller:0.16.0"

代码

 rememberSystemUiController().run {
setStatusBarColor(statusbarColor, false)
setSystemBarsColor(statusbarColor, false)
setNavigationBarColor(statusbarColor, false)
}

整体效果

我们发现状态栏和底部导航栏的颜色都变了

如何处理内容部分超出底部导航栏的区域

使用WindowCompat.setDecorFitsSystemWindows(window, false)处理了页面后,Scafoold的内容区域也会被顶到底部导航栏的下方,同样也需要我们处理

以下是处理前和处理后的代码和效果

处理前

代码

LazyColumn() {
items(30) { index ->
Box(
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth()
.height(50.dp)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Text(text = index.toString())
}
}
}

效果

这里只展示到第27个item,第28、29个item没有展示出来,所以需要处理才行 

处理后

代码

 {padding->
LazyColumn(Modifier.padding(bottom = padding.calculateBottomPadding())) {//这里会计算出距离底部的距离,然后设置距离底部的padding
items(30) { index ->
Box(
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth()
.height(50.dp)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Text(text = index.toString())
}
}
}

}

效果

改正后的第29个item展示了出来 

代码:github.com/ananananzhu…

收起阅读 »

算法题:String类型转int类型(不用Java内置函数)

如何不采用java的内置函数,把String类型转换为int类型,想到两种方法,如下代码自己测试下 package com.journey.test; public class AtoiTest { public static void main(Str...
继续阅读 »

如何不采用java的内置函数,把String类型转换为int类型,想到两种方法,如下代码自己测试下


package com.journey.test;

public class AtoiTest {
public static void main(String[] args) throws Exception {
String s = "-2233113789";
System.out.println("转换前的字符串: " + s);
System.out.println("atoi1转换后的字符串:" + atoi1(s));
System.out.println("atoi2转换后的字符串:" + atoi2(s));

}

方法一:遍历检索法,遍历字符串,charAt() 方法用于返回指定索引处的字符,取出字符对照0-9的数字。


  /**
* 不用java内置函数,将String字符串转换为数字
* @param s
* @return
* @throws Exception
*/
public static int atoi1(String s) throws Exception {
if (s == null || s.length() == 0) {
throw new Exception("要转换的字符串为空,无法转换!");
}
int retInt = 0;
int[] num = new int[s.length()];
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '-':
num[i] = -1;
break;
case '0':
num[i] = 0;
break;
case '1':
num[i] = 1;
break;
case '2':
num[i] = 2;
break;
case '3':
num[i] = 3;
break;
case '4':
num[i] = 4;
break;
case '5':
num[i] = 5;
break;
case '6':
num[i] = 6;
break;
case '7':
num[i] = 7;
break;
case '8':
num[i] = 8;
break;
case '9':
num[i] = 9;
break;
default:
throw new Exception("要转换的字符串格式错误,无法转换!");
}
}
for (int i = 0; i < num.length; i++) {
if (num[i] < 0 && i > 0) {
throw new Exception("要转换的字符串格式错误,无法转换!");
}
if (num[i] < 0) {
continue;
}
retInt += Math.pow(10, num.length - i - 1) * num[i];
}
if (num[0] == -1) {//代表负数
retInt = -retInt;
}
return retInt;
}


方法二:判断字符是否在 范围 s.charAt(i)>'9' || s.charAt(i)<'0'


  /**
* 不用java内置函数,将String字符串转换为数字
* @param s
* @return
* @throws Exception
*/
public static int atoi2(String s) throws Exception{
int retInt = 0;
if (s == null || s.length() == 0) {
throw new Exception("要转换的字符串为空,转换失败!");
}
boolean isNegative = false;
for (int i = 0; i < s.length(); i++) {
if (i==0) {
if(s.charAt(i)=='-'){
isNegative = true;
continue;
}
}else{
if(s.charAt(i)>'9' || s.charAt(i)<'0'){
throw new Exception("要转换的字符串格式错误,转换失败!");
}
}
retInt *=10;
retInt += s.charAt(i) - '0';
}
return isNegative ? -retInt : retInt;
}
}

作者:大咚雷
链接:https://juejin.cn/post/7025761253503008798
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Window和WindowManager和ViewRootImpl

1 Window1.1什么是Window?Window是一个抽象类,提供了绘制窗口的一组通用API。Window负责Android中的显示,可以理解为一个View的载体,负责将这个View显示出来。-PhoneWindow是Window的唯一子类。举例:Act...
继续阅读 »

1 Window

1.1什么是Window?

  • Window是一个抽象类,提供了绘制窗口的一组通用API。
  • Window负责Android中的显示,可以理解为一个View的载体,负责将这个View显示出来。-
  • PhoneWindow是Window的唯一子类。

举例:Activity的mWindow属性就是一个Window对象,它实际是一个PhoneWindow对象,这个对象负责Activity的显示。DecorView是Activity中所有View的根View,因此mWindow对象可以说是DecorView的载体,负责将这个DecorView显示出来。

1.2 Window的类型

类型层级(z-ordered)例子
应用 Window1~99Activity
子 Window1000~1999Dialog
系统 Window2000~2999Toast
  • 子 Window无法单独存在,必须依赖与父级Window,例如Dialog必须依赖与Activity的存在。
  • Window分层,在显示时层级高的窗口会覆盖在在层级低的窗口。

2 WindowManager

2.1 什么是WindowManager?

WindowManager是窗口管理器,它是一个接口,继承了ViewManager接口。

public interface ViewManager//定义对View的增删改
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

public interface WindowManager extends ViewManager {}//可见WindowManager也提供对View的增删改的接口方法

WindowManagerImpl是WindowManager的具体实现类。

获取WindowManagerImpl对象的方法:

  • context.getSystemService(Context.WINDOW_SERVICE)

  • context.getWindowManager()

2.2 WindowManager的作用

其实Window的具体创建和实现是位于系统级服务WindowManagerService内部的,我们本地应用是无法直接访问的,因此需要借助WindowManager来实现与系统服务通信,使得系统服务创建和显示窗口。通过WindowManager与WindowManagerService的交互的过程是一个IPC过程。因此可以说WindowManager是访问Window的入口

  • WindowManager作为我们唯一访问Window的入口,却只提供了对View的增删改操作。因此可以说操控Window的核心就是对载体View的操作。

2.3 使用WindowManager创建Window的过程

通过调用WindowManagerImpl对象的addView方法,会让系统的窗口服务按我们的要求帮我们创建一个窗口,并在这个窗口中添加我们提供的View。

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,mContext.getUserId());
}
  • addView方法需要传入一个View对象和一个WindowManager.LayoutParams对象。WindowManager.LayoutParams比较常用的属性有flags与type,我们通过flags设置窗口属性,通过type设置窗口的类型。

可以看到,WindowManagerImpl内部是委托mGlobal的成员变量来实现的,mGlobal是一个WindowManagerGlobal对象。

public final class WindowManagerImpl implements WindowManager {
...
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
}

WindowManagerGlobal是单例模式,即一个进程中只有一个WindowManagerGlobal实例,所有的WindowManagerImpl对象都是委托这个实例进行代理的。

//经典懒汉式线程安全单例模式(那还记得双检锁和静态内部类方式实现吗...)
private static WindowManagerGlobal sDefaultWindowManager;

private WindowManagerGlobal() {
}

public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
  • WindowManagerGlobal维护了4个集合来统一管理整个进程中的所有窗口的信息,分别是:
  private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
属性集合作用
mViewsArrayList<View>存储了所有Window所对应的View
mRootsArrayList<ViewRootImpl>存储了所有Window所对应的ViewRootImpl
mParamsArrayList<WindowManager.LayoutParams>存储了所有Window所对应的布局参数
mDyingViewsArraySet<View>存储的是即将被删除的View对象或正在被删除的View对象

WindowManager的addView方法委托给了mGlobal的addView方法。

WindowManagerGlobal.addView

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
//检查参数是否合法
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
//子Window需要调整部分布局参数
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}

ViewRootImpl root;
View panelParentView = null;

synchronized (mLock) {
...
//创建ViewRootImpl对象
root = new ViewRootImpl(view.getContext(), display);
//设置View的布局属性
view.setLayoutParams(wparams);
//将相关信息保存到对应集合
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

try {
root.setView(view, wparams, panelParentView, userId);//调用ViewRootImpl对象的setView方法(这里也是View绘制的根源)
} catch (RuntimeException e) {
...
}
}
}

3 ViewRootImpl

3.1 什么是ViewRootImpl?

ViewRootImpl是一个类,实现了ViewParent接口(该接口定义了成为一个View的parent的一些“职能”)。

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {}

ViewRootImpl是链接WindowManager和DecorView的纽带(其前身叫ViewRoot)。ViewRootImpl有很多作用,它负责Window中对View的操作,是View的绘制流程和事件分发的发起者。WindowManager与WindowManagerService的IPC交互也是ViewRootImpl负责的,mGlobal的很多操作也都是通过ViewRootImpl来实现的。

PS:看到这我们可以类比WindowManager和ViewGroup的关系。

  • ViewGroup实现了ViewManager和ViewParent两个接口,
  • WindowManager实现了ViewManager接口,同时其内部通过ViewRootImpl来操控View的,ViewRootImpl实现了ViewParent接口。

因此一个进程中的所有WindowManager共同的合作的结果可以看成是一个负责管理该进程所有窗口的窗口Group,内部有很多窗口,并且能对这些窗口进行增删改。(个人看法)

3.2 ViewRootImpl的创建

public ViewRootImpl(Context context, Display display, IWindowSession session,boolean useSfChoreographer) {
mContext = context;
mWindowSession = session;//从WindowManagerGlobal中传递过来的IWindowSession的实例,它是ViewRootImpl和WMS进行通信的代理。
mDisplay = display;
mThread = Thread.currentThread();//保存当前线程
mFirst = true; //true表示第一次添加视图
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
...
}
  • ViewRootImpl保存当前线程到mThread,然后每次处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),ViewRootImpl就会判断发起请求的thread与这个mThread是否相同,不相等就会抛出异常,由于ViewRootImpl是在主(UI)线程中创建的,且UI操作只能在主线程中运行。Activity中的ViewRootImpl的创建是在activity.handleResumeActivity方法中调用windowManager.addView(decorView)中。
  • AttachInfo是View的内部类,AttachInfo对象存储了当前View树所在窗口的各种信息,并且会派发给View树中的每一个View。保存在每个View自己的mAttachInfo变量中。因此同一个View树下的所有View绑定的是同一个AttachInfo对象和同一个ViewRootImpl对象
    • view.getViewRootImpl获取ViewRootImpl对象
    • Window对象可以通过获取DecorView再获取ViewRootImpl对象

3.3 继续Window创建的过程

ViewRootImpl.setView方法是View绘制流程的源头

ViewRootImpl.setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
setView(view, attrs, panelParentView, UserHandle.myUserId());
}

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {
synchronized (this) {
if (mView == null) {
...
requestLayout();
...
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
}
}
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//判断是否创建ViewRootImpl时的线程(Activity中是主线程)
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//创建一个同步屏障(详见Android消息机制)
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//发送一条异步消息,mTraversalRunnable是处理这条消息的回调
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);//移除同步屏障

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}

performTraversals();//View的绘制起点

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

ViewRootImpl.setView中先调用了requestLayout,完成View的绘制,再通过mWindowSession(IWindowSession是一个Binder对象,真正的实现类是Session)远程调用了addToDisPlay方法来完成Window的添加操作。

  • requestLayout中为什么要通过向主线程发送异步消息的方式来完成View的绘制呢???

  • 在Activity的onCreate调用了setContentView后,只是将View添加到了DecorView中,DecorView真正的绘制是在activity.handleResumeActivity方法中,该方法最后会回调activity的onResume方法,因此你会发现在onCreate方法中创建子线程去更新UI不会报错。

  • performTraversals在绘制的最后会用dispatchOnGlobalLayout回调OnGlobalLayoutListener的onGlobalLayout()方法。因此我们可以使用view.getViewTreeObserver().addOnGlobalLayoutListener,实现onGlobalLayout() 方法来即将绘制完成的回调(至少measure和layout结束了)详见View的绘制流程

  • 另外当手动调用invalidate,postInvalidate,requestInvalidate也会最终调用performTraversals,来重新绘制View。

一个结论:一个Window对应一个View也对应一个ViewRootImpl对象。


收起阅读 »

View的事件分发机制

1 基本概念1.1 事件分发的对象是谁?当用户触摸屏幕时将产生点击事件(Touch事件),其相关细节(发生触摸的位置、时间等)会被封装成MotionEvent对象。MotionEvent对象就是事件分发的对象。事件类型事件类型具体动作MotionEvent.A...
继续阅读 »

1 基本概念

1.1 事件分发的对象是谁?

  • 当用户触摸屏幕时将产生点击事件(Touch事件),其相关细节(发生触摸的位置、时间等)会被封装成MotionEvent对象。MotionEvent对象就是事件分发的对象。

  • 事件类型

    事件类型具体动作
    MotionEvent.ACTION_DOWN按下,手指触碰屏幕(事件的开始)
    MotionEvent.ACTION_UP抬起,手指离开屏幕(通常情况下,事件的结束)
    MotionEvent.ACTION_MOVE滑动,手指在屏幕上滑动
    MotionEvent.ACTION_CANCEL手势被取消了,不再接受后续事件(非人为原因)
    MotionEvent.ACTION_OUTSIDE标志着用户触碰到了正常的UI边界
    MotionEvent.ACTION_POINTER_DOWN代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。
    MotionEvent.ACTION_POINTER_UP非最后一个手指抬起
  • 一连串事件通常都是以DOWN事件开始、UP事件结束,中间有 0 ~ 无数个MOVE事件。

when(event?.action?.and(MotionEvent.ACTION_MASK)){} //多指触控需要和MotionEvent.ACTION_MASK取并,才能检测到

1.2 事件分发的本质

  • 将产生的MotionEvent传递给某个具体的View 处理(消费)的整个过程
  • 一旦事件被某个View消费就会返回true,所有View都没有消费的话就会返回false。

1.3 事件分发的顺序

  • Activity → ViewGroup → View
  • 事件最先传递到Activity中,再传递给DecorView(ViewGroup对象),也就是整颗View树的根节点,紧接着沿着View树向下传递(递归过程),直到传递到叶子结点(View对象)。分发过程中,事件一旦在任意地方被消费掉,分发就直接结束

事件是如何到达Activity的?(建议看完这篇文章后,最后来看)

首先触摸信息被系统底层驱动获取,然后交给InputManagerService处理,也就是IMS。IMS会根据这个触摸信息通过WMS找到要分发的window,然后IMS将触摸信息发送给window对应的ViewRootImpl(所以WMS只是提供window相关信息——ViewRootImpl)。随后ViewRootImpl将触摸信息分发给顶层View。在Activity中顶层View就是DecorView,DecorView重写了onDispatchTouchEvent,会将触摸信息分发个Window.Callback接口,而Activity实现了这个接口,并在创建布局的时候将自己设置给了DecorView,所以其实是重新分发回Activity了。

详见文章:juejin.cn/user/393150…

2 事件的分发机制

  • 因此理解View事件的分发机制,就是要理解Activity、 ViewGroup 和View分别是如何分发事件的。

  • Activity、 ViewGroup 和View处理分发离不开以下三个方法:

    方法作用调用时机
    dispatchTouchEvent()分发事件传递到当前对象时(最先调用的方法)
    onTouchEvent()处理事件**(ViewGroup没有重写,调用的是View的)**在dispatchTouchEvent()内部调用
    onInterceptTouchEvent()拦截事件**(三者中只有ViewGroup才有的方法)**在dispatchTouchEvent()内部调用

2.1 Activity的事件分发机制

MotionEvent最先传递到Activity,然后调用dispatchTouchEvent()方法

2.1.1 Activity的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
//当是按下事件时,调用onUserInteraction()
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//调用PhoneWindow的superDispatchTouchEvent(ev)
if (getWindow().superDispatchTouchEvent(ev)) {
//如果PhoneWindow中消费了事件,意味着分发结束了,直接返回true
return true;
}
//如果PhoneWindow中没有消费事件,调用Activity的onTouchEvent,看看Activity会不会消费此事件
return onTouchEvent(ev);
}

非重点(可跳过):onUserInteraction()是个空实现,可被重写,它会在按下屏幕(Activity范围内)的时候回调(还会dispatchKeyEvent、dispatchTrackballEvent等其他事件的一开始调用,但是像按键和轨迹球现在的Android几乎已经见不到了)。此外还会在很多onUserLeaveHint()回调的地方一起回调,onUserLeaveHint()就是因为用户自身选择进入后台时回调(系统选择不会)。总结onUserInteraction()会在和Activity交互时回调(事件,home返回,点击通知栏跳转其他地方等)

来看看PhoneWindow中的superDispatchTouchEvent(ev)方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
//调用DecorView的superDispatchTouchEvent(event)
return mDecor.superDispatchTouchEvent(event);
}

来看看DecorView中的superDispatchTouchEvent(ev)方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
//调用父类的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}

由于FrameLayout没有重写dispatchTouchEvent,所以进入ViewGroup的dispatchTouchEvent

2.2.2 ViewGroup的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//定义返回对象,默认返回false
boolean handled = false;
...
//调用onInterceptTouchEvent看是否需要拦截
intercepted = onInterceptTouchEvent(ev);
//既不取消也不拦截则遍历子View来处理
if (!canceled && !intercepted) {
...
final int childrenCount = mChildrenCount;
...
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//看点击事件的位置是否在某个子View内部
if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
...
//若存在这样的子View的话调用dispatchTransformedTouchEvent方法,该方法根据child是否为空做出不同反应
//child不为空,调用child.dispatchTouchEvent(event)
//child为空,则调用super.dispatchTouchEvent(event)
//即都是调用View的dispatchTouchEvent
handled = dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
}
...
//若没有子View消费事件,则ViewGroup看看自己是否要消费此事件
//child为空,内部调用super.dispatchTouchEvent(event),即调用ViewGroup的父类View.dispatchTouchEvent(event)
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
...
}
...
return handled;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
2.2.3 ViewGroup的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}

没啥好分析的,拦截就返回true,不拦截就返回false,可重写此方法来拦截事件。

2.2.4 View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
//定义返回结果
boolean result = false;
...
ListenerInfo li = mListenerInfo;
if (li != null
&& li.mOnTouchListener != null //1.设置了setOnTouchListener则为true
&& (mViewFlags & ENABLED_MASK) == ENABLED //2.判断当前点击的控件是否enable,大部分控件默认都是enable
&& li.mOnTouchListener.onTouch(this, event)) { //3.mOnTouchListener.onTouch方法的返回值
//以上三个条件都满足则返回true,意味着点击事件已被消费
result = true;
}
//若仍未被消费,调用onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}

可以看出OnTouchListener中的onTouch方法优先级高于onTouchEvent(event)方法

2.2.5 View的onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
//抬起事件,performClickInternal内部调用performClick方法
case MotionEvent.ACTION_UP:
...
performClickInternal();
...
//点击和移动事件内部会判断是否长按,用于抬起事件判断是否触发长按的回调
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
...
}

switch外层还嵌套着判断,提供默认返回:若该控件可点击,返回true,不可点击,返回false。

public boolean performClick() {
...
final boolean result;
final ListenerInfo li = mListenerInfo;
//如果回调了onClick方法,证明事件被消费,返回true。没有则返回false
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
...
return result;
}
2.2.6 Activity的onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}

return false;
}

来看看Window中的shouldCloseOnTouch方法

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
setFinishOnTouchOutside(true)//因此,像Dialog这种可以修改mCloseOnTouchOutside的值,实现点击外部时关闭

分发流程总结:从Activity.dispatchTouchEvent开始分发事件给DecorView这个ViewGroup,在从ViewGroup.dispatchTouchEvent向下分发,ViewGroup中先调用onInterceptTouchEvent判断是否需要拦截,如果不需要拦截就递归分发直到叶子结点的子View,View调用dispatchTouchEvent中有onTouchListener的话先调用onTouch方法,在根据返回情况调用自身onTouchEvent方法,onTouchEvent中抬起事件中检查是否有onClickListener,有的话调用onClick方法消费事件,没有的话,回到ViewGroup,所以子View都不消费事件的话调用自身父类的onTouchEvent,就是View中的,同样检查一遍。如果DecorView所有子View都不消费,且自身也不消费,就回到Acticity。调用Activity的onTouchEvent,如果有设置点击Activity外消费的话,且事件确实是Activity外部的话就有Activity消费,否则返回false。

收起阅读 »

含有边框的TextView-Android

前言实际的项目中我们经常会遇到边框的问题,一开始我都是直接用shape来实现,但是这种方式非常的麻烦,后面又用了三方库SuperTextView,后面学习了自定义View自己来实现一下吧.Codepublic class BorderTextView exte...
继续阅读 »

前言

实际的项目中我们经常会遇到边框的问题,一开始我都是直接用shape来实现,但是这种方式非常的麻烦,后面又用了三方库SuperTextView,后面学习了自定义View自己来实现一下吧.

Code

public class BorderTextView extends AppCompatTextView {

public BorderTextView(Context context) {
this(context, null);
}

public BorderTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public BorderTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}


/**
* @param borderColor border颜色
* @param borderWidths border 宽度
* @param borderRadius border 圆角半径
*/
public void setBorder(final int borderColor, final int[] borderWidths, final int[] borderRadius) {
setTextColor(borderColor);
Drawable drawable = new GradientDrawable() {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
drawBorder(canvas, borderColor, borderWidths, borderRadius);
}
};
setBackground(drawable);
}

/**
* 绘制border
*/
private void drawBorder(Canvas canvas, final int borderColor, final int[] borderWidths, final int[] borderRadius) {
//获取当前canvas的宽高
Rect rect = canvas.getClipBounds();
final int width = rect.width();
final int height = rect.height();

int borderWidthLeft;
int borderWidthTop;
int borderWidthRight;
int borderWidthBottom;

//取得我们的边框宽度,并附加给相应变量
if (borderWidths != null && borderWidths.length == 4) {
borderWidthLeft = Math.min(width / 2, borderWidths[0]);
borderWidthTop = Math.min(height / 2, borderWidths[1]);
borderWidthRight = Math.min(width / 2, borderWidths[2]);
borderWidthBottom = Math.min(height / 2, borderWidths[3]);
} else {
return;
}

// 设置画笔
Paint paint = new Paint();
//抗锯齿
paint.setAntiAlias(true);
//画笔颜色
paint.setColor(borderColor);
//画笔样式
paint.setStyle(Paint.Style.STROKE);
//设置边框宽度
paint.setStrokeWidth(borderWidthLeft);

// 判断当前边框是否相等
if ((borderWidthLeft == borderWidthTop) && (borderWidthLeft == borderWidthRight) && (borderWidthLeft == borderWidthBottom)) {
if (borderWidthLeft == 0) {
return;
}
// borderRadius != null且borderWidth!-0;计算并画出圆角边框,否则为直角边框
if (borderRadius != null && borderRadius.length == 4) {
int sum = 0;
/**
* 循环传递的最后一个参数,相加
* 是数组的原因是适应更多的边框需求,因为你不一定四个边框都是一个圆角度数
*/
for (int i = 0; i < borderRadius.length; i++) {
if (borderRadius[i] < 0) {
return;
}
sum += borderRadius[i];
}
//如果传递的都是0直接绘制即可
if (sum == 0) {
canvas.drawRect(rect, paint);
}
int borderWidth = borderWidthLeft;

int mMaxRadiusX = width / 2 - borderWidth / 2;
int mMaxRadiusY = height / 2 - borderWidth / 2;

int topLeftRadiusX = Math.min(mMaxRadiusX, borderRadius[0]);
int topLeftRadiusY = Math.min(mMaxRadiusY, borderRadius[0]);
int topRightRadiusX = Math.min(mMaxRadiusX, borderRadius[1]);
int topRightRadiusY = Math.min(mMaxRadiusY, borderRadius[1]);
int bottomRightRadiusX = Math.min(mMaxRadiusX, borderRadius[2]);
int bottomRightRadiusY = Math.min(mMaxRadiusY, borderRadius[2]);
int bottomLeftRadiusX = Math.min(mMaxRadiusX, borderRadius[3]);
int bottomLeftRadiusY = Math.min(mMaxRadiusY, borderRadius[3]);

//绘制左上圆角,通过旋转来达到圆角的效果,本质上其实绘制的是圆弧
if (topLeftRadiusX < borderWidth || topLeftRadiusY < borderWidth) {

RectF arc1 = new RectF(0, 0, topLeftRadiusX * 2, topLeftRadiusY * 2);
paint.setStyle(Paint.Style.FILL);
canvas.drawArc(arc1, 180, 90, true, paint);
} else {
RectF arc1 = new RectF(borderWidth / 2, borderWidth / 2, topLeftRadiusX * 2 - borderWidth / 2, topLeftRadiusY * 2 - borderWidth / 2);
paint.setStyle(Paint.Style.STROKE);
canvas.drawArc(arc1, 180, 90, false, paint);
}
//绘制上方的边框
canvas.drawLine(topLeftRadiusX, borderWidth / 2, width - topRightRadiusX, borderWidth / 2, paint);

//绘制右上圆角
if (topRightRadiusX < borderWidth || topRightRadiusY < borderWidth) {
RectF arc2 = new RectF(width - topRightRadiusX * 2, 0, width, topRightRadiusY * 2);
paint.setStyle(Paint.Style.FILL);
canvas.drawArc(arc2, 270, 90, true, paint);
} else {
RectF arc2 = new RectF(width - topRightRadiusX * 2 + borderWidth / 2, borderWidth / 2, width - borderWidth / 2, topRightRadiusY * 2 - borderWidth / 2);
paint.setStyle(Paint.Style.STROKE);
canvas.drawArc(arc2, 270, 90, false, paint);
}
//绘制右边边框
canvas.drawLine(width - borderWidth / 2, topRightRadiusY, width - borderWidth / 2, height - bottomRightRadiusY, paint);
//绘制右下圆角
if (bottomRightRadiusX < borderWidth || bottomRightRadiusY < borderWidth) {
RectF arc3 = new RectF(width - bottomRightRadiusX * 2, height - bottomRightRadiusY * 2, width, height);
paint.setStyle(Paint.Style.FILL);
canvas.drawArc(arc3, 0, 90, true, paint);
} else {
RectF arc3 = new RectF(width - bottomRightRadiusX * 2 + borderWidth / 2, height - bottomRightRadiusY * 2 + borderWidth / 2, width - borderWidth / 2, height - borderWidth / 2);
paint.setStyle(Paint.Style.STROKE);
canvas.drawArc(arc3, 0, 90, false, paint);
}
//绘制底部边框
canvas.drawLine(bottomLeftRadiusX, height - borderWidth / 2, width - bottomRightRadiusX, height - borderWidth / 2, paint);
//绘制左下圆角
if (bottomLeftRadiusX < borderWidth || bottomLeftRadiusY < borderWidth) {
RectF arc4 = new RectF(0, height - bottomLeftRadiusY * 2, bottomLeftRadiusX * 2, height);
paint.setStyle(Paint.Style.FILL);
canvas.drawArc(arc4, 90, 90, true, paint);
} else {
RectF arc4 = new RectF(borderWidth / 2, height - bottomLeftRadiusY * 2 + borderWidth / 2, bottomLeftRadiusX * 2 - borderWidth / 2, height - borderWidth / 2);
paint.setStyle(Paint.Style.STROKE);
canvas.drawArc(arc4, 90, 90, false, paint);
}
//绘制左边边框
canvas.drawLine(borderWidth / 2, topLeftRadiusY, borderWidth / 2, height - bottomLeftRadiusY, paint);
} else {
//如果没有传递圆角的参数,直接绘制即可
canvas.drawRect(rect, paint);
}
} else {
//当边框的宽度不同时,绘制不同的线粗,通过borderWidthLeft,rect.top,rect.bottom来确定每根线所在的位置
if (borderWidthLeft > 0) {
paint.setStrokeWidth(borderWidthLeft);
canvas.drawLine(borderWidthLeft / 2, rect.top, borderWidthLeft / 2, rect.bottom, paint);
}
if (borderWidthTop > 0) {
paint.setStrokeWidth(borderWidthTop);
canvas.drawLine(rect.left, borderWidthTop / 2, rect.right, borderWidthTop / 2, paint);
}
if (borderWidthRight > 0) {
paint.setStrokeWidth(borderWidthRight);
canvas.drawLine(rect.right - borderWidthRight / 2, rect.top, rect.right - borderWidthRight / 2, rect.bottom, paint);
}
if (borderWidthBottom > 0) {
paint.setStrokeWidth(borderWidthBottom);
canvas.drawLine(rect.left, rect.bottom - borderWidthBottom / 2, width, rect.bottom - borderWidthBottom / 2, paint);
}
}
}
}

效果

image.png

相应代码里都有注释,代码本质是通过绘制四根线来实现边框的效果,通过我们传递的两个参数,一个是边框宽度,利用数组,拥有更强的扩展性,可以设置四个方向的线粗.第二个是圆角度数,顺序分别是左上,右上,右下,左下.

当我们的圆角有参数时,线的宽度是有改变的,会稍微短一点,留给矩形控件,防止过度绘制.

Drawable drawable = new GradientDrawable() {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
drawBorder(canvas, borderColor, borderWidths, borderRadius);
}
};

这一部分代码你也可以使用BitmapDrawable,不过编译器会提示过时,问题不大,也能运行.

这种代码我不知道该怎么解释,相应的RectF,canvas的构造方法我都介绍吐了,都是一个样子,只不过计算宽高很复杂而已,总之思路就向上面说的一样.

收起阅读 »

安卓TextView完美展示html格式代码

对于TextView展示html格式代码,最简单的办法就是使用textview.setText(Html.fromHtml(html));,即便其中有img标签,我们依然可以使用ImageGetter,和TagHandler对其中的图片做处理,但用过的都知道,...
继续阅读 »

对于TextView展示html格式代码,最简单的办法就是使用textview.setText(Html.fromHtml(html));,即便其中有img标签,我们依然可以使用ImageGetter,和TagHandler对其中的图片做处理,但用过的都知道,效果不太理想,甚至无法满足产品简单的需求,那么今天博主就来为大家提供一个完美的解决方案!

html代码示例:

这里写图片描述

效果图:

这里写图片描述

首先,要介绍一个开源项目,因为本篇博客所提供的方案是基于这个项目并进行扩展的: github.com/NightWhistl…

该项目对html格式代码(内部标签和样式)基本提供了所有的转化方案,效果还是蛮不错的,但对于图片的处理仅做了展示,而对大小设置,点击事件等并未给出解决方案,所以本篇博客即是来对其进行扩展完善,满足日常开发需求!

首先,看HtmlSpanner的使用方法(注:HtmlSpanner内部代码实现不做详细分析,有兴趣的可下载项目研究):

textView.setText(htmlSpanner.fromHtml(html));

htmlSpanner.fromHtml(html)返回的是Spannable格式数据,使用非常简单,但是仅对html做了展示处理, 如果有这样的需求

  1. 图片需要动态控制大小;
  2. 图片点击后可以查看大图;
  3. 如果有多张图片,点击后进入多图浏览界面,且点进去即是当前图片位置;

这就需要我们能做到以下几点:

  1. 展示图片(设置图片大小)的代码可控;
  2. 可以监听图片点击事件;
  3. 点击图片时可以获取点击的图片url及该图片在全部图片中的position;

那么我们先来看HtmlSpanner对img是如何处理的: 找到项目中类:ImageHanler.java

public class ImageHandler extends TagNodeHandler {

@Override
public void handleTagNode(TagNode node, SpannableStringBuilder builder,
int start, int end, SpanStack stack) {
String src = node.getAttributeByName("src");

builder.append("\uFFFC");

Bitmap bitmap = loadBitmap(src);

if (bitmap != null) {
Drawable drawable = new BitmapDrawable(bitmap);
drawable.setBounds(0, 0, bitmap.getWidth() - 1,
bitmap.getHeight() - 1);

stack.pushSpan( new ImageSpan(drawable), start, builder.length() );
}
}

/**
* Loads a Bitmap from the given url.
*
* @param url
* @return a Bitmap, or null if it could not be loaded.
*/
protected Bitmap loadBitmap(String url) {
try {
return BitmapFactory.decodeStream(new URL(url).openStream());
} catch (IOException io) {
return null;
}
}
}

在handleTagNode方法中我们可以获取到图片的url,并得到了bitmap,有了bitmap那么我们就可以根据bitmap获取图片宽高并动态调整大小了;

drawable.setBounds(0, 0, bitmap.getWidth() - 1,bitmap.getHeight() - 1);

传入计算好的宽高即可;

对于img的点击事件,需要用到TextView的一个方法:setMovementMethod()及一个类:LinkMovementMethod;此时的点击事件不再是view.OnclickListener了,而是通过LinkMovementMethod类中的onTouch事件进行判断的:

  @Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();

if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();

x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();

x += widget.getScrollX();
y += widget.getScrollY();

Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);

ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}

return true;
} else {
Selection.removeSelection(buffer);
}
}

return super.onTouchEvent(widget, buffer, event);
}

我们知道img标签转化后的最终归宿是ImageSpan,因此我们判断buffer.getSpans为ImageSpan时即点击了图片,捕获了点击不算完事,我们需要一个点击事件的回调啊,因此我们需要重写LinkMovementMethod来完成回调(回调方法有多种,我这里用了一个handler):

package net.nightwhistler.htmlspanner;



import android.os.Handler;
import android.os.Message;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.TextView;

public class LinkMovementMethodExt extends LinkMovementMethod {
private static LinkMovementMethod sInstance;
private Handler handler = null;
private Class spanClass = null;

public static MovementMethod getInstance(Handler _handler,Class _spanClass) {
if (sInstance == null) {
sInstance = new LinkMovementMethodExt();
((LinkMovementMethodExt)sInstance).handler = _handler;
((LinkMovementMethodExt)sInstance).spanClass = _spanClass;
}

return sInstance;
}

int x1;
int x2;
int y1;
int y2;

@Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();

if (event.getAction() == MotionEvent.ACTION_DOWN){
x1 = (int) event.getX();
y1 = (int) event.getY();
}

if (event.getAction() == MotionEvent.ACTION_UP) {
x2 = (int) event.getX();
y2 = (int) event.getY();

if (Math.abs(x1 - x2) < 10 && Math.abs(y1 - y2) < 10) {

x2 -= widget.getTotalPaddingLeft();
y2 -= widget.getTotalPaddingTop();

x2 += widget.getScrollX();
y2 += widget.getScrollY();

Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y2);
int off = layout.getOffsetForHorizontal(line, x2);

Object[] spans = buffer.getSpans(off, off, spanClass);
if (spans.length != 0) {
if (spans[0] instanceof MyImageSpan){
Selection.setSelection(buffer,
buffer.getSpanStart(spans[0]),
buffer.getSpanEnd(spans[0]));
Message message = handler.obtainMessage();
message.obj = spans[0];
message.what = 2;
message.sendToTarget();
}
return true;
}
}
}

//return false;
return super.onTouchEvent(widget, buffer, event);


}



public boolean canSelectArbitrarily() {
return true;
}

public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode,
KeyEvent event) {
return false;
}
}

注意里面的这部分代码:

if (spans[0] instanceof MyImageSpan)

MyImageSpan是什么鬼?重写的ImageSpan吗?对了就是重写的ImageSpan!为什么要重写呢?我们在通过handler发送ImageSpan并接收到后我们需要通过ImageSpan获取img的url,但此时通过ImageSpan的gerSource()并不能获取到,所以我们就要重写一下ImageSpan,在创建ImageSpan时就把url set进去:

/**
* Created by byl on 2016-12-9.
*/

public class MyImageSpan extends ImageSpan{

public MyImageSpan(Context context, Bitmap b) {
super(context, b);
}

public MyImageSpan(Context context, Bitmap b, int verticalAlignment) {
super(context, b, verticalAlignment);
}

public MyImageSpan(Drawable d) {
super(d);
}

public MyImageSpan(Drawable d, int verticalAlignment) {
super(d, verticalAlignment);
}

public MyImageSpan(Drawable d, String source) {
super(d, source);
}

public MyImageSpan(Drawable d, String source, int verticalAlignment) {
super(d, source, verticalAlignment);
}

public MyImageSpan(Context context, Uri uri) {
super(context, uri);
}

public MyImageSpan(Context context, Uri uri, int verticalAlignment) {
super(context, uri, verticalAlignment);
}

public MyImageSpan(Context context, @DrawableRes int resourceId) {
super(context, resourceId);
}

public MyImageSpan(Context context, @DrawableRes int resourceId, int verticalAlignment) {
super(context, resourceId, verticalAlignment);
}

private String url;

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

同时在ImageHandler类的handleTagNode方法中也要替换ImageSpan:

MyImageSpan span=new MyImageSpan(drawable);
span.setUrl(src);
stack.pushSpan( span, start, builder.length() );

最终的实现流程为:

 new Thread(new Runnable() {
@Override
public void run() {
final Spannable spannable = htmlSpanner.fromHtml(html);
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(spannable);
tv.setMovementMethod(LinkMovementMethodExt.getInstance(handler, ImageSpan.class));
}
});
}
}).start();
   final Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case 1://获取图片路径列表
String url = (String) msg.obj;
Log.e("jj", "url>>" + url);
imglist.add(url);
break;
case 2://图片点击事件
int position=0;
MyImageSpan span = (MyImageSpan) msg.obj;
for (int i = 0; i < imglist.size(); i++) {
if (span.getUrl().equals(imglist.get(i))) {
position = i;
break;
}
}
Log.e("jj","position>>"+position);
Intent intent=new Intent(MainActivity.this,ImgPreviewActivity.class);
Bundle b=new Bundle();
b.putInt("position",position);
b.putStringArrayList("imglist",imglist);
intent.putExtra("b",b);
startActivity(intent);
break;
}
}

;
};

好了,现在就差点击图片浏览大图(包括多图浏览)了,上面的handler中,当msg.what为1时传来的即是图片路径,这个是在哪里发送的呢?当然是解析html获取到img标签时啦!在ImageHanlder里:

public class ImageHandler extends TagNodeHandler {

Context context;
Handler handler;
int screenWidth ;

public ImageHandler() {
}

public ImageHandler(Context context,int screenWidth, Handler handler) {
this.context=context;
this.screenWidth=screenWidth;
this.handler=handler;
}

@Override
public void handleTagNode(TagNode node, SpannableStringBuilder builder,int start, int end, SpanStack stack) {
int height;
String src = node.getAttributeByName("src");
builder.append("\uFFFC");
Bitmap bitmap = loadBitmap(src);
if (bitmap != null) {
Drawable drawable = new BitmapDrawable(bitmap);
if(screenWidth!=0){
Message message = handler.obtainMessage();
message.obj = src;
message.what = 1;
message.sendToTarget();
height=screenWidth*bitmap.getHeight()/bitmap.getWidth();
drawable.setBounds(0, 0, screenWidth,height);
}else{
drawable.setBounds(0, 0, bitmap.getWidth() - 1,bitmap.getHeight() - 1);
}
MyImageSpan span=new MyImageSpan(drawable);
span.setUrl(src);
stack.pushSpan( span, start, builder.length() );
}


}

/**
* Loads a Bitmap from the given url.
*
* @param url
* @return a Bitmap, or null if it could not be loaded.
*/
protected Bitmap loadBitmap(String url) {
try {
return BitmapFactory.decodeStream(new URL(url).openStream());
} catch (IOException io) {
return null;
}
}
}

screenWidth变量 和Handler对象都是这在初始化ImageHanlder时传入的,初始化ImageHanlder的地方在HtmlSpanner类的registerBuiltInHandlers()方法中:

if(context!=null){
registerHandler("img", new ImageHandler(context,screenWidth,handler));
}else{
registerHandler("img", new ImageHandler());
}

因此,在ImageHanlder中获取到img的url时就通过handler将其路径发送到主界面存储起来,点击的时候通过比较url得到该图片的position,并和图片列表imglist传入浏览界面即可!

需要注意的是,如果html代码中有图片则需要网络权限,并且加载时需要在线程中...

demo下载地址:download.csdn.net/detail/baiy…

ps:如觉得使用handler稍显麻烦,则可以在LinkMovementMethodExt中写一个自定义接口作为点击回调:

public interface ClickImgListener {
void clickImg(String url);
}
  Object[] spans = buffer.getSpans(off, off, ImageSpan.class);
if (spans.length != 0) {
if (spans[0] instanceof MyImageSpan) {
Selection.setSelection(buffer,buffer.getSpanStart(spans[0]),buffer.getSpanEnd(spans[0]));
if(clickImgListener!=null)clickImgListener.clickImg(((MyImageSpan)spans[0]).getUrl());
}
return true;
}

在ImageHanler中,声明一个变量private ArrayList imgList;来存放img的url:

1.private ArrayList<String> imgList;

2.this.bitmapList = new ArrayList<>();

3.public ArrayList<String> getImgList() {
return imgList;
}

4.imgList.add(src);

最终实现:

HtmlSpanner htmlSpanner = new HtmlSpanner(context);
new Thread(() -> {
final Spannable spannable = htmlSpanner.fromHtml(html);
runOnUiThread(() -> {
textView.setText(spannable);
textView.setMovementMethod(new LinkMovementMethodExt((url) -> clickImg(url, htmlSpanner.getImageHandler().getImgList())));
});
}).start();

void clickImg(String url, ArrayList<String> imglist) {
//点击事件处理
}

**另外:**如果html中图片过多且过大,很可能在这部分导致内存溢出:

bitmap = BitmapFactory.decodeStream(new URL(src).openStream());

可以使用这种方法来降低内存占用:

BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = 4;
bitmap=BitmapFactory.decodeStream(new URL(src).openStream(), null, bitmapOptions);

当然这会影响图片显示的清晰度,好在有点击查看原图功能,算是一种补偿吧,也可根据具体业务具体对待!

收起阅读 »

Google 宣布废弃 LiveData.observe 方法

本篇文章作为技术动态了解即可,废弃 LiveData.observe() 扩展方法,已经不是什么新的新闻了,在很久以前,Google 废弃掉这个方法的时候,第一时间我在 朋友圈 和 掘金沸点 发过一个...
继续阅读 »

本篇文章作为技术动态了解即可,废弃 LiveData.observe() 扩展方法,已经不是什么新的新闻了,在很久以前,Google 废弃掉这个方法的时候,第一时间我在 朋友圈  掘金沸点 发过一个消息,如下图所示。

通过这篇文章你将学习到以下内容:

  • 为什么增加 LiveData.observe() 扩展方法?
  • 既然增加了,为什么有要废弃 LiveData.observe() 扩展方法?
  • Kotlin 1.4 语法的特性
    • 什么是函数式(SAM)接口?
    • 什么是 SAM 转换?

为什么废弃 LiveData.observe 扩展方法

我们先来看看官方是如何解释,如下图所示:

在 Kotlin 1.4 上本身能够将默认的 observe() 方法转换为 lambda 语法,以前只有在使用 Kotlin 扩展时才可用。因此将 LiveData.observe() 扩展方法废弃掉了。

在 Kotlin 1.4 之前 LiveData.observe() 写法如下所示。

liveData.observe(this, Observer<String> {
// ......
})

但是这种写法有点复杂,因此 Google 在 lifecycle-livedata-ktx 库中添加了扩展方法,使代码更简洁,可读性更强。

liveData.observe(this){
// ......
}

在 Kotlin 1.4 时,增加了一个新的特性 SAM conversions for Kotlin interfaces ,支持将 SAM(单一抽象方法)接口,转换成 lambda 表达式,因此废弃了 LiveData.observe() 扩展方法。所以升级 lifecycle-livedata-ktx 库到最新版本,将会出现如下提示。

迁移也非常简单,升级到 Kotlin 1.4 之后,只需要移除掉下列包的导入即可。

import androidx.lifecycle.observe

为什么增加 LiveData.observe 扩展方法

接下来我们一起来了解一下 LiveData.observe() 扩展方法的由来,源于一位大神在 issuetracker 上提的一个问题, 如下图所示:

大神认为 SAM 转换,可以使代码更简洁,可读性更强,因此期望 Google 能够支持,现阶段 LiveData.observe() 写法相比 java8 是比较复杂的。

// java8
liveData.observe(owner, name -> {
// ......
});

// SAM 转换之前
liveData.observe(this, Observer<String> { name ->
// ......
})

// SAM 转换之后
liveData.observe(this){ name ->
// ......
}

这里需要插入两个 Kotlin 语法的知识点:

  • 什么是函数式(SAM)接口?
  • 什么是 SAM 转换?

什么是函数式(SAM)接口

只有一个抽象方法的接口称为函数式接口或 SAM(单一抽象方法)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。

什么是 SAM 转换

对于函数式接口,可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁,可读性更强,代码如下所示。

fun interface ByteCode {
fun follow(name: String)
}
fun testFollow(bytecode: ByteCode) {
// ......
}

// 传统的使用方法
testFollow(object : ByteCode{
override fun follow(name: String) {
// ......
}
})

// SAM 转换
testFollow{
// ......
}

在 Kotlin 1.4 之前不支持实现 SAM 转换,于是 Google 在 lifecycle-livedata-ktx 库中添加了 LiveData.observe() 扩展方法,达到相同的目的,commit 如下图所示。

在 Kotlin 1.4 之后,Kotlin 开始支持 SAM 转换,所以 Google 废弃 LiveData.observe() 扩展方法, Google 工程师也对此进行了讨论,如下图所示。

大神 Sergey Vasilinets 建议,为了不破坏源代码兼容性,只是在这个版本中弃用。在以后的版本更新中将会更新错误级别为 error,因此在这里建议如果已经升级到了 Kotlin 1.4,将下列包的导入从代码中移除即可。

import androidx.lifecycle.observe

在 Kotlin 1.5.0 中使用 dynamic invocations (invokedynamic) 进行编译, 实现 SAM(单一抽象方法) 转换,这个就不在本文讨论范围内,放在以后进一步分析。 kotlinlang.org/docs/whatsn…


收起阅读 »

Jackson 之 LocalDateTime 序列化与反序列化

前言 在 Java 8 中对 LocalDateTime、LocalDate 的序列化和反序列化有很多种操作 全局 在 ObjectMapper 对象中配置 JavaTimeModule,此为全局配置。 @Bean public ObjectM...
继续阅读 »

前言


在 Java 8 中对 LocalDateTime、LocalDate 的序列化和反序列化有很多种操作


全局


ObjectMapper 对象中配置 JavaTimeModule,此为全局配置。


    @Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();

// other serializer and deSerializer config ...

JavaTimeModule javaTimeModule = new JavaTimeModule();

javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

objectMapper.registerModule(javaTimeModule);
return objectMapper;
}

DateTimeFormatter.ofPattern 可以设置不同的时间日期模板,来实现不同的效果


局部


使用 @JsonFormat 注解


pattern 可以配置不同的时间格式模板


@Data
public static class Article {
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime date;
}

Serializer 和 DeSerializer


Jackson 提供了默认的 LocalDate 和 LocalDateTime 的 Serializer 和 DeSerializer,不过需要引入额外的 maven 依赖


<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.5</version>
</dependency>

@Data
public static class Article {
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDateTime date;
}

与此同时,还可以自定义 Serializer 和 DeSerializer,以满足某些独特场景中的时间日期格式。
比如对任意格式的时间同一反序列化为标准的 LocalDateTime 对象。


public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return this.deserialize(p.getText().trim());
}

private LocalDateTime deserialize(String source) {
if (StringUtils.isBlank(source)) {
return null;
} else if (source.matches("^\\d{4}-\\d{1,2}$")) {
// yyyy-MM
return LocalDateTime.parse(source + "-01T00:00:00.000", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}T{1}\\d{1,2}")) {
// yyyy-MM-ddTHH
return LocalDateTime.parse(source + ":00:00.000", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else {
// yyyy-MM-ddTHH:mm:ss.SSS
return LocalDateTime.parse(source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}

}

作者:菁芜
链接:https://juejin.cn/post/7025160932653268999
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »