注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

这一次,解决Flutter Dialog的各种痛点!

前言 Q:你一生中闻过最臭的东西,是什么? A:我那早已腐烂的梦。 兄弟萌!!!我又来了! 这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包! 将之前的flutter_smart_dialog,在保持api稳定的基础...
继续阅读 »

前言



Q:你一生中闻过最臭的东西,是什么?


A:我那早已腐烂的梦。



兄弟萌!!!我又来了!


这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包!


将之前的flutter_smart_dialog,在保持api稳定的基础上,进行了各种抓头重构,解决了一系列问题


现在,我终于可以说:它现在是一个简洁,强大,侵入性极低的pub包!


关于侵入性问题



  • 之前为了解决返回关闭弹窗,使用了一个很不优雅的解决方法,导致侵入性有点高

  • 这真是让我如坐针毡,如芒刺背,如鲠在喉,这个问题终于搞定了!


同时,我在pub包内部设计了一个弹窗栈,能自动移除栈顶弹窗,也可以定点移除栈内标记的弹窗。


问题


使用系统弹窗存在一系列坑,来和各位探讨探讨




  • 必须传BuildContext



    • 在一些场景必须多做一些传参工作,蛋痛但不难的问题




  • loading弹窗



    • 使用系统弹窗做loading弹窗,肯定遇到过这个坑比问题

      • loading封装在网络库里面:请求网络时加载loading,手贱按了返回按钮,关闭了loading

      • 然后请求结束后发现:特么我的页面怎么被关了!!!



    • 系统弹窗就是一个路由页面,关闭系统就是用pop方法,这很容易误关正常页面

      • 当然肯定有解决办法,路由监听的地方处理,此处就不细表了






  • 某页面弹出了多个系统Dialog,很难定点关闭某个非栈顶弹窗



    • 蛋蛋,这是路由入栈出栈机制导致的,理解的同时也一样要吐槽




  • 系统Dialog,点击事件无法穿透暗色背景



    • 这个坑比问题,我是真没辙




思考


上面列举了一些比较常见的问题,最严重的问题,应该就是loading的问题



  • loading是个超高频使用的弹窗,关闭loading弹窗的方法,同时也能关闭正常使用的页面,本身就是一个隐患

  • 本菜狗不具备大厂大佬们魔改flutter的能力,菜则思变,我只能从其它方向切入,寻求解决方案


系统的Page就是基于Overlay去实现的,咱们也要骚起来,从Overlay入手


这次,我要一次性帮各位解决:toast消息,loading弹窗,以及更强大的自定义dialog!


快速上手


初始化



dependencies:
flutter_smart_dialog: ^3.0.0


初始化方式一:强力推荐😃




  • 配置更加简洁


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: RouteConfig.main,
getPages: RouteConfig.getPages,
// here
navigatorObservers: [FlutterSmartDialog.observer],
// here
builder: FlutterSmartDialog.init(),
);
}
}


初始化方式二:兼容老版本😊




  • 老版本初始化方式仍然有效,区别是:需要在就加载MaterialApp之前,调用下FlutterSmartDialog.monitor()


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// here
FlutterSmartDialog.monitor();
return MaterialApp(
home: SmartDialogPage(),
// here
navigatorObservers: [FlutterSmartDialog.observer],
/// here
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(child: child);
},
);
}
}


大功告成🚀



上面俩种初始化方式,任选一种即可;然后,就可以完整使用本库的所有功能了


我非常推荐第一种初始化的方式,因为足够简洁;简洁明了的东西用起来,会让人心情愉悦🌞


极简使用



  • toast使用💬


SmartDialog.showToast('test toast');

toastDefault



  • loading使用


SmartDialog.showLoading();
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingDefault



  • dialog使用🎨


var custom = Container(
height: 80,
width: 180,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
child: Text('easy custom dialog', style: TextStyle(color: Colors.white)),
);
// here
SmartDialog.show(widget: custom, isLoadingTemp: false);

dialogEasy


OK,上面展示了,只需要极少的代码,就可以调用相应的功能


当然,内部还有不少地方做了特殊优化,接下来,我会详细的向大家描述下


你可能会有的疑问


初始化框架的时候,相比以前,居然让大家多写了一个参数,内心十分愧疚😩


关闭页面本质上是一个比较复杂的情况,涉及到



  • 物理返回按键

  • AppBar的back按钮

  • 手动pop


为了监控这些情况,不得已增加了一个路由监控参数



关于 FlutterSmartDialog.init()



本方法不会占用你的builder参数,init内部回调出来了builder,你可以大胆放心的继续套



  • 例如:继续套Bloc全局实例😄


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: RouteConfig.main,
getPages: RouteConfig.getPages,
navigatorObservers: [FlutterSmartDialog.observer],
builder: FlutterSmartDialog.init(builder: _builder),
);
}
}

Widget _builder(BuildContext context, Widget? child) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: BlocSpanOneCubit()),
],
child: child!,
);
}


实体返回键



对返回按钮的监控,是非常重要的,基本能覆盖大多数情况


initBack



pop路由



虽然对返回按钮的监控能覆盖大多数场景,但是一些手动pop的场景就需要新增参数监控



  • 不加FlutterSmartDialog.observer

    • 如果打开了穿透参数(就可以和弹窗后的页面交互),然后手动关闭页面

    • 就会出现这种很尴尬的情况




initPopOne



  • 加了FlutterSmartDialog.observer,就能比较合理的处理了

    • 当然,这里的过渡动画,也提供了参数控制是否开启❤




initPopTwo



超实用的参数:backDismiss




  • 这个参数是默认设置为true,返回的时候会默认关闭弹窗;如果设置为false,将不会关闭页面

    • 这样就可以十分轻松的做一个紧急弹窗,禁止用户的下一步操作



  • 我们来看一个场景:假定某开源作者决定弃坑软件,不允许用户再使用该软件的弹窗


SmartDialog.show(
// here
backDismiss: false,
clickBgDismissTemp: false,
isLoadingTemp: false,
widget: Container(
height: 480,
width: 500,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.white,
),
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: Wrap(
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 10,
children: [
// title
Text(
'特大公告',
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
// content
Text('鄙人日夜钻研下面秘籍,终于成功钓到富婆'),
Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211102213746.jpeg',
height: 200,
width: 400,
),
Text('鄙人思考了三秒钟,怀着\'沉重\'的心情,决定弃坑本开源软件'),
Text('本人今后的生活是富婆和远方,已无\'精力\' 再维护本开源软件了'),
Text('各位叼毛,有缘江湖再见!'),
// button (only method of close the dialog)
ElevatedButton(
onPressed: () => SmartDialog.dismiss(),
child: Text('再会!'),
)
],
),
),
),
);

hardClose


从上面的效果图可以看出来



  • 点击遮罩,无法关闭弹窗

  • 点击返回按钮无法关闭弹窗

  • 只能点我们自己的按钮,才能关闭弹窗,点击按钮的逻辑可以直接写成关闭app之类


只需要俩个简单的参数设置,就能实现这样一个很棒的应急弹窗



设置全局参数



SmartDialog的全局参数都有着一个比较合理的默认值


为了应付多变的场景,你可以修改符合你自己要求的全局参数



  • 设置符合你的要求的数据,放在app入口就行初始化就行

    • 注:如果没有特殊要求,完全可以不用初始化全局参数




SmartDialog.config
..alignment = Alignment.center
..isPenetrate = false
..clickBgDismiss = true
..maskColor = Colors.black.withOpacity(0.3)
..maskWidget = null
..animationDuration = Duration(milliseconds: 260)
..isUseAnimation = true
..isLoading = true;


  • 代码的注释写的很完善,某个参数不明白的,点进去看看就行了


image-20211102223129866


Toast篇


toast的特殊性


严格来说,toast是一个非常特殊的弹窗,我觉得理应具备下述的特征



toast消息理应一个个展示,后续消息不应该顶掉前面的toast




  • 这是一个坑点,如果框架内部不做处理,很容易出现后面toast会直接顶掉前面toast的情况


toastOne



展示在页面最上层,不应该被一些弹窗之类遮挡




  • 可以发现loading和dialog的遮罩等布局,均未遮挡toast信息


toastTwo



对键盘遮挡情况做处理




  • 键盘这玩意有点坑,会直接遮挡所有布局,只能曲线救国

    • 在这里做了一个特殊处理,当唤起键盘的时候,toast自己会动态的调整自己和屏幕底部的距离

    • 这样就能起到一个,键盘不会遮挡toast的效果




toastSmart


自定义Toast



参数说明



toast暴露的参数其实并不多,只提供了四个参数



  • 例如:toast字体大小,字体颜色,toast的背景色等等之类,我都没提供参数

    • 一是觉得提供了这些参数,会让整个参数输入变的非常多,乱花渐入迷人眼

    • 二是觉得就算我提供了很多参数,也不一定会满足那些奇奇怪怪的审美和需求



  • 基于上述的考虑,我直接提供了底层参数,直接将widget参数提供出来

    • 你可以随心所欲的定制toast了

    • 注意:使用了widget参数,msgalignment参数会失效




image-20211031155838900



调整toast显示的位置



SmartDialog.showToast('the toast at the bottom');
SmartDialog.showToast('the toast at the center', alignment: Alignment.center);
SmartDialog.showToast('the toast at the top', alignment: Alignment.topCenter);

toastLocation



更强大的自定义toast




  • 首先,整一个自定义toast


class CustomToast extends StatelessWidget {
const CustomToast(this.msg, {Key? key}) : super(key: key);

final String msg;

@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.only(bottom: 30),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 7),
decoration: BoxDecoration(
color: _randomColor(),
borderRadius: BorderRadius.circular(100),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
//icon
Container(
margin: EdgeInsets.only(right: 15),
child: Icon(Icons.add_moderator, color: _randomColor()),
),

//msg
Text('$msg', style: TextStyle(color: Colors.white)),
]),
),
);
}

Color _randomColor() {
return Color.fromRGBO(
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
1,
);
}
}


  • 使用


SmartDialog.showToast('', widget: CustomToast('custom toast'));


  • 效果


toastCustom


Loading篇


避坑指南



  • 开启loading后,可以使用以下方式关闭

    • SmartDialog.dismiss():可以关闭loading和dialog

    • status设置为SmartStatus.loading:仅仅关闭loading




// easy close
SmartDialog.dismiss();
// exact close
SmartDialog.dismiss(status: SmartStatus.loading);


  • 一般来说,loading弹窗是封装在网络库里面的,随着请求状态的自动开启和关闭

    • 基于这种场景,我建议:使用dismiss时,加上status参数,将其设置为:SmartStatus.loading



  • 坑比场景

    • 网络请求加载的时候,loading也随之打开,这时很容易误触返回按钮,关闭loading

    • 当网络请求结束时,会自动调用dismiss方法

    • 因为loading已被关闭,假设此时页面又有SmartDialog的弹窗,未设置status的dismiss就会关闭SmartDialog的弹窗

    • 当然,这种情况很容易解决,封装进网络库的loading,使用:SmartDialog.dismiss(status: SmartStatus.loading); 关闭就行了



  • status参数,是为了精确关闭对应类型弹窗而设计的参数,在一些特殊场景能起到巨大的作用

    • 如果大家理解这个参数的含义,那对于何时添加status参数,必能胸有成竹




参数说明


参数在注释里面写的十分详细,就不赘述了,来看看效果


image-20211031215728656



  • maskWidgetTemp:强大的遮罩自定义功能😆,发挥你的脑洞吧。。。


var maskWidget = Container(
width: double.infinity,
height: double.infinity,
child: Opacity(
opacity: 0.6,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101103911.jpeg',
fit: BoxFit.fill,
),
),
);
SmartDialog.showLoading(maskWidgetTemp: maskWidget);

loadingOne



  • maskColorTemp:支持快捷自定义遮罩颜色


SmartDialog.showLoading(maskColorTemp: randomColor().withOpacity(0.3));

/// random color
Color randomColor() => Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

loadingTwo



  • background:支持加载背景自定义


SmartDialog.showLoading(background: randomColor());

/// random color
Color randomColor() => Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

loadingThree



  • isLoadingTemp:动画效果切换


SmartDialog.showLoading(isLoadingTemp: false);

loadingFour



  • isPenetrateTemp:交互事件可以穿透遮罩,这是个十分有用的功能,对于一些特殊的需求场景十分关键


SmartDialog.showLoading(isPenetrateTemp: true);

loadingFive


自定义Loading


使用showLoading可以轻松的自定义出强大的loading弹窗;鄙人脑洞有限,就简单演示下



自定义一个loading布局



class CustomLoading extends StatefulWidget {
const CustomLoading({Key? key, this.type = 0}) : super(key: key);

final int type;

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

class _CustomLoadingState extends State
with TickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});
super.initState();
}

@override
Widget build(BuildContext context) {
return Stack(children: [
// smile
Visibility(visible: widget.type == 0, child: _buildLoadingOne()),

// icon
Visibility(visible: widget.type == 1, child: _buildLoadingTwo()),

// normal
Visibility(visible: widget.type == 2, child: _buildLoadingThree()),
]);
}

Widget _buildLoadingOne() {
return Stack(alignment: Alignment.center, children: [
RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101174606.png',
height: 110,
width: 110,
),
),
Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101181404.png',
height: 60,
width: 60,
),
]);
}

Widget _buildLoadingTwo() {
return Stack(alignment: Alignment.center, children: [
Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101162946.png',
height: 50,
width: 50,
),
RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101173708.png',
height: 80,
width: 80,
),
),
]);
}

Widget _buildLoadingThree() {
return Center(
child: Container(
height: 120,
width: 180,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Column(mainAxisSize: MainAxisSize.min, children: [
RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101163010.png',
height: 50,
width: 50,
),
),
Container(
margin: EdgeInsets.only(top: 20),
child: Text('loading...'),
),
]),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}


来看看效果




  • 效果一


SmartDialog.showLoading(isLoadingTemp: false, widget: CustomLoading());
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingSmile



  • 效果二


SmartDialog.showLoading(
isLoadingTemp: false,
widget: CustomLoading(type: 1),
);
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingIcon



  • 效果三


SmartDialog.showLoading(widget: CustomLoading(type: 2));
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingNormal


Dialog篇


花里胡哨



弹窗从不同位置弹出,动画是有区别的



image-20211031221419600



  • alignmentTemp:该参数设置不同,动画效果会有所区别


var location = ({
double width = double.infinity,
double height = double.infinity,
}) {
return Container(width: width, height: height, color: randomColor());
};

//left
SmartDialog.show(
widget: location(width: 50),
alignmentTemp: Alignment.centerLeft,
);
await Future.delayed(Duration(milliseconds: 500));
//top
SmartDialog.show(
widget: location(height: 50),
alignmentTemp: Alignment.topCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//right
SmartDialog.show(
widget: location(width: 50),
alignmentTemp: Alignment.centerRight,
);
await Future.delayed(Duration(milliseconds: 500));
//bottom
SmartDialog.show(
widget: location(height: 50),
alignmentTemp: Alignment.bottomCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//center
SmartDialog.show(
widget: location(height: 100, width: 100),
alignmentTemp: Alignment.center,
isLoadingTemp: false,
);

dialogLocation



  • isPenetrateTemp:交互事件穿透遮罩


SmartDialog.show(
alignmentTemp: Alignment.centerRight,
isPenetrateTemp: true,
clickBgDismissTemp: false,
widget: Container(
width: 80,
height: double.infinity,
color: randomColor(),
),
);

dialogPenetrate


dialog栈



  • 这是一个强大且实用的功能!

    • 可以很轻松的定点关闭某个弹窗




var stack = ({
double width = double.infinity,
double height = double.infinity,
String? msg,
}) {
return Container(
width: width,
height: height,
color: randomColor(),
alignment: Alignment.center,
child: Text('弹窗$msg', style: TextStyle(color: Colors.white)),
);
};

//left
SmartDialog.show(
tag: 'A',
widget: stack(msg: 'A', width: 60),
alignmentTemp: Alignment.centerLeft,
);
await Future.delayed(Duration(milliseconds: 500));
//top
SmartDialog.show(
tag: 'B',
widget: stack(msg: 'B', height: 60),
alignmentTemp: Alignment.topCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//right
SmartDialog.show(
tag: 'C',
widget: stack(msg: 'C', width: 60),
alignmentTemp: Alignment.centerRight,
);
await Future.delayed(Duration(milliseconds: 500));
//bottom
SmartDialog.show(
tag: 'D',
widget: stack(msg: 'D', height: 60),
alignmentTemp: Alignment.bottomCenter,
);
await Future.delayed(Duration(milliseconds: 500));

//center:the stack handler
SmartDialog.show(
alignmentTemp: Alignment.center,
isLoadingTemp: false,
widget: Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(15)),
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: Wrap(spacing: 20, children: [
ElevatedButton(
child: Text('关闭弹窗A'),
onPressed: () => SmartDialog.dismiss(tag: 'A'),
),
ElevatedButton(
child: Text('关闭弹窗B'),
onPressed: () => SmartDialog.dismiss(tag: 'B'),
),
ElevatedButton(
child: Text('关闭弹窗C'),
onPressed: () => SmartDialog.dismiss(tag: 'C'),
),
ElevatedButton(
child: Text('关闭弹窗D'),
onPressed: () => SmartDialog.dismiss(tag: 'D'),
),
]),
),
);

dialogStack


骚气的小技巧


有一种场景比较蛋筒



  • 我们使用StatefulWidget封装了一个小组件

  • 在某个特殊的情况,我们需要在这个组件外部,去触发这个组件内部的一个方法

  • 对于这种场景,有不少实现方法,但是弄起来可能有点麻烦


这里提供一个简单的小思路,可以非常轻松的触发,组件内部的某个方法



  • 建立一个小组件


class OtherTrick extends StatefulWidget {
const OtherTrick({Key? key, this.onUpdate}) : super(key: key);

final Function(VoidCallback onInvoke)? onUpdate;

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

class _OtherTrickState extends State {
int _count = 0;

@override
void initState() {
// here
widget.onUpdate?.call(() {
_count++;
setState(() {});
});

super.initState();
}

@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 50, vertical: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Text('Counter: $_count ', style: TextStyle(fontSize: 30.0)),
),
);
}
}


  • 展示这个组件,然后外部触发它


VoidCallback? callback;

// display
SmartDialog.show(
alignmentTemp: Alignment.center,
widget: OtherTrick(
onUpdate: (VoidCallback onInvoke) => callback = onInvoke,
),
);

await Future.delayed(Duration(milliseconds: 500));

// handler
SmartDialog.show(
alignmentTemp: Alignment.centerRight,
maskColorTemp: Colors.transparent,
widget: Container(
height: double.infinity,
width: 150,
color: Colors.white,
alignment: Alignment.center,
child: ElevatedButton(
child: Text('add'),
onPressed: () => callback?.call(),
),
),
);


  • 来看下效果


trick


最后



相关地址




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

【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…


收起阅读 »

Swift 协议

协议规定了用来实现某一特定功能所必需的方法和属性。任意能够满足协议要求的类型被称为遵循(conform)这个协议。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。语法协议的语法格式如下:protocol SomeProtocol ...
继续阅读 »

协议规定了用来实现某一特定功能所必需的方法和属性。

任意能够满足协议要求的类型被称为遵循(conform)这个协议。

类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。

语法

协议的语法格式如下:

protocol SomeProtocol {
// 协议内容
}

要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号:分隔,作为类型定义的一部分。遵循多个协议时,各协议之间用逗号,分隔。

struct SomeStructure: FirstProtocol, AnotherProtocol {
// 结构体内容
}

如果类在遵循协议的同时拥有父类,应该将父类名放在协议名之前,以逗号分隔。

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 类的内容
}

对属性的规定

协议用于指定特定的实例属性或类属性,而不用指定是存储型属性或计算型属性。此外还必须指明是只读的还是可读可写的。

协议中的通常用var来声明变量属性,在类型声明后加上{ set get }来表示属性是可读可写的,只读属性则用{ get }来表示。

protocol classa {

var marks: Int { get set }
var result: Bool { get }

func attendance() -> String
func markssecured() -> String

}

protocol classb: classa {

var present: Bool { get set }
var subject: String { get set }
var stname: String { get set }

}

class classc: classb {
var marks = 96
let result = true
var present = false
var subject = "Swift 协议"
var stname = "Protocols"

func attendance() -> String {
return "The \(stname) has secured 99% attendance"
}

func markssecured() -> String {
return "\(stname) has scored \(marks)"
}
}

let studdet = classc()
studdet.stname = "Swift"
studdet.marks = 98
studdet.markssecured()

print(studdet.marks)
print(studdet.result)
print(studdet.present)
print(studdet.subject)
print(studdet.stname)

以上程序执行输出结果为:

98
true
false
Swift 协议
Swift

对 Mutating 方法的规定

有时需要在方法中改变它的实例。

例如,值类型(结构体,枚举)的实例方法中,将mutating关键字作为函数的前缀,写在func之前,表示可以在该方法中修改它所属的实例及其实例属性的值。

protocol daysofaweek {
mutating func show()
}

enum days: daysofaweek {
case sun, mon, tue, wed, thurs, fri, sat
mutating func show() {
switch self {
case .sun:
self = .sun
print("Sunday")
case .mon:
self = .mon
print("Monday")
case .tue:
self = .tue
print("Tuesday")
case .wed:
self = .wed
print("Wednesday")
case .thurs:
self = .thurs
print("Wednesday")
case .fri:
self = .fri
print("Firday")
case .sat:
self = .sat
print("Saturday")
default:
print("NO Such Day")
}
}
}

var res = days.wed
res.show()

以上程序执行输出结果为:

Wednesday

对构造器的规定

协议可以要求它的遵循者实现指定的构造器。

你可以像书写普通的构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体,语法如下:

protocol SomeProtocol {
init(someParameter: Int)
}

实例

protocol tcpprotocol {
init(aprot: Int)
}

协议构造器规定在类中的实现

你可以在遵循该协议的类中实现构造器,并指定其为类的指定构造器或者便利构造器。在这两种情况下,你都必须给构造器实现标上"required"修饰符:

class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 构造器实现
}
}

protocol tcpprotocol {
init(aprot: Int)
}

class tcpClass: tcpprotocol {
required init(aprot: Int) {
}
}

使用required修饰符可以保证:所有的遵循该协议的子类,同样能为构造器规定提供一个显式的实现或继承实现。

如果一个子类重写了父类的指定构造器,并且该构造器遵循了某个协议的规定,那么该构造器的实现需要被同时标示required和override修饰符:

protocol tcpprotocol {
init(no1: Int)
}

class mainClass {
var no1: Int // 局部变量
init(no1: Int) {
self.no1 = no1 // 初始化
}
}

class subClass: mainClass, tcpprotocol {
var no2: Int
init(no1: Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
required override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}
let res = mainClass(no1: 20)
let show = subClass(no1: 30, no2: 50)

print("res is: \(res.no1)")
print("res is: \(show.no1)")
print("res is: \(show.no2)")

以上程序执行输出结果为:

res is: 20
res is: 30
res is: 50

协议类型

尽管协议本身并不实现任何功能,但是协议可以被当做类型来使用。

协议可以像其他普通类型一样使用,使用场景:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

实例

protocol Generator {
associatedtype members
func next() -> members?
}

var items = [10,20,30].makeIterator()
while let x = items.next() {
print(x)
}

for lists in [1,2,3].map( {i in i*5}) {
print(lists)
}

print([100,200,300])
print([1,2,3].map({i in i*10}))

以上程序执行输出结果为:

10
20
30
5
10
15
[100, 200, 300]
[10, 20, 30]

在扩展中添加协议成员

我们可以可以通过扩展来扩充已存在类型( 类,结构体,枚举等)。

扩展可以为已存在的类型添加属性,方法,下标脚本,协议等成员。

protocol AgeClasificationProtocol {
var age: Int { get }
func agetype() -> String
}

class Person {
let firstname: String
let lastname: String
var age: Int
init(firstname: String, lastname: String) {
self.firstname = firstname
self.lastname = lastname
self.age = 10
}
}

extension Person : AgeClasificationProtocol {
func fullname() -> String {
var c: String
c = firstname + " " + lastname
return c
}

func agetype() -> String {
switch age {
case 0...2:
return "Baby"
case 2...12:
return "Child"
case 13...19:
return "Teenager"
case let x where x > 65:
return "Elderly"
default:
return "Normal"
}
}
}

协议的继承

协议能够继承一个或多个其他协议,可以在继承的协议基础上增加新的内容要求。

协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 协议定义
}

实例

protocol Classa {
var no1: Int { get set }
func calc(sum: Int)
}

protocol Result {
func print(target: Classa)
}

class Student2: Result {
func print(target: Classa) {
target.calc(1)
}
}

class Classb: Result {
func print(target: Classa) {
target.calc(5)
}
}

class Student: Classa {
var no1: Int = 10

func calc(sum: Int) {
no1 -= sum
print("学生尝试 \(sum) 次通过")

if no1 <= 0 {
print("学生缺席考试")
}
}
}

class Player {
var stmark: Result!

init(stmark: Result) {
self.stmark = stmark
}

func print(target: Classa) {
stmark.print(target)
}
}

var marks = Player(stmark: Student2())
var marksec = Student()

marks.print(marksec)
marks.print(marksec)
marks.print(marksec)
marks.stmark = Classb()
marks.print(marksec)
marks.print(marksec)
marks.print(marksec)

以上程序执行输出结果为:

学生尝试 1 次通过
学生尝试 1 次通过
学生尝试 1 次通过
学生尝试 5 次通过
学生尝试 5 次通过
学生缺席考试
学生尝试 5 次通过
学生缺席考试

类专属协议

你可以在协议的继承列表中,通过添加class关键字,限制协议只能适配到类(class)类型。

该class关键字必须是第一个出现在协议的继承列表中,其后,才是其他继承协议。格式如下:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
// 协议定义
}

实例

protocol TcpProtocol {
init(no1: Int)
}

class MainClass {
var no1: Int // 局部变量
init(no1: Int) {
self.no1 = no1 // 初始化
}
}

class SubClass: MainClass, TcpProtocol {
var no2: Int
init(no1: Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
required override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}

let res = MainClass(no1: 20)
let show = SubClass(no1: 30, no2: 50)

print("res is: \(res.no1)")
print("res is: \(show.no1)")
print("res is: \(show.no2)")

以上程序执行输出结果为:

res is: 20
res is: 30
res is: 50

协议合成

Swift 支持合成多个协议,这在我们需要同时遵循多个协议时非常有用。

语法格式如下:

protocol Stname {
var name: String { get }
}

protocol Stage {
var age: Int { get }
}

struct Person: Stname, Stage {
var name: String
var age: Int
}

func show(celebrator: Stname & Stage) {
print("\(celebrator.name) is \(celebrator.age) years old")
}

let studname = Person(name: "Priya", age: 21)
print(studname)

let stud = Person(name: "Rehan", age: 29)
print(stud)

let student = Person(name: "Roshan", age: 19)
print(student)

以上程序执行输出结果为:

Person(name: "Priya", age: 21)
Person(name: "Rehan", age: 29)
Person(name: "Roshan", age: 19)

检验协议的一致性

你可以使用is和as操作符来检查是否遵循某一协议或强制转化为某一类型。

  • is操作符用来检查实例是否遵循了某个协议
  • as?返回一个可选值,当实例遵循协议时,返回该协议类型;否则返回nil
  • as用以强制向下转型,如果强转失败,会引起运行时错误。

实例

下面的例子定义了一个 HasArea 的协议,要求有一个Double类型可读的 area:

protocol HasArea {
var area: Double { get }
}

// 定义了Circle类,都遵循了HasArea协议
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}

// 定义了Country类,都遵循了HasArea协议
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}

// Animal是一个没有实现HasArea协议的类
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}

let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]

for object in objects {
// 对迭代出的每一个元素进行检查,看它是否遵循了HasArea协议
if let objectWithArea = object as? HasArea {
print("面积为 \(objectWithArea.area)")
} else {
print("没有面积")
}
}

以上程序执行输出结果为:

面积为 12.5663708
面积为 243610.0
没有面积
收起阅读 »

Swift 扩展

扩展就是向一个已有的类、结构体或枚举类型添加新功能。扩展可以对一个类型添加新的功能,但是不能重写已有的功能。Swift 中的扩展可以:添加计算型属性和计算型静态属性定义实例方法和类型方法提供新的构造器定义下标定义和使用新的嵌套类型使一个已有类型符合某个协议语法...
继续阅读 »

扩展就是向一个已有的类、结构体或枚举类型添加新功能。

扩展可以对一个类型添加新的功能,但是不能重写已有的功能。

Swift 中的扩展可以:

  • 添加计算型属性和计算型静态属性
  • 定义实例方法和类型方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使一个已有类型符合某个协议

语法

扩展声明使用关键字 extension

extension SomeType {
// 加到SomeType的新功能写到这里
}

一个扩展可以扩展一个已有类型,使其能够适配一个或多个协议,语法格式如下:

extension SomeType: SomeProtocol, AnotherProctocol {
// 协议实现写到这里
}

计算型属性

扩展可以向已有类型添加计算型实例属性和计算型类型属性。

实例

下面的例子向 Int 类型添加了 5 个计算型实例属性并扩展其功能:

extension Int {
var add: Int {return self + 100 }
var sub: Int { return self - 10 }
var mul: Int { return self * 10 }
var div: Int { return self / 5 }
}

let addition = 3.add
print("加法运算后的值:\(addition)")

let subtraction = 120.sub
print("减法运算后的值:\(subtraction)")

let multiplication = 39.mul
print("乘法运算后的值:\(multiplication)")

let division = 55.div
print("除法运算后的值: \(division)")

let mix = 30.add + 34.sub
print("混合运算结果:\(mix)")

以上程序执行输出结果为:

加法运算后的值:103
减法运算后的值:110
乘法运算后的值:390
除法运算后的值: 11
混合运算结果:154

构造器

扩展可以向已有类型添加新的构造器。

这可以让你扩展其它类型,将你自己的定制类型作为构造器参数,或者提供该类型的原始实现中没有包含的额外初始化选项。

扩展可以向类中添加新的便利构造器 init(),但是它们不能向类中添加新的指定构造器或析构函数 deinit() 。

struct sum {
var num1 = 100, num2 = 200
}

struct diff {
var no1 = 200, no2 = 100
}

struct mult {
var a = sum()
var b = diff()
}


extension mult
{
init
(x: sum, y: diff) {
_
= x.num1 + x.num2
_
= y.no1 + y.no2
}
}


let a = sum(num1: 100, num2: 200)
let b = diff(no1: 200, no2: 100)

let getMult = mult(x: a, y: b)
print("getMult sum\(getMult.a.num1, getMult.a.num2)")
print("getMult diff\(getMult.b.no1, getMult.b.no2)")

以上程序执行输出结果为:

getMult sum(100, 200)
getMult diff
(200, 100)

方法

扩展可以向已有类型添加新的实例方法和类型方法。

下面的例子向Int类型添加一个名为 topics 的新实例方法:

extension Int {
func topics
(summation: () -> ()) {
for _ in 0..<self {
summation
()
}
}
}

4.topics({
print("扩展模块内")
})

3.topics({
print("内型转换模块内")
})

以上程序执行输出结果为:

扩展模块内
扩展模块内
扩展模块内
扩展模块内
内型转换模块内
内型转换模块内
内型转换模块内

这个topics方法使用了一个() -> ()类型的单参数,表明函数没有参数而且没有返回值。

定义该扩展之后,你就可以对任意整数调用 topics 方法,实现的功能则是多次执行某任务:


可变实例方法

通过扩展添加的实例方法也可以修改该实例本身。

结构体和枚举类型中修改self或其属性的方法必须将该实例方法标注为mutating,正如来自原始实现的修改方法一样。

实例

下面的例子向 Swift 的 Double 类型添加了一个新的名为 square 的修改方法,来实现一个原始值的平方计算:

extension Double {
mutating func square
() {
let pi = 3.1415
self = pi * self * self
}
}

var Trial1 = 3.3
Trial1.square()
print("圆的面积为: \(Trial1)")


var Trial2 = 5.8
Trial2.square()
print("圆的面积为: \(Trial2)")


var Trial3 = 120.3
Trial3.square()
print("圆的面积为: \(Trial3)")

以上程序执行输出结果为:

圆的面积为: 34.210935
圆的面积为: 105.68006
圆的面积为: 45464.070735

下标

扩展可以向一个已有类型添加新下标。

实例

以下例子向 Swift 内建类型Int添加了一个整型下标。该下标[n]返回十进制数字

extension Int {
subscript
(var multtable: Int) -> Int {
var no1 = 1
while multtable > 0 {
no1
*= 10
--multtable
}
return (self / no1) % 10
}
}

print(12[0])
print(7869[1])
print(786543[2])

以上程序执行输出结果为:

2
6
5

嵌套类型

扩展可以向已有的类、结构体和枚举添加新的嵌套类型:

extension Int {
enum calc
{
case add
case sub
case mult
case div
case anything
}

var print: calc {
switch self
{
case 0:
return .add
case 1:
return .sub
case 2:
return .mult
case 3:
return .div
default:
return .anything
}
}
}

func result
(numb: [Int]) {
for i in numb {
switch i.print {
case .add:
print(" 10 ")
case .sub:
print(" 20 ")
case .mult:
print(" 30 ")
case .div:
print(" 40 ")
default:
print(" 50 ")

}
}
}

result
([0, 1, 2, 3, 4, 7])

以上程序执行输出结果为:

 10 
20
30
40
50
50

1 篇笔记 写笔记

  1.    沉迷打码小凳子

      100***8089@qq.com

       参考地址

    1

    扩展下标文中的代码对于较高版本的swift可能会报错:

    'var' in this position is interpreted as an argument label
    Left side of mutating operator isn't mutable: 'multtable' is immutable

    验证了写法,这样写可以避免问题:

    extension Int{
    subscript
    (digitIndex:Int)->Int{
    var decimalBase = 1
    var digit = digitIndex
    // 不能直接使用digitIndex,会报错
    while digit > 0 {
    decimalBase
    *= 10
    digit
    = digit - 1
    }
    return (self/decimalBase) % 10
    }
    }

    print(12[0])
    print(7869[1])
    print(786543[2])

    参考了网上的写法,还可以这样写:

    extension Int{
    subscript
    (digitIndex:Int)->Int{

    var decimalBase = 1
    for _ in 0 ..< digitIndex{
    decimalBase
    *= 10
    }
    return (self/decimalBase) % 10
    }
    }
    print(12[0])
    print(7869[1])
    print(786543[2])
收起阅读 »

Swift 类型转换

Swift 语言类型转换可以判断实例的类型。也可以用于检测实例类型是否属于其父类或者子类的实例。Swift 中类型转换使用 is 和 as 操作符实现,is 用于检测值的类型,as 用于转换类型。类型转换也可以用来检查一个类是否实现了某个协议。定义一个类层次以...
继续阅读 »

Swift 语言类型转换可以判断实例的类型。也可以用于检测实例类型是否属于其父类或者子类的实例。

Swift 中类型转换使用 is 和 as 操作符实现,is 用于检测值的类型,as 用于转换类型。

类型转换也可以用来检查一个类是否实现了某个协议。


定义一个类层次

以下定义了三个类:Subjects、Chemistry、Maths,Chemistry 和 Maths 继承了 Subjects。

代码如下:

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫

检查类型

类型转换用于检测实例类型是否属于特定的实例类型。

你可以将它用在类和子类的层次结构上,检查特定类实例的类型并且转换这个类实例的类型成为这个层次结构中的其他类型。

类型检查使用 is 关键字。

操作符 is 来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true,否则返回 false。

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。
if item is Chemistry {
++chemCount
} else if item is Maths {
++mathsCount
}
}

print("化学科目包含 \(chemCount) 个主题,数学包含 \(mathsCount) 个主题")

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学科目包含 2 个主题,数学包含 3 个主题

向下转型

向下转型,用类型转换操作符(as? 或 as!)

当你不确定向下转型可以成功时,用类型转换的条件形式(as?)。条件形式的类型转换总是返回一个可选值(optional value),并且若下转是不可能的,可选值将是 nil。

只有你可以确定向下转型一定会成功时,才使用强制形式(as!)。当你试图向下转型为一个不正确的类型时,强制形式的类型转换会触发一个运行时错误。

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0

for item in sa {
// 类型转换的条件形式
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
}

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学主题是: '固体物理', 赫兹
数学主题是: '流体动力学', 千兆赫
化学主题是: '热物理学', 分贝
数学主题是: '天体物理学', 兆赫
数学主题是: '微分方程', 余弦级数

Any和AnyObject的类型转换

Swift为不确定类型提供了两种特殊类型别名:

  • AnyObject可以代表任何class类型的实例。
  • Any可以表示任何类型,包括方法类型(function types)。

注意:
只有当你明确的需要它的行为和功能时才使用AnyAnyObject。在你的代码里使用你期望的明确的类型总是更好的。

Any 实例

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0

for item in sa {
// 类型转换的条件形式
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
}

// 可以存储Any类型的数组 exampleany
var exampleany = [Any]()

exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学主题是: '固体物理', 赫兹
数学主题是: '流体动力学', 千兆赫
化学主题是: '热物理学', 分贝
数学主题是: '天体物理学', 兆赫
数学主题是: '微分方程', 余弦级数
整型值为 12
Pi 值为 3.14159
Any 实例
主题 '固体物理', 兆赫

AnyObject 实例

class Subjects {
var physics: String
init(physics: String) {
self.physics = physics
}
}

class Chemistry: Subjects {
var equations: String
init(physics: String, equations: String) {
self.equations = equations
super.init(physics: physics)
}
}

class Maths: Subjects {
var formulae: String
init(physics: String, formulae: String) {
self.formulae = formulae
super.init(physics: physics)
}
}

// [AnyObject] 类型的数组
let saprint: [AnyObject] = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]


let samplechem = Chemistry(physics: "固体物理", equations: "赫兹")
print("实例物理学是: \(samplechem.physics)")
print("实例方程式: \(samplechem.equations)")


let samplemaths = Maths(physics: "流体动力学", formulae: "千兆赫")
print("实例物理学是: \(samplemaths.physics)")
print("实例公式是: \(samplemaths.formulae)")

var chemCount = 0
var mathsCount = 0

for item in saprint {
// 类型转换的条件形式
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
}

var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

以上程序执行输出结果为:

实例物理学是: 固体物理
实例方程式: 赫兹
实例物理学是: 流体动力学
实例公式是: 千兆赫
化学主题是: '固体物理', 赫兹
数学主题是: '流体动力学', 千兆赫
化学主题是: '热物理学', 分贝
数学主题是: '天体物理学', 兆赫
数学主题是: '微分方程', 余弦级数
整型值为 12
Pi 值为 3.14159
Any 实例
主题 '固体物理', 兆赫

在一个switch语句的case中使用强制形式的类型转换操作符(as, 而不是 as?)来检查和转换到一个明确的类型。

收起阅读 »

Swift 自动引用计数(ARC)

Swift 使用自动引用计数(ARC)这一机制来跟踪和管理应用程序的内存通常情况下我们不需要去手动释放内存,因为 ARC 会在类的实例不再被使用时,自动释放其占用的内存。但在有些时候我们还是需要在代码中实现内存管理。ARC 功能当每次使用 init() 方法创...
继续阅读 »

Swift 使用自动引用计数(ARC)这一机制来跟踪和管理应用程序的内存

通常情况下我们不需要去手动释放内存,因为 ARC 会在类的实例不再被使用时,自动释放其占用的内存。

但在有些时候我们还是需要在代码中实现内存管理。

ARC 功能

  • 当每次使用 init() 方法创建一个类的新的实例的时候,ARC 会分配一大块内存用来储存实例的信息。

  • 内存中会包含实例的类型信息,以及这个实例所有相关属性的值。

  • 当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。

  • 为了确保使用中的实例不会被销毁,ARC 会跟踪和计算每一个实例正在被多少属性,常量和变量所引用。

  • 实例赋值给属性、常量或变量,它们都会创建此实例的强引用,只要强引用还在,实例是不允许被销毁的。

ARC 实例

class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 开始初始化")
}
deinit {
print("\(name) 被析构")
}
}

// 值会被自动初始化为nil,目前还不会引用到Person类的实例
var reference1: Person?
var reference2: Person?
var reference3: Person?

// 创建Person类的新实例
reference1 = Person(name: "Runoob")


//赋值给其他两个变量,该实例又会多出两个强引用
reference2 = reference1
reference3 = reference1

//断开第一个强引用
reference1 = nil
//断开第二个强引用
reference2 = nil
//断开第三个强引用,并调用析构函数
reference3 = nil

以上程序执行输出结果为:

Runoob 开始初始化
Runoob 被析构

类实例之间的循环强引用

在上面的例子中,ARC 会跟踪你所新创建的 Person 实例的引用数量,并且会在 Person 实例不再被需要时销毁它。

然而,我们可能会写出这样的代码,一个类永远不会有0个强引用。这种情况发生在两个类实例互相保持对方的强引用,并让对方不被销毁。这就是所谓的循环强引用。

实例

下面展示了一个不经意产生循环强引用的例子。例子定义了两个类:Person和Apartment,用来建模公寓和它其中的居民:

class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) 被析构") }
}

class Apartment {
let number: Int
init(number: Int) { self.number = number }
var tenant: Person?
deinit { print("Apartment #\(number) 被析构") }
}

// 两个变量都被初始化为nil
var runoob: Person?
var number73: Apartment?

// 赋值
runoob = Person(name: "Runoob")
number73 = Apartment(number: 73)

// 意感叹号是用来展开和访问可选变量 runoob 和 number73 中的实例
// 循环强引用被创建
runoob!.apartment = number73
number73!.tenant = runoob

// 断开 runoob 和 number73 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁
// 注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。
// 强引用循环阻止了Person和Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏
runoob = nil
number73 = nil

解决实例之间的循环强引用

Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:

  • 弱引用
  • 无主引用

弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。

对于生命周期中会变为nil的实例使用弱引用。相反的,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

弱引用实例

class Module {
let name: String
init(name: String) { self.name = name }
var sub: SubModule?
deinit { print("\(name) 主模块") }
}

class SubModule {
let number: Int

init(number: Int) { self.number = number }

weak var topic: Module?

deinit { print("子模块 topic 数为 \(number)") }
}

var toc: Module?
var list: SubModule?
toc = Module(name: "ARC")
list = SubModule(number: 4)
toc!.sub = list
list!.topic = toc

toc = nil
list = nil

以上程序执行输出结果为:

ARC 主模块
子模块 topic 数为 4

无主引用实例

class Student {
let name: String
var section: Marks?

init(name: String) {
self.name = name
}

deinit { print("\(name)") }
}
class Marks {
let marks: Int
unowned let stname: Student

init(marks: Int, stname: Student) {
self.marks = marks
self.stname = stname
}

deinit { print("学生的分数为 \(marks)") }
}

var module: Student?
module = Student(name: "ARC")
module!.section = Marks(marks: 98, stname: module!)
module = nil

以上程序执行输出结果为:

ARC
学生的分数为 98

闭包引起的循环强引用

循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包 "捕获" self,从而产生了循环强引用。

实例

下面的例子为你展示了当一个闭包引用了self后是如何产生一个循环强引用的。例子中定义了一个叫HTMLElement的类,用一种简单的模型表示 HTML 中的一个单独的元素:

class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name: String, text: String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized")
}

}

// 创建实例并打印信息
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

HTMLElement 类产生了类实例和 asHTML 默认值的闭包之间的循环强引用。

实例的 asHTML 属性持有闭包的强引用。但是,闭包在其闭包体内使用了self(引用了self.name和self.text),因此闭包捕获了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。

解决闭包引起的循环强引用:在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。


弱引用和无主引用

当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用。

相反的,当捕获引用有时可能会是nil时,将闭包内的捕获定义为弱引用。

如果捕获的引用绝对不会置为nil,应该用无主引用,而不是弱引用。

实例

前面的HTMLElement例子中,无主引用是正确的解决循环强引用的方法。这样编写HTMLElement类来避免循环强引用:

class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name: String, text: String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) 被析构")
}

}

//创建并打印HTMLElement实例
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

// HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息
paragraph = nil

以上程序执行输出结果为:

<p>hello, world</p>
p 被析构
收起阅读 »

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

高并发场景下JVM调优实践之路

一、背景 2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。 通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图: 可以看出,在观察周期里: 平均每10分钟Young&n...
继续阅读 »

一、背景


2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。


通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图:




可以看出,在观察周期里:




  • 平均每10分钟Young GC次数66次,峰值为470次;




  • 平均每10分钟Full GC次数0.25次,峰值5次;




可见Full GC非常频繁,Young GC在特定的时段也比较频繁,存在较大的优化空间。由于对GC停顿的优化是降低接口的P99时延一个有效的手段,所以决定对该核心服务进行JVM调优。


二、优化目标




  • 接口P99时延降低30%




  • 减少Young GC和Full GC次数、停顿时长、单次停顿时长




由于GC的行为与并发有关,例如当并发比较高时,不管如何调优,Young GC总会很频繁,总会有不该晋升的对象晋升触发Full GC,因此优化的目标根据负载分别制定:


目标1:高负载(单机1000 QPS以上)




  • Young GC次数减少20%-30% ,Young GC累积耗时不恶化;




  • Full GC次数减少50%以上,单次、累积Full GC耗时减少50%以上,服务发布不触发Full GC。




目标2:中负载(单机500-600)




  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;




  • Full GC次数不高于4次/天,服务发布不触发Full GC。




目标3:低负载(单机200 QPS以下)




  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;




  • Full GC次数不高于1次/天,服务发布不触发Full GC。




三、当前存在的问题


当前服务的JVM配置参数如下:


-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

单纯从参数上分析,存在以下问题:


**未显示指定收集器 **


JDK 8默认搜集器为ParrallelGC,即Young区采用Parallel Scavenge,老年代采用Parallel Old进行收集,这套配置的特点是吞吐量优先,一般适用于后台任务型服务器。


比如批量订单处理、科学计算等对吞吐量敏感,对时延不敏感的场景,当前服务是视频与用户交互的门户,对时延非常敏感,因此不适合使用默认收集器ParrallelGC,应选择更合适的收集器。



Young区配比不合理


当前服务主要提供API,这类服务的特点是常驻对象会比较少,绝大多数对象的生命周期都比较短,经过一次或两次Young GC就会消亡。


再看下当前JVM配置


整个堆为4G,Young区总共1G,默认-XX:SurvivorRatio=8,即有效大小为0.9G,老年代常驻对象大小约400M。


这就意味着,当服务负载较高,请求并发较大时,Young区中Eden + S0区域会迅速填满,进而Young GC会比较频繁。


另外会引起本应被Young GC回收的对象过早晋升,增加Full GC的频率,同时单次收集的区域也会增大,由于Old区使用的是ParralellOld,无法与用户线程并发执行,导致服务长时间停顿,可用性下降, P99响应时间上升。


未设置


-XX:MetaspaceSize和-XX:MaxMetaspaceSize


Perm区在jdk 1.8已经过时,被Meta区取代,
因此-XX:PermSize=512M -XX:MaxPermSize=512M配置会被忽略,
真正控制Meta区GC的参数为
-XX:MetaspaceSize:
Metaspace初始大小,64位机器默认为21M左右

-XX:MaxMetaspaceSize:
Metaspace的最大值,64位机器默认为18446744073709551615Byte,
可以理解为无上限

-XX:MaxMetaspaceExpansion:
增大触发metaspace GC阈值的最大要求

-XX:MinMetaspaceExpansion:
增大触发metaspace GC阈值的最小要求,默认为340784Byte

这样服务在启动和发布的过程中,元数据区域达到21M时会触发一次Full GC (Metadata GC Threshold),随后随着元数据区域的扩张,会夹杂若干次Full GC (Metadata GC Threshold),使服务发布稳定性和效率下降。


此外如果服务使用了大量动态类生成技术的话,也会因为这个机制产生不必要的Full GC (Metadata GC Threshold)。




四、优化方案/验证方案


上面已分析出当前配置存在的较为明显的不足,下面优化方案主要先针对性解决这些问题,之后再结合效果决定是否继续深入优化。


当前主流/优秀的搜集器包含:




  • Parrallel Scavenge + Parrallel Old:吞吐量优先,后台任务型服务适合;




  • ParNew + CMS:经典的低停顿搜集器,绝大多数商用、延时敏感的服务在使用;




  • G1:JDK 9默认搜集器,堆内存比较大(6G-8G以上)的时候表现出比较高吞吐量和短暂的停顿时间;




  • ZGC:JDK 11中推出的一款低延迟垃圾回收器,目前处在实验阶段;





结合当前服务的实际情况(堆大小,可维护性),我们选择ParNew + CMS方案是比较合适的。


参数选择的原则如下:


1)Meta区域的大小一定要指定,且MetaspaceSize和MaxMetaspaceSize大小应设置一致,具体多大要结合线上实例的情况,通过jstat -gc可以获取该服务线上实例的情况。


# jstat -gc 31247
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
37888.0 37888.0 0.0 32438.5 972800.0 403063.5 3145728.0 2700882.3 167320.0 152285.0 18856.0 16442.4 15189 597.209 65 70.447 667.655

可以看出MU在150M左右,因此-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M是比较合理的。


2)Young区也不是越大越好


当堆大小一定时,Young区越大,Young GC的频率一定越小,但Old区域就会变小,如果太小,稍微晋升一些对象就会触发Full GC得不偿失。


如果Young区过小,Young GC就会比较频繁,这样Old区就会比较大,单次Full GC的停顿就会比较大。因此Young区的大小需要结合服务情况,分几种场景进行比较,最终获得最合适的配置。


基于以上原则,以下为4种参数组合:


1.ParNew +CMS,Young区扩大1倍


-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

**2.ParNew +CMS,**Young区扩大1倍,


去除-XX:+CMSScavengeBeforeRemark(使用【-XX:CMSScavengeBeforeRemark】参数可以做到在重新标记前先执行一次新生代GC)。


因为老年代和年轻代之间的对象存在跨代引用,因此老年代进行GC Roots追踪时,同样也会扫描年轻代,而如果能够在重新标记前先执行一次新生代GC,那么就可以少扫描一些对象,重新标记阶段的性能也能因此提升。)


-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

3.ParNew +CMS,Young区扩大0.5倍


-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

4.ParNew +CMS,Young区不变


-Xms4096M -Xmx4096M -Xmn1024M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

下面,我们需要在压测环境,对不同负载下4种方案的实际表现进行比较,分析,验证。


4.1 压测环境验证/分析


高负载场景(1100 QPS)GC表现



可以看出,在高负载场景,4种ParNew + CMS的各项指标表现均远好于Parrallel Scavenge + Parrallel Old。其中:




  • 方案4(Young区扩大0.5倍)表现最佳,接口P95,P99延时相对当前方案降低50%,Full GC累积耗时减少88%, Young GC次数减少23%,Young GC累积耗时减少4%,Young区调大后,虽然次数减少了,但Young区大了,单次Young GC的耗时也大概率会上升,这是符合预期的。




  • Young区扩大1倍的两种方案,即方案2和方案3,表现接近,接口P95,P99延时相对当前方案降低40%,Full GC累积耗时减少81%, Young GC次数减少43%,Young GC累积耗时减少17%,略逊于Young区扩大0.5倍,总体表现不错,这两个方案进行合并,不再区分。




Young区不变的方案在新方案里,表现最差,淘汰。所以在中负载场景,我们只需要对比方案2和方案4。


中负载场景(600 QPS)GC表现



可以看出,在中负载场景,2种ParNew + CMS(方案2和方案4)的各项指标表现也均远好于Parrallel Scavenge + Parrallel Old。




  • Young区扩大1倍的方案表现最佳,接口P95,P99延时相对当前方案降低32%,Full GC累积耗时减少93%, Young GC次数减少42%,Young GC累积耗时减少44%;




  • Young区扩大0.5倍的方案稍逊一些。




综合来看,两个方案表现十分接近,原则上两种方案都可以,只是Young区扩大0.5倍的方案在业务高峰期的表现更佳,为尽量保证高峰期服务的稳定和性能,目前更倾向于选择ParNew + CMS,Young区扩大0.5倍方案。


4.2 灰度方案/分析


为保证覆盖业务的高峰期,选择周五、周六、周日分别从两个机房随机选择一台线上实例,线上实例的指标符合预期后,再进行全量升级。


目标组  xx.xxx.60.6


采用方案2,即目标方案


-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

对照组1  xx.xxx.15.215


采用原始方案


-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

对照组2  xx.xxx.40.87


采用方案4,即候选目标方案


-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

灰度3台机器。


我们先分析下Young GC相关指标:


Young GC次数



Young GC累计耗时



Young GC单次耗时



可以看出,与原始方案相比,目标方案的YGC次数减少50%,累积耗时减少47%,吞吐量提升的同时,服务停顿的频率大大降低,而代价是单次Young GC的耗时增长3ms,收益是非常高的。


对照方案2即Young区2G的方案整体表现稍逊与目标方案,再分析Full GC指标。


老年代内存增长情况



Full GC次数



Full GC累计/单次耗时



与原始方案相比,使用目标方案时,老年代增长的速度要缓慢很多,基本在观测周期内Full GC发生的次数从155次减少至27次,减少82%,停顿时间均值从399ms减少至60ms,减少85%,毛刺也非常少。


对照方案2即Young区2G的方案整体表现逊于目标方案。到这里,可以看出,目标方案从各个维度均远优于原始方案,调优目标也基本达成。


但细心的同学会发现,目标方案相对原始方案,"Full GC"(实际上是CMS Background GC)耗时更加平稳,但每个若干次"Full GC"后会有一个耗时很高的毛刺出现,这意味这个用户请求在这个时刻会停顿2-3s,能否进一步优化,给用户一个更加极致的体验呢?



4.3 再次优化


这里首先要分析这现象背后的逻辑。



对于CMS搜集器,采用的搜集算法为Mark-Sweep-[Compact]。


CMS搜集器GC的种类:


CMS Background GC


这种GC是CMS最常见的一类,是周期性的,由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,采用的是Mark-Sweep方式,由于没有Compact这种耗时操作,且可以与用户进程并行,所以CMS的停顿会比较低,GC日志中出现GC (CMS Initial Mark)字样就代表发生了一次CMS Background GC。


Background GC由于采用的是Mark-Sweep,会导致老年代内存碎片,这也是CMS最大的弱点。


CMS Foreground GC


这种GC是CMS搜集器里真正意义上的Full GC,采用Serial Old或Parralel Old进行收集,出现的频率就较低,当往往出现后就会造成较大的停顿。


触发CMS Foreground GC的场景有很多,场景的如下:




  • System.gc();




  • jmap -histo:live pid;




  • 元数据区域空间不足;




  • 晋升失败,GC日志中的标志为ParNew(promotion failed);




  • 并发模式失败,GC日志中的标志为councurrent mode failure字样。




不难推断,目标方案中的毛刺是晋升失败或并发模式失败造成的,由于线上没有开启打印gc日志,但也无妨,因为这两种场景的根因是一致的,就是若干次CMS Backgroud GC后造成的老年代内存碎片。


我们只需要尽可能减少由于老年代碎片触发晋升失败、并发模式失败即可。


CMS Background GC由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,该阈值由-XX:CMSInitiatingOccupancyFraction; -XX:+UseCMSInitiatingOccupancyOnly两个参数控制,不设置,默认首次为92%,后续会根据历史情况进行预测,动态调整。


如果我们固定阈值的大小,将该阈值设置为一个相对合理的值,既不使GC过于频繁,又可以降低晋升失败或并发模式失败的概率,就可以大大缓解毛刺产生的频率。


目标方案的堆分布如下:




  • Young区 1.5G




  • Old区 2.5G




  • Old区常驻对象 约400M




按经验数据,75%,80%是比较折中的,因此我们选择-XX:CMSInitiatingOccupancyFraction=75 -


XX:+UseCMSInitiatingOccupancyOnly进行灰度观察(我们也对80%的场景做了对照实验,75%优于80%)。


最终目标方案的配置为:


-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

如上配置,灰度 xx.xxx.60.6 一台机器;



从再次优化的结果上看,CMS Foreground GC引起的毛刺基本消失,符合预期。


因此,视频服务最终目标方案的配置为;


-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

五、结果验收


灰度持续7天左右,覆盖工作日与周末,结果符合预期,因此符合在线上开启全量的条件,下面对全量后的结果进行评估。


Young GC次数



Young GC累计耗时



单次Young GC耗时



从Young GC指标上看,调整后Young GC次数平均减少30%,Young GC累积耗时平均减少17%,Young GC单次耗时平均增加约7ms,Young GC的表现符合预期。


除了技术手段,我们也在业务上做了一些优化,调优前实例的Young GC会出现明显的、不规律的(定时任务不一定分配到当前实例)毛刺,这里是业务上的一个定时任务,会加载大量数据,调优过程中将该任务进行分片,分摊到多个实例上,进而使Young GC更加平滑。


Full GC单次/累积耗时




从"Full GC"的指标上看,"Full GC"的频率、停顿极大减少,可以说基本上没有真正意义上的Full GC了。


核心接口-A (下游依赖较多) P99响应时间,减少19%(从 3457 ms下降至 2817 ms);



核心接口-B (下游依赖中等)  P99响应时间,减少41%(从 1647ms下降至 973ms);



核心接口-C (下游依赖最少) P99响应时间,减少80%(从 628ms下降至 127ms);



综合来看,整个结果是超出预期的。Young GC表现与设定的目标非常吻合,基本上没有真正意义上的Full GC,接口P99的优化效果取决于下游依赖的多少,依赖越少,效果越明显。


六、写在最后


由于GC算法复杂,影响GC性能的参数众多,并且具体参数的设置又取决于服务的特点,这些因素都很大程度增加了JVM调优的难度。


本文结合视频服务的调优经验,着重介绍调优的思路和落地过程,同时总结出一些通用的调优流程,希望能给大家提供一些参考。



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

Flutter 图片库高燃新登场

背景 去年,闲鱼图片库在大规模的应用下取得了不错的成绩,但也遇到了一些问题和诉求,需要进一步的演进,以适应更多的业务场景与最新的 flutter 特性。比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存...
继续阅读 »

背景


去年,闲鱼图片库在大规模的应用下取得了不错的成绩,但也遇到了一些问题和诉求,需要进一步的演进,以适应更多的业务场景与最新的 flutter 特性。比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。


这次,我们巧妙地将外接纹理与 FFi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。没错,Power 系列将新增一员,我们将新的图片库命名为 「PowerImage」!


我们将新增以下核心能力:


•支持加载 ui.Image 能力。在去年基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。•支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。•新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。•支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。•完善自定义图片类型通道。解决业务自定义图片获取诉求。•完善的异常捕获与收集。•支持动图。


去年图片方案可以参考《闲鱼Flutter图片框架架构演进(超详细)》


Flutter 原生方案


在我们新方案开始之前,先简单回忆一下 flutter 原生图片方案。



原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuildRawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image


Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。 ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。 ImageStream:图片资源加载的对象。


在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?


新的方案


我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。


FFI


正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image


首先 native 侧先获取必要的参数(以 iOS 为例):


    _rowBytes = CGImageGetBytesPerRow(cgImage);

CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
_handle = (long)CFDataGetBytePtr(rawDataRef);

NSData *data = CFBridgingRelease(rawDataRef);
self.data = data;
_length = data.length;

dart 侧拿到后


@override
FutureOr<ImageInfo> createImageInfo(Map map) {
Completer<ImageInfo> completer = Completer<ImageInfo>();
int handle = map['handle'];
int length = map['length'];
int width = map['width'];
int height = map['height'];
int rowBytes = map['rowBytes'];
ui.PixelFormat pixelFormat =
ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
Pointer<Uint8> pointer = Pointer<Uint8>.fromAddress(handle);
Uint8List pixels = pointer.asTypedList(length);
ui.decodeImageFromPixels(pixels, width, height, pixelFormat,
(ui.Image image) {
ImageInfo imageInfo = ImageInfo(image: image);
completer.complete(imageInfo);
//释放 native 内存
PowerImageLoader.instance.releaseImageRequest(options);
}, rowBytes: rowBytes);
return completer.future;
}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。


这里有两个优化方向:


1.解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。2.与 flutter 官方讨论,尝试从内部减少这次内存拷贝。


FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。


Texture


Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image 只有 textureId。这里有几个问题需要解决:


问题一:Image Widget 需要 ui.Image 去 build RawImage 从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。 问题二:ImageCache 依赖 ImageInfo 中 ui.Image 的宽高进行 cache 大小计算以及缓存前的校验。 问题三:native 侧 texture 生命周期管理


都有解决方案:


问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget 问题二:为 Texture 自定义 ui.image,如下:


import 'dart:typed_data';
import 'dart:ui' as ui show Image;
import 'dart:ui';

class TextureImage implements ui.Image {
int _width;
int _height;
int textureId;
TextureImage(this.textureId, int width, int height)
: _width = width,
_height = height;

@override
void dispose() {
// TODO: implement dispose
}

@override
int get height => _height;

@override
Future<ByteData> toByteData(
{ImageByteFormat format = ImageByteFormat.rawRgba}) {
// TODO: implement toByteData
throw UnimplementedError();
}

@override
int get width => _width;
}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。 实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE[1] 与 PR[2]。


问题三:关于 native 侧感知 flutter image 释放时机的问题


• flutter 在 2.2.0 之后,ImageCache 提供了释放时机,可以直接复用,无需修改。•< 2.2.0 版本,需要修改 ImageCache,获取 cache 被丢弃的时机,在 cache 被丢弃的时候,通知 native 进行释放。


修改的 ImageCache 释放如下(部分代码):


typedef void HasRemovedCallback(dynamic key, dynamic value);

class RemoveAwareMap<K, V> implements Map<K, V> {
HasRemovedCallback hasRemovedCallback;
...
}
//------
final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
//------
void hasImageRemovedCallback(dynamic key, dynamic value) {
if (key is ImageProviderExt) {
waitingToBeCheckedKeys.add(key);
}
if (isScheduledImageStatusCheck) return;
isScheduledImageStatusCheck = true;
//We should do check in MicroTask to avoid if image is remove and add right away
scheduleMicrotask(() {
waitingToBeCheckedKeys.forEach((key) {
if (!_pendingImages.containsKey(key) &&
!_cache.containsKey(key) &&
!_liveImages.containsKey(key)) {
if (key is ImageProviderExt) {
key.dispose();
}
}
});
waitingToBeCheckedKeys.clear();
isScheduledImageStatusCheck = false;
});
}

整体架构


我们将两种解决方案非常优雅地结合在了一起:



我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。


蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。


蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。


这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。


除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。


数据对比


FFI vs Texture:



机型:iPhone 11 Pro,图片:300 张网络图,行为:在listView中手动滚动到底部再滚动到顶部,native Cache:100MB,flutter Cache:100MB
复制代码

这里有两个现象:


Texture:    395MB波动,内存较平滑
FFI: 480MB波动,内存有毛刺

Texture 方案在内存方面表现优于 FFI,在内存水位与毛刺两方面:


•内存水位:由于 Texture 方案在 flutter 侧的 cache 为占位空壳,没有实际占用内存,因此只在 native 图片库的内存缓存中存在一份,所以 flutter 侧内存缓存实际上比 ffi 方案少了 100MB•毛刺:由于 ffi 方案不能避免 flutter 侧内存拷贝,会有先拷贝再释放的过程,所以会有毛刺。


结论:


1.Texture 适用于日常场景,优先选择;2.FFI 更适用于


1.flutter <= 1.23.0-18.1.pre 版本中,在模拟器上显示图片2.获取 ui.Image 图片数据3.flutter 侧解码,解码前的数据拷贝影响较小。(比如集团 Hummer 的外接解码库)


滚动流畅性分析:



设备: Android OnePlus 8t,CPU和GPU进行了锁频。
case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。
方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。

结论:


•UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。•Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。


更精简的代码:


dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。


FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。


未来


相信很多人注意到了,上文中少了动图部分。当前动图部分正在开发中,内部的 Pre Release 版本中,在 load 的时候返回的实际上是 OneFrameImageStreamCompleter,对于动图,我们将替换为 MultiFrameImageStreamCompleter,后面如何做,只是一些策略问题,并不难。顺便抛个另一种方案:可以把动图解码前的数据给 flutter 侧解码与渲染,但支持的格式不如原生丰富。


我们希望能将 PowerImage 贡献给社区,为了实现这一目标,我们提供了详细的设计文档、接入文档、性能报告,另外我们也在完善单元测试,在代码提交后或者 CR 时,都会进行单元测试。


最后,也是大家最关心的:我们计划在今年十二月底将代码开源在 「XianyuTech[3]」。


References


[1] ISSUE: github.com/flutter/flu…

[2] PR: github.com/flutter/flu…

[3] XianyuTech: github.com/XianyuTech


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

@OnLifecycleEnvent 被废弃,替代方案更简单

近期 androidx.lifecycle 发布了 2.4.0 版本,此次更新中 @OnLifecycleEvent 注解被废弃,官方建议使用 LifecycleEventObserver 或者 DefaultLifecycleObserver 替代 现...
继续阅读 »

近期 androidx.lifecycle 发布了 2.4.0 版本,此次更新中 @OnLifecycleEvent 注解被废弃,官方建议使用 LifecycleEventObserver 或者 DefaultLifecycleObserver 替代





现代的 Android 应用中都少不了 Lifecycle 的身影,正是各种 lifecycle-aware 组件的存在保证了程序的健壮性。


Lifecycle 本质是一个观察者模式的最佳实践,通过实现 LifecycleObserver 接口,开发者可以自自定 lifecycle-aware 组件,感知 Activity 或 Fragment 等 LifecycleOwner 的生命周期回调。


趁新版本发布之际,我们再回顾一下 Lifecycle 注解的使用以及废弃后的替代方案


Lifecycle Events & States


Lifecyce 使用两组枚举分别定义了 EventState



  • Events

    • ON_CREATE

    • ON_START

    • ON_RESUME

    • ON_PAUSE

    • ON_STOP

    • ON_DESTROY

    • ON_ANY



  • States

    • INITIALIZED

    • CREATED

    • STARTED

    • RESUMED

    • DESTROYED




Events 对应了 Activity 等原生系统组件的生命后期回调, 每当 Event 发生时意味着这些 LifecycleOwner 进入到一个新的 State



作为 观察者的 LifecycleObserver 可以感知到 被观察者的 LifecycleOwner 其生命周期 State 变化时的 Event。定义 LifecycleObserver 有三种方式:



  1. 实现 LifecycleEventObserver 接口

  2. 使用 @OnLifecycleEvent 注解


实现 LifecycleEventObserver


public interface LifecycleEventObserver extends LifecycleObserver {
void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event);
}

LifecycleEventObserver 是一个单方法接口,在 Kotlin 中可转为写法更简洁的 Lambda
进行声明


val myEventObserver = LifecycleEventObserver { source, event ->
when(event) {
Lifecycle.Event.ON_CREATE -> TODO()
Lifecycle.Event.ON_START -> TODO()
else -> TODO()
}
}

LifecycleEventObserver 本身就是 LifecycleObserver 的派生,使用时直接 addObserver 到 LivecycleOwner 的 Lifecycle 即可。


需要在 onStateChanged 中写 swich / case 自己分发事件。相对于习惯重写 Activity 或者 Fragment 的 onCreateonResume 等方法,稍显啰嗦。


因此 Lifecycle 给我们准备了 @OnLifecycleEvent 注解


使用 @OnLifecycleEvent 注解


使用方法很简单,继承 LifecycleObserver 接口,然后在成员方法上添加注解即可


val myEventObserver = object : LifecycleObserver {

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onStart() {
TODO()
}

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreat() {
TODO()
}
}

添加注册后,到 LifecycleOwner 的 Event 分发时,会自动回调注解匹配的成员方法,由于省去了手动 switch/case 的过程,深受开发者喜欢


注解解析过程


Event 分发时,怎么就会回到到注解对应的方法的?


通过 addObserver 添加的 LifecycleObserver ,都会转为一个 LifecycleEventObserver ,LifecycleOwner 通过调用其 onStateChanged 分发 Event


Lifecycling#lifecycleEventObserver 中处理转换


public class Lifecycling {

@NonNull
static LifecycleEventObserver lifecycleEventObserver(Object object) {
boolean isLifecycleEventObserver = object instanceof LifecycleEventObserver;
boolean isFullLifecycleObserver = object instanceof FullLifecycleObserver;
// 观察者是 FullLifecycleObserver
if (isLifecycleEventObserver && isFullLifecycleObserver) {
return new FullLifecycleObserverAdapter((FullLifecycleObserver) object,
(LifecycleEventObserver) object);
}

// 观察者是 LifecycleEventObserver
if (isFullLifecycleObserver) {
return new FullLifecycleObserverAdapter((FullLifecycleObserver) object, null);
}

if (isLifecycleEventObserver) {
return (LifecycleEventObserver) object;
}

final Class<?> klass = object.getClass();
int type = getObserverConstructorType(klass);

// 观察者是通过 apt 产生的类
if (type == GENERATED_CALLBACK) {
List<Constructor<? extends GeneratedAdapter>> constructors =
sClassToAdapters.get(klass);
if (constructors.size() == 1) {
GeneratedAdapter generatedAdapter = createGeneratedAdapter(
constructors.get(0), object);
return new SingleGeneratedAdapterObserver(generatedAdapter);
}
GeneratedAdapter[] adapters = new GeneratedAdapter[constructors.size()];
for (int i = 0; i < constructors.size(); i++) {
adapters[i] = createGeneratedAdapter(constructors.get(i), object);
}
return new CompositeGeneratedAdaptersObserver(adapters);
}

// 观察者需要通过反射生成一个 wrapper
return new ReflectiveGenericLifecycleObserver(object);
}

...

public static String getAdapterName(String className) {
return className.replace(".", "_") + "_LifecycleAdapter";
}
}

逻辑很清晰,根据 LifecycleObserver 类型不用转成不同的 LifecycleEventObserver,


用一段伪代码梳理如下:


if (lifecycleObserver is FullLifecycleObserver) {
return FullLifecycleObserverAdapter // 后文介绍
} else if (lifecycleObserver is LifecycleEventObserver) {
return this
} else if (type == GENERATED_CALLBACK) {
return GeneratedAdaptersObserver
} else {// type == REFLECTIVE_CALLBACK
return ReflectiveGenericLifecycleObserver
}

注解有两种使用用途。


场景一:runtime 时期使用反射生成 wrapper


class ReflectiveGenericLifecycleObserver implements LifecycleEventObserver {
private final Object mWrapped;
private final CallbackInfo mInfo;

ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}

@Override
public void onStateChanged(LifecycleOwner source, Event event) {
mInfo.invokeCallbacks(source, event, mWrapped);
}
}

CallbackInfo 是关键,通过反射收集当前 LifecycleObserver 的回调信息。onStateChanged 中通过反射调用时,不会因为因为缺少 method 报错。


场景二:编译时使用 apt 生成 className + _LifecycleAdapter


除了利用反射, Lifecycle 还提供了 apt 方式处理注解。


添加 gradle 依赖:


dependencies {
// java 写法
annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.3.1"
// kotlin 写法
kapt "androidx.lifecycle:lifecycle-compiler:2.3.1"
}

这样在编译器就会根据 LifecyceObserver 类名生成一个添加 _LifecycleAdapter 后缀的类。 比如我们加了 onCreatonStart 的注解,生成的代码如下:


public class MyEventObserver_LifecycleAdapter implements GeneratedAdapter {
final MyEventObserver mReceiver;

MyEventObserver_LifecycleAdapter(MyEventObserver receiver) {
this.mReceiver = receiver;
}

@Override
public void callMethods(LifecycleOwner owner, Lifecycle.Event event, boolean onAny,
MethodCallsLogger logger) {
boolean hasLogger = logger != null;
if (onAny) {
return;
}
if (event == Lifecycle.Event.ON_CREATE) {
if (!hasLogger || logger.approveCall("onCreate", 1)) {
mReceiver.onCreate();
}
return;
}
if (event == Lifecycle.Event.ON_START) {
if (!hasLogger || logger.approveCall("onStart", 1)) {
mReceiver.onStart();
}
return;
}
}
}

apt 减少了反射的调用,性能更好,当然会牺牲一些编译速度。


为什么要使用注解


生命周期的 Event 种类很多,我们往往不需要全部实现,如过不使用注解,可能需要实现所有方法,产生额外的无用代码


上面代码中的 FullLifecycleObserver 就是一个全部方法的接口


interface FullLifecycleObserver extends LifecycleObserver {

void onCreate(LifecycleOwner owner);

void onStart(LifecycleOwner owner);

void onResume(LifecycleOwner owner);

void onPause(LifecycleOwner owner);

void onStop(LifecycleOwner owner);

void onDestroy(LifecycleOwner owner);
}

从接口不是 public 的( java 代码 ) 可以看出,官方也无意让我们使用这样的接口,增加开发者负担。


遭废弃的原因


既然注解这么好,为什么又要废弃呢?



This annotation required the usage of code generation or reflection, which should be avoided.



从官方文档的注释可以看到,注解要么依赖反射降低运行时性能,要么依靠 APT 降低编译速度,不是完美的方案。


我们之所引入注解,无非是不想多实现几个空方法。早期 Android 工程不支持 Java8 编译,接口没有 default 方法, 现如今 Java8 已经是默认配置,可以为接口添加 default 方法,此时注解已经失去了存在的意义。


如今官方推荐使用 DefaultLifecycleObserver 接口来定义你的 LifecycleObserver


public interface DefaultLifecycleObserver extends FullLifecycleObserver {

@Override
default void onCreate(@NonNull LifecycleOwner owner) {
}

@Override
default void onStart(@NonNull LifecycleOwner owner) {
}

@Override
default void onResume(@NonNull LifecycleOwner owner) {
}

@Override
default void onPause(@NonNull LifecycleOwner owner) {
}

@Override
default void onStop(@NonNull LifecycleOwner owner) {
}

@Override
default void onDestroy(@NonNull LifecycleOwner owner) {
}
}

FullLifecycleObserverAdapter, 无脑回调 FullLifecycleObserver 即可


class FullLifecycleObserverAdapter implements GenericLifecycleObserver {

private final FullLifecycleObserver mObserver;

FullLifecycleObserverAdapter(FullLifecycleObserver observer) {
mObserver = observer;
}

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
switch (event) {
case ON_CREATE:
mObserver.onCreate(source);
break;
case ON_START:
mObserver.onStart(source);
break;
case ON_RESUME:
mObserver.onResume(source);
break;
case ON_PAUSE:
mObserver.onPause(source);
break;
case ON_STOP:
mObserver.onStop(source);
break;
case ON_DESTROY:
mObserver.onDestroy(source);
break;
case ON_ANY:
throw new IllegalArgumentException("ON_ANY must not been send by anybody");
}
}
}

需要注意 DefaultLifecycleObserver 在 2.4.0 之前也是可以使用的, 存在于 androidx.lifecycle.lifecycle-common-java8 这个库中, 2.4.0 开始 统一移动到 androidx.lifecycle.lifecycle-common 了 ,已经没有 java8 单独的扩展库了。


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

「如何优雅的不写注释?」每个工程师都要不断追求的漫漫长路

引言 ✨ 作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。 说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并...
继续阅读 »

引言 ✨


作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。


说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并不是银弹,也不是最好的手段,在我的眼里,注释更多承担的是一个兜底的作用。


于是便有了本篇文章,我想通过更多亲身经历和书籍参考来阐述证明我的观点:



  • 什么是好注释

  • 什么是坏注释

  • 怎么不通过注释提高编码可读性「易于上手版」

  • 挖一个关于前端架构的大坑「待我能力提升」



注意我并不是贬低使用注释,而是想给大家推销一种思想,即更多的用代码阐述自己的想法,把注释作为一种保底手段,而不是银弹。而且我切实的知道很多历史因素导致代码极其难以阅读,利用注释去表达信息也是没有办法的。但是希望大家读完本文之后可以更多去考量如何让代码更可读,而不是怎么写注释



不堪回首的摸爬滚打 🌧️


image.png


我毕业工作这一年以来,一直在不断加深对整洁编码的理解,我也经历过迷惑和自我怀疑。但是通过不断的实践,思考,回顾,我渐渐的有了自己的理解。


希望大家在阅读完成我的经历与摸爬滚打后,有所收获,如果觉得没啥参考性,就当作看了一场平平无奇的故事。


求学 🎓


曾经在求学过程中,大家的老师应该都会鼓励大家去多些注释去阐述你的编码思想,我便一直认为写注释是一个标准,是我在开发中必须要去做的事情。


然而现在我回过头去看曾经老师的教诲,我会觉得是出于以下三点考虑:



  • 对于一个计算机初学者,写注释可以让你去梳理你的编码思路

  • 写注释的时候也会对代码逻辑进行思考,类比伪代码

  • 很现实的一点,帮助老师理解你的代码,毕竟有的代码不写注释老师都不知道判卷的时候该给分还是不给分


实习 🧱


之后我便开始了我的第一段职业生涯,大四实习,我来到我现在所在的公司呆了两个月,做的也是比较简单的工作,改一改前人留下的遗产「bug」,到即将返校的时候,我接到了一个开发任务,当时我开发了一天,晚上进行 code-review,我记得当时我信心满满,给每个子方法和关键变量都写了注释,本来信心满满,但是最后我的代码被前辈批到自闭,其中不乏关于代码设计和命名相关的建议,也是在这次 review 中我听到了一句足以毁灭我学习生涯所建立的世界观的话:



好的代码是不需要注释的



工作 💻


工作之后,最开始我接触的项目也是一个恶臭代码的重灾区了,充斥着各种难以阅读的代码逻辑,但是那时候我还没有一个评判好坏的能力,一度去怀疑自己。



是不是我太菜了,才读不懂别人那些高端的代码



没错,我真的这么想过,甚至十分的自我怀疑,但是后期我经历了几件事情让我对这个自我怀疑的想法消除了:



  • 在导师的指导下对代码的反复 code-review 并重写



当时我们发现该项目存在需求遗漏,于是这个需求便来到了我的头上,即使项目紧急,导师还是给我细心 review,最后这个功能我重写了三四次,也让我对什么样的代码是好的有了一个粗略的概念。




  • 对某一个模块的完全重新设计与编码



经历了从设计到评审,再到编码,最后 review 的过程。




  • 相关书籍的阅读



本篇文章不做书籍推荐了,只是表达对于如何整洁编码是存在很多前人经验与指导原则存在的,新人可以优先阅读《代码整洁之道




  • 丰富的自我实践与思考



在我后来参与产线内部平台建设,负责安全运维大模块,负责冬奥会项目过程中也是不断的在追求整洁编码。思考,实践,回顾,一直伴随着我的职业道路,对于代码如何编写的更整洁也渐渐有了自己的想法。



在工作的过程中,我对于“好的代码是不需要注释的”这句话的理解也在不断加深。当然,如果对于某些难以处理的遗留问题,注释也是一个不错的方法对其进行注解描述。


总结 🔍


最开始我觉得注释是必要的,后经过经验的积累,前辈的教导,自己的学习,不断的思考与回顾,到现在有了自己的一套思想。当然我不会去说我的思想是正确的,可能过几年之后我会回来打我自己的脸,其实想法的改变,也能代表一种成长吧~


有关注释的杂七杂八 🌲


image.png



别给糟糕的代码加注释,重新写吧




  • 什么也比不上放置良好的注释来的有用

  • 什么也不会比乱七八糟的注释更有本事搞乱一个模块

  • 什么也不会比陈旧,提供错误信息的注释更具破坏性


若编程语言有足够的表达力,或者我们长于用这些语言来表达意图,就不那么需要注释 —— 也许根本不需要。


上面的话引自《代码整洁之道》。但是从事这个行业越久我越无法否认其正确性,我们必须要知道的一件事是代码具有实时性,即你现在项目中的代码总是当前最新的,否则也无法正确运行。然而上面的注释我们根本无法知道是什么时候写的,不具备实时性。



  • 代码是在变动,演化的。然而注释并不能随之变动

  • 程序员不会长期维护注释

  • 注释会撒谎,而代码不会

  • 不准确的注释比没注释坏的多

  • ...


所以我的想法很坚定,注释无法美化糟糕的代码,与其花时间为糟糕的代码编写解释不如花时间把糟糕的代码变得整洁。



用代码来阐述思想一直是最好的办法。



当然总有些注释是必须的或是有利的,还有一些注释是有害的,下面和大家聊一聊什么是好注释,什么是坏注释。


好注释 🌈



  • 法律信息



比如版权或者著作权的声明




  • 提供信息的注释



比如描述一个方法的返回值,但是其实可以利用函数名来传达信息




  • 阐释



把某些晦涩难懂的参数或者返回值翻译成某种可读的形式,更好的方式是让参数和返回值自身就足够清楚




  • TODO 注释



这个可能大家都会经常用,用来记录我们哪里还有任务没有完成




  • 放大



比如一个看似不起眼却很重要的变量,我们可以用注释凸显它的重要性




  • ...


坏注释 😈



  • 喃喃自语



只有作者读的懂的注释,当你打算开始写注释,就要讲清楚原委,和读者有良好的沟通




  • 多余的注释



有的注释写不写没啥作用,很简单的方法都要写注释,甚至读代码都比看注释快




  • 误导性注释



程序员都已经够辛苦了,你还要用注释欺骗人家




  • 循规式注释



要求每个方法每个变量都要有注释,很多废话只会扰乱读者




  • 位置标记



比如打了一堆 ****** 来标注位置,这个我上学的时候经常干




  • 废话注释



毫无作用的废话




  • 注释掉的代码



很多读者会想,代码依然留在那一定有原因,最后不敢删除畏手畏脚




  • 信息过多的注释



注释中包含很多无关的细节,其实对读者完全没有必要




  • ...


优雅的不写注释 🌿


image.png


首先我再次阐述之前说过的话,编码实际上是一种社会行为,是需要沟通的。而如何让我们不借助注释来阐述我们的思想,其实是需要我们长期探索并在实践中积累经验的,从我的经验与视角出发,其实让我们的代码库更加整洁其实主要从以下两个方面考量:



  • 整洁编码

  • 前端架构


下面我分开来讲~



注意,编码不是一个人的事情,在我眼里如何做到团队成员编码风格的相近才是最具成效且需要长期努力的任务,也是相对理想且难以做到的。正所谓,就算我们写的代码很烂,但是烂的我们的成员可以相互理解,也是一种优秀「瞎说的,哈哈哈,代码可维护性还是要团队成员一起追求的」。



整洁编码 📚


首先我先引用几位前辈的话,带大家感受一下,什么样的代码是整洁的:



  • Bjarne:我喜欢优雅和高效的代码,代码的逻辑应当直接了当,叫缺陷难以隐藏们。尽量减少依赖关系,使之便于维护,依据某种分层战略完善错误处理代码,性能调至最优,省得引诱别人做没有规矩的优化,搞出一堆混乱出来,整洁的代码只做好一件事。

  • Grady: 整洁的代码简单直接,整洁的代码从不隐藏设计者的意图,充满干净利落的抽象和直截了当的控制语句。


对于整洁编码可以先简单总结:



  • 尽量减少依赖关系,便于维护

  • 简单直接,充满了干净利落逻辑处理和直截了当的控制语句。

  • 能够全部运行通过,并配有单元测试和验收测试

  • 没有重复的代码

  • 写的代码能够完全提现我们的设计理念「这个可以通过类、方法、属性的命名,代码逻辑编码的清晰来体现



在我们日常编码中,命名和函数可以说是我们最常接触的,也是最能影响我们代码整洁度的。于是本文中,我将围绕这两个方向为大家介绍几种易于上手的整洁编码方案。



下文参考我之前写过的一篇文章:关于整洁代码与重构的摸爬滚打


命名 🌟



  • 只要命名需要通过注释来补充,就表示我们的命名还是存在问题

  • 所有的命名都要有实际意义,命名会告诉你它为什么存在,它做什么事情,应该怎么用


比如列举一段曾经上学的时候可能写出的代码:


#include <stdio.h>  
int main(){
printf("Hello, C! \n");
int i = 10;
int m = 1;
for(int j = 0; j < i; j+=m){
for(int n = 0; n< i-j;n++){
printf("*");
}
printf("\n");
}
return 0;
}

我们看这里命名都是一大堆 i,m,n,j之类的根本不知道这些变量用来干嘛,其实这段代码最后仅仅打印出来的是 * 组成的直角三角形。但是当时写代码我确实就是这样,i, j,m,n等等字母用了一遍,也不包含什么语义上的东西,变量命名就是字母表里面选。


当然现在的命名就高端多了,开始从词典里面找了,还要排列组合,比如 getUserisAdmin。语义上提升了,通过命名我们也可以直观的判断这个方法是干嘛的,这个变量是干嘛的。


这样看其实命名是很有学问的事情,下面我开始列举几点命名中可以快速提升代码整洁度的方法:



  • 避免引起误导



不要用不该作为变量的专有名词来为变量命名,一组账号accountList ,却不是List类型,也是存在误导。命名过于相似:比如 XYZHandlerForAORBORC 和XYZControllerForAORBORDORC,谁能一眼就看出来呢~




  • 做有意义的区分


let fn = (array1,array2) =>{ 
for(let i =0 ;i<array1.length;i++){
array2[i] = array1[i];
}
}


比如上面 array1array2 就不是有意义的区分,这只是一个赋值操作,完全可以是 sourceArrayDesArray

再比如 起的名字:userInfouserData都是这种的,我们很难读懂这两个有啥子区别,这种区分也没啥意义,说白了这只是单词拼写的区分,而不是在语义上区分开了。




  • 使用读的出来的名称



编程是社会活动,免不了与人交流对话,使用难以轻松读出来的声音会导致你的思想难于传达。并且人类的大脑中有专门处理语言的区域,可以辅助你理解问题,不加以运用简直暴殄天物。简单举个例子:getYMDHMS,这个方法就是获取时间,然而就是难以阅读,是不好的命名。




  • 使用可以搜索的名称



之前的代码,我用个 i 作为变量。如果代码很长,我这想要追踪一下这个i的变化,简直折磨。同理我不喜欢直接以 valuedatainfo 等单词直接做变量,因为他们经常以其他变量的组成部分出现,难以追踪。




  • 程序中有意义的数字或者字符串应该用常量进行替换,方便查找


export const DEFAULT_ORDERBY = '-updateTime' 
export const DEFAULT_CHECKEDNUM = 0


比如采用上面的方式,既可以让代码更加语义化也方便集中修改




  • 类名和对象名应为名词或名词词组,方法名应为动词或动词词组



比如我们常用的 updatexxxfilteredXXX 都是这样的命名规则




  • 属性命名添加有用必要的语境,但是短名称如果足够用清楚,就比长名称好,别添加不必要的语境

  • 每个概念对应一个词



比如 taglabelticketworkOrder 各种混着用岂不是乱糟糟的,这读者容易混淆,也会为以后造成负担,也可能会隐藏一些 bug。所以我们在项目开发前可以确定一个名词术语表来避免这种情况发生。




  • ...


函数 🌟



大师写代码是在讲故事,而不是在写程序。




  • 短小:20封顶最佳

  • 函数的缩进层级尽可能的少

  • 函数参数尽量少

  • 使用具有描述性的函数名



当然函数越短小,功能越集中,就越便于取好名字




  • 抽取异常处理,即 try-catch 就是一个函数 ,函数应该只做一件事,错误处理就是一件事

  • 标识参数丑陋不堪


const updateList = (flag) {
if(flag){
// ...
} else {
// ...
}
}


比如一个方法,定义成上面这个样子,我们很难通过方法定义直接了解其能力以及参数的含义。




  • 函数名是动词,参数是名词,并保证顺序



比如 saveField(name)assertExpectedEqualsActual(expected,actual)




  • 无副作用



比如一个方法名是 updateList,后来者应该顺理成章的认为这个方法只会更新列表,然而开发者在这个方法中加入了其他的逻辑,可能导致后来者在使用这个方法后导致副作用,而代码报错无法正常运行。




  • 重复是软件中一切邪恶的根源,拒绝重复的代码

  • ...


写代码和写文章一样,先去想你要写什么,最后再去打磨,初稿也许粗糙无序,那就要斟酌推敲,直到达成心中的样子。编程艺术也是语言设计的艺术。


前端架构 🎋



本人现在工作一年有余,一年半不足,对于前端架构并不能很好的输出给大家,所以在此给大家先挖一个大坑,本章节中如有错误理解,请大家不吝赐教,与我探讨交流,感谢。



首先,我先解释一下我为什么要把前端架构放在这样的一篇文章中,其实是存在两条原因:



  • 从个人开发角度来看,优秀的前端架构可以增强代码的维护性



试想一个组织结构恶臭的项目,一定会影响你的阅读的,杂乱不堪的组件划分原则,不清晰的边界通通都会成为巨大的阻力。




  • 最近换了组,到了天擎终端平台组,新的 leader 也分享了很多关于组件化的经验与理解



浅薄无知的小寒草🌿,在线求鞭策。



那么,大家在提到前端架构的时候,会想到什么呢,我反正会想到以下几点:



  • 组件化

  • 架构模式

  • 规范 / 约定

  • 分层

  • ...


下面我逐条来讲~


架构模式 ✨


组件化我先跳过,最后再说,先说说架构模式,大家脑子里一定会想到 MVVMMVC 等模式,比如我们常用的 Vue 框架中的 MVVM ,以及普遍在 Spring 那一套中被提及并在在 nest.js 中有所应用的 MVC。但是关于架构模式前端说的可能还是相对较少,我的水平也有限,而且说起来可能就会跑题了,于是也不在本文过多赘述。


规范&约定 ✨


关于规范或者约定,常见的包括:



  • 目录结构

  • 命名规范

  • 术语表

  • ...


其实这几点我们很好理解,我们会通过约定或者脚手架等方式来规范化我们的目录结构,使我们同一个产线下项目的目录结构保证一致。以及我们在开发前的设计阶段可能也需要出具一份术语表,这个前文也听到过一个含义用一个确定的词来表示,否则可能会导致代码的混乱。


关于命名规范,首先我们需要去约定一个统一的命名规则,我们常见的是变量命名为小驼峰,文件命名为连字符。但是这个命名规范其实我们可以做的事情不止这些,比如我说几个例子:



  • 前端命名规范是小驼峰,服务端命名是下划线,我们怎么处理让前端编码中屏蔽掉命名规则差异。

  • 同一个含义我们可以用很多命名来表示,比如:handleStaffUpdate / updateStaff。在项目初期我们完全可以对其进行约束使用哪种命名风格,以让我们项目一致性加强。

  • ...


分层 ✨


关于分层,大家的差异可能会比较大,比如我们可能会把我们的前端项目分为以下几层:



  • 业务层

  • 服务层

  • 模型层「可能有也可能没有」


业务层就是我们比较熟悉的,各种业务代码。


服务层「server」不知道大家的项目中有没有,我们项目使用 grpc 接口,由于接口粒度较高,我们通常会在 server 层对接口再次处理,合并,或者在这个层去完成一些服务端不合理设计的屏蔽。


模型层「model」不常有,但是一些复杂的又需要复用的逻辑可能有这个层,就相当于逻辑的抽象,脱离于视图,之后如果我们需要复用这里的逻辑,而视图不同,我们就可以使用这个 model


合理的分层可以让我们的项目更清晰,减少代码冗杂,提升可维护性。


组件化 ✨


其实组件化一直都是前端架构中的大课题,首先我们可以通过组件化能得到什么,其实最重要的可能就是:



  • 复用


不知道大家的项目有没有统计代码复用率,我们是有的,而且这也是前端工程质量很重要的一个指标。然而在追求组件化的过程中其实我们很少会拥有一个衡量标准:



  • 什么情况需要拆分组件

  • 什么情况不需要拆分组件


团队对这个问题没有一个统一认知的情况下很容易造成:



  • 五花八门的组件拆分原则导致代码结构混乱

  • 无效的组件拆分导致文件过多,维护困难

  • 过深的组件嵌套层级「经历过的人一定会对此深恶痛绝」

  • ...


其实我最开始的时候也喜欢把组件按照很细的粒度进行拆分,想的是总会有用到的时候嘛,但是从架构整洁的角度出发,过细或者过于粗糙的组件拆分都会导致维护困难,复用困难等问题,现在的我可能更会从复用性角度出发:



  • 这个东西会不会复用


只从复用性考量很容易的就会把组件区分为两大类:



  • 需要复用的组件

  • 几乎不会被复用的组件



注意我没有说什么组件是肯定不会被复用的,而是几乎不会被复用。



所以我们就可以坐下来思考,把我们工作中常见的场景拎出来,过一遍,因为我们工作的业务场景不同,所以我肯定还是以我的业务场景出发,那么我可以把我的组件分成几种:



  • page 组件

  • layout 组件

  • 业务组件


其中我认为,page 组件是几乎不会复用的组件,layout 组件和业务组件在我眼里是可以复用的组件。



这只是很粗糙的的区分,之后还有很多问题:



  • 如何把业务组件写的好用

  • 如何确定一个组件的边界

  • ...


这些我们就要从消费者角度考量了。



当然其实组件化也可以和分层一起考虑,因为组件其实也会有层级,比如:



  • 基础 ui 组件[参考element-ui]

  • 基础业务组件


基础业务组件也可以按照是否跨模块等原则继续进行分层,这个可以按照大家的业务场景自行考量。


总结 ✨


从实际经验出发,合理的架构确实是项目易于维护「从而优雅的不写注释🌿」,而这是一个自顶向下分析决策的过程,本章节篇幅有限,加上我水平有限,无法在此过多赘述,还请大家持续期待我的分享。


结束语 ☀️


image.png


那么本篇文章就结束了,涵盖了我个人经历上的摸爬滚打,解析什么样的注释是好的,什么样的注释是坏的,并从编码整洁度与前端架构的角度出发来考量如何提升代码的可维护性。以此来论述我的观点:



注释不是维护代码的银弹,而便于维护的代码需要从整洁编码前端架构两个「或者更多」层面入手。



我工作的时间不长也不短了,已经一年出头了,我一直秉承着编码是社会性工作,需要协同合作,代码的可维护性也是一名职业软件工程师需要持续追求的观点。


思考,实践,回顾的过程没有停歇,我在此也希望大家多思考,作为一名工程师我们需要追求的不仅仅只有:



收起阅读 »

Node 之一个进程的死亡

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。 一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能...
继续阅读 »

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。


一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能够看到脚本的身影:



  1. CI,用以测试、质量保障及部署等

  2. Cron,用以定时任务

  3. Docker,用以构建镜像


如果在这些重要流程中脚本出错无法及时发现问题,将有可能引发更加隐蔽的问题。如果在 HTTP 服务出现问题时,无法捕获,服务异常是不可忍受的。


最近观察项目镜像构建,会偶尔发现一两个镜像虽然构建成功,但容器却跑不起来的情况究其原因,是因为 一个 Node 进程灭亡却未曾感知到的问题


Exit Code



什么是 exit code?



exit code 代表一个进程的返回码,通过系统调用 exit_group 来触发。


POSIX 中,0 代表正常的返回码,1-255 代表异常返回码,在业务实践中,一般主动抛出的错误码都是 1。在 Node 应用中调用 API process.exitCode = 1 来代表进程因期望外的异常而中断退出。


这里有一张关于异常码的附表 Appendix E. Exit Codes With Special Meanings


异常码在操作系统中随处可见,以下是一个关于 cat 进程的异常以及它的 exit code,并使用 strace 追踪系统调用。


$ cat a
cat: a: No such file or directory

# 使用 strace 查看 cat 的系统调用
# -e 只显示 write 与 exit_group 的系统调用
$ strace -e write,exit_group cat a
write(2, "cat: ", 5cat: ) = 5
write(2, "a", 1a) = 1
write(2, ": No such file or directory", 27: No such file or directory) = 27
write(2, "\n", 1
) = 1
exit_group(1) = ?
+++ exited with 1 +++

strace 追踪进程显示的最后一行可以看出,该进程的 exit code 是 1,并把错误信息输出到 stderr (stderr 的 fd 为2) 中


如何查看 exit code


strace 中可以来判断进程的 exit code,但是不够方便过于冗余,更无法第一时间来定位到异常码。


有一种更为简单的方法,通过 echo $? 来确认返回码


$ cat a
cat: a: No such file or directory

$ echo $?
1

$ node -e "preocess.exit(52)"
$ echo $?
52

未曾感知的痛苦何在: throw new ErrorPromise.reject 区别


以下是两段代码,第一段抛出一个异常,第二段 Promise.reject,两段代码都会如下打印出一段异常信息,那么两者有什么区别?


function error () {
throw new Error('hello, error')
}

error()

// Output:

// /Users/shanyue/Documents/note/demo.js:2
// throw new Error('hello, world')
// ^
//
// Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)

async function error () {
return new Error('hello, error')
}

error()

// Output:

// (node:60356) UnhandledPromiseRejectionWarning: Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)
// at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1)
// at Module._compile (internal/modules/cjs/loader.js:701:30)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)

在对上述两个测试用例使用 echo $? 查看 exit code,我们会发现 throw new Error()exit code 为 1,而 Promise.reject() 的为 0。


从操作系统的角度来讲,exit code 为 0 代表进程成功运行并退出,然而此时即使有 Promise.reject,操作系统也会视为它执行成功。


这在 DockerfileCI 中执行脚本时将留有安全隐患。


Dockerfile 在 Node 镜像构建时的隐患


当使用 Dockerfile 构建镜像或者 CI 时,如果进程返回非0返回码,构建就会失败。


这是一个浅显易懂的含有 Promise.reject() 问题的镜像,我们从这个镜像来看出问题所在。


FROM node:12-alpine

RUN node -e "Promise.reject('hello, world')"

构建镜像过程如下,最后两行提示镜像构建成功:即使在构建过程打印出了 unhandledPromiseRejection 信息,但是镜像仍然构建成功。


$ docker build -t demo .
Sending build context to Docker daemon 33.28kB
Step 1/2 : FROM node:12-alpine
---> 18f4bc975732
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 79a6d53c5aa6
(node:1) UnhandledPromiseRejectionWarning: hello, world
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Removing intermediate container 79a6d53c5aa6
---> 09f07eb993fe
Successfully built 09f07eb993fe
Successfully tagged demo:latest

但如果是在 node 15 镜像内,镜像会构建失败,至于原因以下再说。


FROM node:15-alpine

RUN node -e "Promise.reject('hello, world')"

$ docker build -t demo .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM node:15-alpine
---> 8bf655e9f9b2
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 4573ed5d5b08
node:internal/process/promises:245
triggerUncaughtException(err, true /* fromPromise */);
^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello, world".] {
code: 'ERR_UNHANDLED_REJECTION'
}
The command '/bin/sh -c node -e "Promise.reject('hello, world')"' returned a non-zero code: 1

Promise.reject 脚本解决方案


能在编译时能发现的问题,绝不要放在运行时。所以,构建镜像或 CI 中需要执行 node 脚本时,对异常处理需要手动指定 process.exitCode = 1 来提前暴露问题


runScript().catch(() => {
process.exitCode = 1
})

在构建镜像时,Node 也有关于异常解决方案的建议:



(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see nodejs.org/api/cli.htm…). (rejection id: 1)



根据提示,--unhandled-rejections=strict 将会把 Promise.reject 的退出码设置为 1,并在将来的 node 版本中修正 Promise 异常退出码。


而下一个版本 Node 15.0 已把 unhandled-rejections 视为异常并返回非0退出码。


$ node --unhandled-rejections=strict error.js 

Signal


在外部,如何杀死一个进程?答:kill $pid


而更为准确的来说,一个 kill 命令用以向一个进程发送 signal,而非杀死进程。大概是杀进程的人多了,就变成了 kill。



The kill utility sends a signal to the processes specified by the pid operands.



每一个 signal 由数字表示,signal 列表可由 kill -l 打印


# 列出所有的 signal
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

这些信号中与终端进程接触最多的为以下几个,其中 SIGTERM 为 kill 默认发送信号,SIGKILL 为强制杀进程信号


在 Node 中,process.on 可以监听到可捕获的退出信号而不退出。以下示例监听到 SIGINT 与 SIGTERM 信号,SIGKILL 无法被监听,setTimeout 保证程序不会退出


console.log(`Pid: ${process.pid}`)

process.on('SIGINT', () => console.log('Received: SIGINT'))
// process.on('SIGKILL', () => console.log('Received: SIGKILL'))
process.on('SIGTERM', () => console.log('Received: SIGTERM'))

setTimeout(() => {}, 1000000)

运行脚本,启动进程,可以看到该进程的 pid,使用 kill -2 97864 发送信号,进程接收到信号并未退出


$ node signal.js
Pid: 97864
Received: SIGTERM
Received: SIGTERM
Received: SIGTERM
Received: SIGINT
Received: SIGINT
Received: SIGINT

容器中退出时的优雅处理


当在 k8s 容器服务升级时需要关闭过期 Pod 时,会向容器的主进程(PID 1)发送一个 SIGTERM 的信号,并预留 30s 善后。如果容器在 30s 后还没有退出,那么 k8s 会继续发送一个 SIGKILL 信号。如果古时皇帝白绫赐死,教你体面。


其实不仅仅是容器,CI 中脚本也要优雅处理进程的退出。


当接收到 SIGTERM/SIGINT 信号时,预留一分钟时间做未做完的事情。


async function gracefulClose(signal) {
await new Promise(resolve => {
setTimout(resolve, 60000)
})

process.exit()
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

这个给脚本预留时间是比较正确的做法,但是如果是一个服务有源源不断的请求过来呢?那就由服务主动关闭吧,调用 server.close() 结束服务


const server = http.createServer(handler)

function gracefulClose(signal) {
server.close(() => {
process.exit()
})
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

总结



  1. 当进程结束的 exit code 为非 0 时,系统会认为该进程执行失败

  2. 通过 echo $? 可查看终端上一进程的 exit code

  3. Node 中 Promise.reject 时 exit code 为 0

  4. Node 中可以通过 process.exitCode = 1 显式设置 exit code

  5. 在 Node12+ 中可以通过 node --unhandled-rejections=strict error.js 执行脚本,视 Promise.rejectexit code 为 1,在 Node15 中修复了这一个问题

  6. Node 进程退出时需要优雅退出

  7. k8s 关闭 POD 时先发一个 SIGTERM 信号,留 30s 时间处理未完成的事,如若 POD 没有正常退出,30s 过后发送 SIGKILL 信号


收起阅读 »

Android Gradle 基础自定义构建

win7 Android Studio 2.1.3基础自定义构建 Basic Build Customization本章目的理解Gradle文件build tasks入门自定义构建理解Gradle文件在Android Studio中新建一个项目后,会自动创建3...
继续阅读 »

win7 Android Studio 2.1.3

基础自定义构建 Basic Build Customization

本章目的

  • 理解Gradle文件
  • build tasks入门
  • 自定义构建

理解Gradle文件

在Android Studio中新建一个项目后,会自动创建3个Gradle文件。

MyApp
├── build.gradle
├── settings.gradle
└── app
└── build.gradle

每个文件都有自己的作用

settings.gradle文件

新建工程的settings文件类似下面这样

include ':app'

Gradle为每个settings文件创建Settings对象,并调用其中的方法。

The top-level build file 最外层的构建文件

能对工程中所有模块进行配置。如下

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.3'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

buildscript代码块是具体配置的地方,引用JCenter仓库。 本例中,一个仓库代表着依赖库,换句话说是app可以从中下载使用库文件。
JCenter是一个有名的 Maven 仓库。

dependencies代码块用来配置依赖。上面注释说明了,不要在此添加依赖,而应该到独立的模块 中去配置依赖。

allprojects能对所有模块进行配置。

模块中的build文件

模块中的独立配置文件,会覆盖掉top-level的build.gradle文件

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.2"

defaultConfig {
applicationId "com.xxx.rust.newproj"
minSdkVersion 18
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:25.1.0'
}

下面来看3个主要的代码块。

plugin

第一行应用了Android 应用插件。Android插件由Google团队开发维护。该插件提供构建,测试,打包应用和模块需要的所有的task。

android

最大的一个区域。defaultConfig区域对app核心进行配置,会配置覆盖AndroidManifest.xml中的配置。

applicationId复写掉manifest文件中的包名。但applicationId和包名有区别。
manifest中的包名,在源代码和R文件中使用。所以package name在android studio中理解为一个查询类的路径比较合理。
applicationId在Android系统中是作为应用的唯一标识,即在一个Android设备中所有的应用程序的applicationId都是唯一的。

dependencies

是Gradle标准配置的一部分。 Android中用来配置使用到的库。

定制化构建 Customizing the build

BuildConfig and resources

自从SDK17以来,构建工具会生成一个BuildConfig类,包含着静态变量DEBUG和一些信息。
如果你想在区分debug和正式版,比如打log,这个BuildConfig类很有用。
可以通过Gradle来扩展这个类,让它拥有更多的静态变量。

以NewProj工程为例,app\build.gradle

android {
compileSdkVersion 25
buildToolsVersion "25.0.2"

defaultConfig {
applicationId "com.xxx.rust.newproj"
minSdkVersion 18
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
buildConfigField("String", "BASE_URL", "\"http://www.baidu.com\"")
buildConfigField("String", "A_CONTENT", "\"debug content\"")
resValue("string", "str_version", "debug_ver")
}
release {
buildConfigField("String", "BASE_URL", "\"http://www.qq.com\"")
buildConfigField("String", "A_CONTENT", "\"release content\"")
resValue("string", "str_version", "release_ver")

minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

上面的buildConfigFieldresValue在编译后,能在源代码中使用
注意上面那个转义的分号不可少;注意里面的大小写,这里传入的参数就像是直接填入的代码一样

下面是编译后生成的BuildConfig文件,可以看到buildConfigField的东西已经在里面了

public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.xxx.rust.newproj";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
// Fields from build type: debug
public static final String A_CONTENT = "debug content";
public static final String BASE_URL = "http://www.baidu.com";
}

resValue会被添加到资源文件中

mTv2.setText(R.string.str_version);

通过 build.gradle 增加获取 applicationId 的方式

模块build.gradle中添加属性applicationId,会被编译到BuildConfig中

project.afterEvaluate {
project.android.applicationVariants.all { variant ->
def applicationId = [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join()
}
}

在代码中可以直接使用

String appID = BuildConfig.APPLICATION_ID;

获取时间的方法

模块build.gradle中添加方法getTime(),并在buildTypes中添加域。

// 获取当前时间
static def getTime() {
String timeNow = new Date().format('YYYYMMdd-HHmmss')
return timeNow
}

android {
// ...
buildTypes {
debug {
buildConfigField "String", "BUILD_TIME", "\"" + getTime() + "\""
}
release {
buildConfigField "String", "BUILD_TIME", "\"" + getTime() + "\""
// ...
}
}
}

BuildConfig.java中得到这个域。

  // Fields from build type: debug
public static final String BUILD_TIME = "20180912-100335";

修改release apk文件名的方法

gradle版本3.1.4。使用了上面的方法getTime()

android {
// ...
// 修改release的apk名字
applicationVariants.all { variant ->
variant.outputs.all {
if (variant.buildType.name == 'release') {
outputFileName = "xxx_release_${defaultConfig.versionName}_${getTime()}.apk"
}
}
}
}

以前的方法可能会遇到问题:Cannot set the value of read-only property 'outputFile' for ApkVariantOutputImpl_Decorated
参考:stackoverflow.com/questions/4…

工程范围的设置

如果一个工程中有多个模块,可以对整个工程应用设置,而不用去修改每一个模块。

NewProj\build.gradle

allprojects {
repositories {
jcenter()
}
}

ext {
compileSDKVersion = 25
local = 'Hello from the top-level build'
}

每一个build.gradle文件都能定义额外的属性,在ext代码块中。

在一个模块的libmodule\build.gradle文件中,可以引用rootProject的ext属性

android {
compileSdkVersion rootProject.ext.compileSDKVersion
buildToolsVersion "25.0.2"
// ....
}

工程属性 Project properties

定义properties的地方

  • ext代码块
  • gradle.properties文件
  • 命令行 -P 参数

工程build.gradle文件

ext {
compileSDKVersion = 25
local = 'Hello from the top-level build'
}

/**
* Print properties info
*/

task aPrintSomeInfo {
println(local)
println('project dir: ' + projectDir)
println(projectPropertiesFileText)
}

task aPrintAllProperites() {
println('\nthis is aPrintAllProperites task\n')
Iterator pIt = properties.iterator()
while (pIt.hasNext()) {
println(pIt.next())
}
}

gradle.properties文件中增加

projectPropertiesFileText = Hello there from gradle.properties

在as的Gradle栏上双击执行aPrintSomeInfo,会连带下一个task也执行

13:08:10: Executing external task 'aPrintSomeInfo'...
Hello from the top-level build
project dir: G:\openSourceProject\NewProj
Hello there from gradle.properties

this is aPrintAllProperites task
......
BUILD SUCCESSFUL

Total time: 1.025 secs
13:08:11: External task execution finished 'aPrintSomeInfo'.

参考:Gradle for Android Kevin Pelgrims


收起阅读 »

Android Handler解读

Handler通常都会面被问到这几个问题1.一个线程有几个Handler?2.一个线程有几个Looper?如何保证?3.Handler内存泄漏原因?4.子线程中可以new Handler吗?5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?...
继续阅读 »

Handler通常都会面被问到这几个问题

  • 1.一个线程有几个Handler?
  • 2.一个线程有几个Looper?如何保证?
  • 3.Handler内存泄漏原因?
  • 4.子线程中可以new Handler吗?
  • 5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?
  • 6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
  • 7.我们使用Message时应该如何创建它

Handler的总体框架

Handler的流程

这是我在网上看到的一张图,很形象的体现Handler的工作流程,也说明了Handler几个关键类之间的关系 Handler 只负责将message放到MessageQueue,然后再从MessageQueue取出message发送出去 MessageQueue 就是传送带,上面一直传送的许多message Looper 就是传送带的轮子,他带动这MessageQueue一直跑动 Thread 就是动力,要是没有线程,整个传送都不会开始,并且Looper还提供了一个开关给Thread,开启才会传送

image.png

MessageQueue 和 Message

添加消息

只要你使用handler发送消息,最后都会走到handler#enqueueMessag 然后调用MessageQueue#enqueueMessage,可以看到方法需要传入一个Message的

handler#enqueueMessage handler#enqueueMessag

MessageQueue#enqueueMessage MessageQueue#enqueueMessage

而且MessageQueue里面还存放了一个mMessage变量,有什么作用呢,让我们先来看一下Message 是什么 image.png

Message就是我们所发送的一个个消息体,而在这个类中 可以看到,一个Message变量里,又存放一个Message叫做next,存放下一个Message的,这又有啥用呢 image.png

再次回到MessageQueue#enqueueMessage,看一看这些变量到底有什么作用 image.png

首先第一个msg1进入时,p = mMessage = null,所以进入第一个if语句 所以msg1.next = p = null,mMessage = msg1

而第二个msg2进入时,假设msg2的执行时间when是在msg1之后的, 此时p = mMessage = msg1,而when(msg2.when) > p.when(msg1.when) 则if语句就不成立了,会进入else语句的for循环 image.png

此时的prev = p = mMessage = msg1, 而p = p.next(p就是msg1,msg1.next = null),此时的p就为null 所以break出去后,for循环也结束了

最后两句就是做了下图的操作 msg2.next = p = null prev.next(msg1.next) = msg2 image.png

结构就像这样,通过这样的赋值操作,这样就形成了一个链表结构 所以MessageQueue就相当于是一个仓库,里面存放着由许许多多的Message组成的链条 image.png

取消息

取消息的方法是MessageQueue#next()方法,里面的代码先不做分析, 我们知道发送消息是handler调用的 那么取消息是谁调用的呢 image.png

根据一开始的图很容易知道,是Loop#loop()调用了该方法 而在这个方法拿到msg后 会调用 msg.target.dispatchMessage(msg)将消息发送出去,这里的msg.target 就是 handler image.png

image.png

所以他们形成了这样一种模式,一种生产者消费者模型

image.png

也就是说要调用Looper.loop()才会取出消息去分发,但是我们再主线程的时候,都是直接使用Handler,是哪里帮我们调用了Looper.loop()函数呢,直接看到主线程的main函数就能看到,也就是说app一启动,主线程就帮我们调用了Looper.loop()函数 image.png

知道流程后,回到一开始的问题

1.一个线程有几个Handler?

这个问题其实不用说都知道,难道主线程不能使用多个Handler吗

2.一个线程有几个Looper?如何保证?

答案很简单,一个线程只有一个Looper,但是怎么保证的呢?

我们先来看看Looper是怎么创建的,是谁创建的 可以看到,Looper的构造函数只在prepare这里使用过,而且系统也有提示我们, image.png

但是Looper存放在了sThreadLocal变量中,所以先看看sThreadLocal是什么 查阅到就是Looper中的一个静态变量的ThreadLocal类,好像看不出什么

image.png

那就进入sThreadLocal.set(Looper)方法看一下

image.png

  • 1.可以看到set方法中,首先获取了当前线程,则prepare() --> set() --> 当前线程

也就是说,Thread1调用prepare方法,获取的当前线程也就是Thread1,不可能为其他线程。

  • 2.然后通过getMap(当前线程)获得ThreadLocalMap,也就是说Thead和ThreadLocalMap有关系。也可以看到Thread中有ThreadLocalMap的变量

image.png

  • 3.最后将this(当前ThreadLocal)与传入的Looper保存在ThreadLocalMap中
  • 4.ThreadLocalMap就是一个保存<key,value>键值对的

所以看一下 Thread,ThreadLocalMap,ThreadLocal,Looper的关系 image.png

所以这里保证了一个Thread对应一个ThreadLocalMap,而ThreadLocalMap又保存这该Thread的ThreadLocal。问题来了<key,vaule>中key是唯一的,但是value是可以代替的,怎么能做到<ThreadLocal,Looper>保存之后Looper不会被代替呢

再回到prepare函数,可以看到在new Looper之前,还有一个get()操作 image.png

get函数做了一个操作,就是查看当前Thread对应的ThreadLocal,在ThreadLocalMap有没有值,有值则在prepare抛出异常 也就是说,prepare在一个线程中,只能够调用一次,也就保证了Looper只能生成一次,也就是唯一的

image.png

3.Handler内存泄漏原因?

我们知道,handler不能作为内部类存在,不然有可能会导致内存泄漏。为什么其他内部类不会呢?

通过java语法我们知道:匿名内部类持有外部类的对象 比如这个,handler是持有HandlerActivity的,不然也不能够调用到其中的方法,而系统是直接帮我们省略了HandlerActivity.this部分的 这就表示** Handler ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存**

image.png

首先我们知道,一个message是通过handler发送的,然后MessageQueue会保存 也就是说 MessageQueue ---持有--> message

接着我们再看看handler#enqueueMessage,我认为红框就是造成内存泄漏的最主要原因,我们通过代码可以看到 message.traget = this 这就意味着 message ---持有--> Handler对象

image.png

将三条链路拼接在一起 MessageQueue ---持有--> message ---持有--> Handler对象 ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存

当Handler发送了一个延迟10s的message。但是5s的时候,Activity销毁了。 此时的message是没有人处理的,即使他已经从MessageQueue扔出去了,但是Activity销毁了没人接收,也就是说这个message一只存在,则上面的这条链路是一只存在的。所以这持有的大量内存一直没人处理,虚拟机也会认为你这块内存是被持有的,他不会回收,就这样造成了内存泄漏。

所以说,Handler的内存泄漏,是说是因为匿名内部类是不够全面的

4.子线程中可以new Handler吗?

答案是可以的。 主线程和子线程都是线程,凭啥子线程不行呢,而且看了这么多代码也没看到什么地方必须要做主线程执行的方法。

下面用一段代码演示一下怎么在子线程创建Handler 首先要自定义自己的线程,在线程中创建出自己的Looper image.png

然后再将子线程的Looper传给Handler,这样创建的Handler就是子线程的了 image.png

但是这样写会有问题吗,显然是有的 我们知道子线程是异步的,而在子线程生成和获取Looper,你怎么知道他什么时候能创建好,怎么知道在Handler创建时,Looper是有值的呢?这一下变成了线程同步问题了,很简单,线程同步就加锁呗。实际上,系统已经写好了一个能在子线程创建Handler的 HandlerThread

可以看到总体还是和我们自己写的差不多的,不过在自己获取Looper和暴露给外界获取Looper加上了锁 也就是说,如果我们在looper还没创建出来时调用getLooper会执行wait(),释放锁且等待 直到run方法拿到锁之后,获取到Looper后去notiftAll()唤醒他 这样就能保证在Handler创建时,Looper是一定有的

image.png

5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?

我们知道Looper会帮我们在MessageQueue里面取消息,当MessageQueue没有消息了,Looper会做什么呢

首先看到获取消息的next()方法,他会调用到native层的方法nativePollOnce,当nativePollOnce取不到消息时,他就会让线程等待

image.png

所以此时的Looper.loop()方法中,系统也提示我们,会在这里阻塞住 而Looper.loop()是在子线程的run中运行的,要是一直没消息,他就会一直阻塞,run方法一直没办法结束,线程也没办法释放,就会造成内存泄露了

image.png

所以Looper给我们提供了一个方法quitSafely,而他会调用到MessageQueue的方法

image.png

他会让mQuitting = true;,接着清除message,接着nativeWake, 这与nativePollOnce是一对的,他会唤醒nativePollOnce继续执行

image.png

所以quitSafely后,next()方法会继续,因为msg = null,mQuitting = true,导致next()直接返回 null

image.png

然后再看调用next()方法的Looper.loop(),msg为null后直接return,for循环退出,loop方法也结束了。这样线程也能得到释放了

image.png

6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?

我们知道Looper创建时,会创建一个MessageQueue,且是唯一对应的 这也就说明一个Thread,Looper,MessageQueue都是唯一对应的关系 image.png

那么在添加消息时,synchronized (this) 的this 就是MessageQueue,而根据对应关系,这里加锁,其实就等于锁住了当前线程。就一个线程内算多个Handler同时添加消息,他们也会被锁限制,从而保证了消息添加的有序性,取消息同理

image.png

7.我们使用Message时应该如何创建它

不知道你们有没有人使用new Message()去创建消息。虽然是可以的,但是如果疯狂的new Message,你每new一个,就占用一块内存,会占用大量的内存和内存碎片

系统也提供了新建Message的方法,发现还是new Message(),那又有什么不同呢。 不同的就是sPool,他也是一个Message变量

image.png

我们回到Looper,没处理完一个消息后,他会调用Message的方法

image.png

而这个方法就是将当前的Message的所有参数清空,变成一个空的Message对象,然后放到sPool中去。等你一下需要Message变量时,他就可以重复里面

image.png

收起阅读 »

Android不使用反射完成LiveDataBus

LiveDataBus大家都很熟悉了,网上也有很多通过反射实现的LiveDataBus。但是通过反射实现的代码比较混乱,也比较难以理解。这里给出一版通过代码实现的。更加的简洁优雅~首先来看一下LiveData原理一般我们都是这样使用的,创建一个LiveData...
继续阅读 »

LiveDataBus大家都很熟悉了,网上也有很多通过反射实现的LiveDataBus。但是通过反射实现的代码比较混乱,也比较难以理解。这里给出一版通过代码实现的。更加的简洁优雅~

首先来看一下LiveData原理

一般我们都是这样使用的,创建一个LiveData去发送数据,在你想观察的地方去注册。这样只要数据发射,你就能拿到你想要的数据了。 

下面就是你再使用红框语句时的调用流程 

先进入 observe 方法看一看

 这样我们创建的LifecycleBoundObserver(observe方法中的new Observer)就和宿主(observe方法中传入的this) 建立了联系。

所以宿主每次生命周期的变化都会调用到 LifecycleBoundObserver的onStateChanged 而从代码中也可以看到,在宿主生命周期是DESTROYED时,会主动移除掉当前mObserver,完成自动反注册,这里注意要把mObservers 和 mObserver分清楚

这里有几点需要注意一下 1.LiveData中的mObservers是一个Map,还有一个mVersion字段默认等于 -1 

2.LifecycleBoundObserver 继承 ObserverWrapper 里面有mObserver,其实就是保存自己 还有一个mLastVersion字段,默认等于 -1 

接下来继续进入activeStateChanged方法,其他方法不多解释。 这里直接进入dispatchingValue 可以看到dispatchingValue不管走哪边都会进入considerNotify 

接下来看considerNotify 

看到这里我相信你已经知道,我们为啥能再onChanged拿到数据了

    viewModel.liveDataObject.observe(this, new Observer<Bean>() {
@Override
public void onChanged(Bean data) {
接收数据
}
});

接下来看一下,postValue和setValue

可以看到setValue是有注解MainThread的,表示只能在主线程中使用
而postValue没有,把某事件抛到主线程去了 

再来来看一下,postValue在切换到主线程中都干了些啥,我们发现他的Runnable中的方法,最终还是执行了setValue。 

所以这样看 postValue只不过是可以在子线程执行,但是消息发送最终还是要到主线程,且执行setValue 而setValue就只能在主线程执行了

在执行了setValue或者postValue后,mVersion+1,接着直接进入到considerNotify 

黏性事件怎么来的?

为了造成黏性事件,我再注册观察者之前就将数据发送出去,然后通过按钮点击再去注册一个观察者,我们能发现,即使是之前发送的数据,仍然能够接受得到,这就是黏性事件。 

造成的原因就是mLastVersion 和 mVersion

实现自己的LiveDataBus

LiveData基本的都了解过了,接下来自己实现一个,既可以接受黏性事件,又可以接受普通事件。

怎么控制黏性事件

其实原本的代码就是可以发送事件的,只不过不能自由的控制黏性事件 如果我们能用一个变量去标志就好了,比如这样标志一个receiveSticky变量 为true就是接受黏性事件,那么调用方法发送数据
为false的话就会,跳过此方法

if (observer.mLastVersion >= mVersion) { 
return;
}
observer.mLastVersion = mVersion;
if(receiveSticky){
observer.mObserver.onChanged((T) mData);
} else {
// 处理普通事件
}

在假设我们能直接在源码添加这个字段的话,那这个receiveSticky从哪里来呢?

1.发送者的角度:从 postValue 和 setValue 入手

比如改写成 postValue(data,receiveSticky) 这样有个弊端,这样只能统一发送黏性或者非黏性,这样如果多个宿主监听同一个消息,而有些需要黏性,有些不需要,这样就很难控制

2.从接收者的角度:从observer入手

我们知道我们传入的observer,在包装成LifecycleBoundObserver后,才有mLastVersion。那我们可以参考一下这种思路

比如:LifecycleBoundObserver包装一下,有了mLastVersion
那么:我们将传入的Observer也包装一层,在创建的时候传入receiveSticky就好了
就像这样:(当然这不是完整版,这只是记录一下思路)

怎么保证接收的是同一个事件

        liveData.postValue
viewModel.liveData.observer(this , Observer {

})

一般我们都是这样发送接收的,这个LiveData都是同一个才能接收同一份数据

所以我们也必须在LiveDataBus保证是同一个LiveData才行。

还是同样的思路,要区分LiveData,就给LiveData加名字就行了呗

那我就再给LiveData包装一层,让调用者传入名字去生成

生成完了就保存下来,以后就用名字去找到对应的LiveData

就好比:

那么他的用法就是这样的:接收消息的有点过于复杂了。 

既然observer是LiveData里面的方法 而每次发送消息时间都是StickyObserver(sticky, Observer()) 这样我们就可以在我们的包装类中去复写一下observer,比如: 

这样发送接收数据就会变成,比之前稍微好一些 

如何解决普通事件的接收

在上面我们其实没对普通事件做处理 我们通过sticky能判断接不接受黏性事件 但是我们不知道在我们注册之前,有没有消息事件发送

override fun onChanged(t: T) {
if (sticky) {
observer.onChanged(t)
} else {
// 普通事件
}
}

回想一下,黏性事件是怎么产生的?

简单的认为,observer.mLastVersion(Observer的) < mVersion(LiveData的) 就会产生黏性事件

所以我们也可以模仿一下,弄两个变量去判断 

在我们的LiveDate中 

在我们的observe中会被改写成这样 

所以显然不能完全完全按照源码照抄

那黏性事件之所以会被发送出去

就是在StickyObserver初始化时mLastVersionmLiveDataVersion没对齐,

导致if (mLastVersion >= stickyLiveData.mLiveDataVersion) {} 没进入

所以进入if条件就有黏性事件,所以我们要改成这样

代码

object LiveDataBus {

// LiveDataBus.with<String>("TestLiveDataBus").postStickyData("测试!")
// LiveDataBus.with<String>("TestLiveDataBus") .observerSticky(this, false) {
//
// }

private val mStickyMap = ConcurrentHashMap<String, StickyLiveData<*>>()

fun <T> with(eventName: String): StickyLiveData<T> {
var stickyLiveData = mStickyMap[eventName]
if (stickyLiveData == null) {
stickyLiveData = StickyLiveData<T>(eventName)
mStickyMap[eventName] = stickyLiveData
}

return stickyLiveData as StickyLiveData<T>
}


/**
* 将发射出去的LiveData包装一下,再做一些数据保存
*/
class StickyLiveData<T>(private var eventName: String) : LiveData<T>() {

var mLiveDataVersion = 0
var mStickyData: T? = null

fun setStickyData(stickyData: T) {
mStickyData = stickyData
setValue(stickyData)
}

fun postStickyData(stickyData: T) {
mStickyData = stickyData
postValue(stickyData)
}

override fun setValue(value: T) {
mLiveDataVersion++
super.setValue(value)
}

override fun postValue(value: T) {
super.postValue(value)
}

fun observerSticky(owner: LifecycleOwner, sticky: Boolean, observer: Observer<in T>) {
// 移除自己保存的StickyLiveData
owner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
mStickyMap.remove(eventName)
}
})

super.observe(owner, StickyObserver(this, sticky, observer))
}

/**
* 重写LiveData的observer,把传入的observer包装一下
*/
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
observerSticky(owner,false, observer)
}
}

class StickyObserver<T>(
private val stickyLiveData: StickyLiveData<T>,
private val sticky: Boolean,
private val observer: Observer<in T>
) : Observer<T> {

/**
* 打个比方:
* 一条数据,名称为TestName,
* 对应一个 StickyLiveData, 也就对应一个version, 初始的值为0,且这个可以复用
* 且会创建StickyObserver,对应一个 mLastVersion, 初始的值为0
*
* 如果 StickyLiveData#version 和 StickyObserver#mLastVersion 没有对齐
* LastVersion < version --> 直接发送数据,就会产生黏性事件
*
* 源码就是这样没对齐,所以无法控制黏性事件
*
* 因为源码的流程
* 将传入的observer包装成LifecycleBoundObserver(继承ObserverWrapper)会将传入的observer做保存和保存在hashMap
* 最后在considerNotify遍历hashMap,活跃的观察者会调用observer.onChanged(t)去发送数据
*
* 所以这里把传入的observer包装成StickyObserver 进入源码后 --> 再变成LifecycleBoundObserver
* 所以最终发送数据会调用StickyObserver的onChanged 就可以做黏性事件的处理了
*
*/
private var mLastVersion = stickyLiveData.mLiveDataVersion

override fun onChanged(t: T) {

if (mLastVersion >= stickyLiveData.mLiveDataVersion) {
if (sticky && stickyLiveData.mStickyData != null) {
observer.onChanged(stickyLiveData.mStickyData)
}
return
}
observer.onChanged(t)
}
}


}

收起阅读 »

Jetpack Compose 自定义 Loading

自学Jetpack Compose 半月有余了,写了一个Loading加载动效效果图实现思路拆分将正方形均分为4份 确定4个符号的中心点位置BoxWithConstraints(modifier = modifier) {    val ...
继续阅读 »

自学Jetpack Compose 半月有余了,写了一个Loading加载动效

效果图

loading02.gif

实现思路拆分

  1. 将正方形均分为4份 确定4个符号的中心点位置

image.png

BoxWithConstraints(modifier = modifier) {
   val circleSizeDp = minOf(maxWidth, maxHeight)
   val density = LocalDensity.current.density
   val circleSizePx = circleSizeDp.value * density
   //均分4份
   val radius = circleSizePx / 4
//right 和 bottom x,y
   val centerOffset = radius * 3

//加号中心点
   var plusOffset by remember { mutableStateOf(Offset(radius, radius)) }
//减号中心点
   var minusOffset by remember { mutableStateOf(Offset(centerOffset, radius)) }
//乘号中心点
   var timesOffset by remember { mutableStateOf(Offset(centerOffset, centerOffset)) }
//除号中心点
   var divOffset by remember { mutableStateOf(Offset(radius, centerOffset)) }
 
}  
  1. 根据4个符号的中心点绘制符号
     //符号长度
val offset = radius / 2 + 15.dp.value
Canvas(modifier = modifier.requiredSize(size = circleSizeDp)) {
//加号
drawLine(
color = lineColor,
start = Offset(plusOffset.x - offset, plusOffset.y),
end = Offset(plusOffset.x + offset, plusOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)

drawLine(
color = lineColor,
start = Offset(plusOffset.x, plusOffset.y - offset),
end = Offset(plusOffset.x, plusOffset.y + offset),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)

//减号
drawLine(
color = lineColor,
start = Offset(minusOffset.x - offset, minusOffset.y),
end = Offset(minusOffset.x + offset, minusOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
//乘号
rotate(degrees = 45F, pivot = timesOffset) {
drawLine(
color = lineColor,
start = Offset(timesOffset.x - offset, timesOffset.y),
end = Offset(timesOffset.x + offset, timesOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
}
rotate(degrees = 135F, pivot = timesOffset) {
drawLine(
color = lineColor,
start = Offset(timesOffset.x - offset, timesOffset.y),
end = Offset(timesOffset.x + offset, timesOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
}
//除号
drawLine(
color = lineColor,
start = Offset(divOffset.x - offset, divOffset.y),
end = Offset(divOffset.x + offset, divOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
//除法2个圆点
drawCircle(
color = lineColor,
style = Fill,
radius = circleRadius,
center = Offset(divOffset.x, divOffset.y - radius / 3)
)
drawCircle(
color = lineColor,
style = Fill,
radius = circleRadius,
center = Offset(divOffset.x, divOffset.y + radius / 3)
)
}

静态绘制效果
image.png

  1. 使用动画动起来

根据4个符号的中心点 构成一个正方形,每次偏移是正方形的边长

image.png

使用rememberInfiniteTransition() 无限循环动画 不断执行0到正方形的边长的动画运算 不断改变4个符号的中心点位置

//移动长度
val animateSize = radius * 2
//记录旋转次数
var currentCount by remember { mutableStateOf(0) }
//rememberInfiniteTransition() 无限动画
val animateValue by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = animateSize,
// keyframes 分时间分段计算返回
// LinearEasing 平滑过渡
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 800
0f at 80 with LinearEasing
0.1f * animateSize at 150 with LinearEasing
0.2f * animateSize at 200 with LinearEasing
0.3f * animateSize at 250 with LinearEasing
0.4f * animateSize at 300 with LinearEasing
0.5f * animateSize at 400 with LinearEasing
0.6f * animateSize at 500 with LinearEasing
0.7f * animateSize at 600
0.8f * animateSize at 700
0.9f * animateSize at 750
animateSize at 800
},
repeatMode = RepeatMode.Restart
)
)
//监听动画结果变化 对4个断
LaunchedEffect(animateValue) {
//根据animateValue ==0 来判断 动画的每次重新执行(无奈、没有相关监听接口)
if (animateValue == 0f) {
//每次重新开始就累加1
currentCount += 1
if (currentCount > 4) {
currentCount = 1
}
}
val plus = radius + animateValue
val minus = centerOffset - animateValue
// 根据 currentCount 标记出动画运行到哪个阶段
when (currentCount) {
1 -> {//加号从左往右
plusOffset = Offset(plus, radius)
minusOffset = Offset(centerOffset, plus)

timesOffset = Offset(minus, centerOffset)
divOffset = Offset(radius, minus)
}
2 -> {//加号从右往下
plusOffset = Offset(centerOffset, plus)
minusOffset = Offset(minus, centerOffset)

timesOffset = Offset(radius, minus)
divOffset = Offset(plus, radius)
}
3 -> {//加号从下往左
plusOffset = Offset(minus, centerOffset)
minusOffset = Offset(radius, minus)

timesOffset = Offset(plus, radius)
divOffset = Offset(centerOffset, plus)
}
4 -> {
plusOffset = Offset(radius, minus)
minusOffset = Offset(plus, radius)

timesOffset = Offset(centerOffset, plus)
divOffset = Offset(minus, centerOffset)
}
}
}

动画实现这个过程有点痛苦,目前Compose 在对动画细粒度监听上没有更好的支持,rememberInfiniteTransition()是无限循环动画,但是没有对动画Restart、 start、 end暴露监听接口 同时差值器提供的也不能满足需求,只能通过keyframes 去一点一点的计算出来 如果有工友有好的方式 还望不要吝啬告知 到这里就基本上完成了

loading02.gif

扩展

使用 ModifierdrawWithContent实现未读消息红点提示

fun Modifier.redPoint(num: String): Modifier = drawWithContent {
drawContent()
drawIntoCanvas {
val padding = 6.dp.toPx()
val topPadding = 3.dp.toPx()

val paint = Paint().apply {
color = Color.Red
}
val paintTextSize= 14.sp.toPx()
//绘制文本用FrameworkPaint
val textPaint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
isDither = true
color=Color.White.toArgb()
textSize = paintTextSize
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
textAlign = android.graphics.Paint.Align.CENTER
}
//测量出文本的宽度
val textWidth = textPaint.measureText(num)

val radius =20.dp.toPx()
val offset=(textWidth+padding*2)
//绘制背景
it.drawRoundRect(
left = size.width-offset,
top = 0f,
right = size.width,
bottom = radius,
radiusX= 10.dp.toPx(),
radiusY= 10.dp.toPx(),
paint = paint
)
//绘制文本
it.nativeCanvas.drawText(num, size.width-offset/2, radius-(radius-paintTextSize)/2-topPadding, textPaint)
}
}

调用

@Composable
fun ImageDemo() {
Image(
painter = painterResource(id = R.drawable.message),
contentDescription = "",
modifier = Modifier
.size(width = 56.dp, height = 56.dp)
.redPoint("99"),
contentScale = ContentScale.FillBounds,
alignment = Alignment.CenterEnd,
)
}

image.png

收起阅读 »

Swift 可选链

可选链(Optional Chaining)是一种可以请求和调用属性、方法和子脚本的过程,用于请求或调用的目标可能为nil。可选链返回两个值:如果目标有值,调用就会成功,返回该值如果目标为nil,调用将返回nil多次请求或调用可以被链接成一个链,如果任意一个节...
继续阅读 »

可选链(Optional Chaining)是一种可以请求和调用属性、方法和子脚本的过程,用于请求或调用的目标可能为nil。

可选链返回两个值:

  • 如果目标有值,调用就会成功,返回该值

  • 如果目标为nil,调用将返回nil

多次请求或调用可以被链接成一个链,如果任意一个节点为nil将导致整条链失效。


可选链可替代强制解析

通过在属性、方法、或下标脚本的可选值后面放一个问号(?),即可定义一个可选链。

可选链 '?'感叹号(!)强制展开方法,属性,下标脚本可选链
? 放置于可选值后来调用方法,属性,下标脚本! 放置于可选值后来调用方法,属性,下标脚本来强制展开值
当可选为 nil 输出比较友好的错误信息当可选为 nil 时强制展开执行错误

使用感叹号(!)可选链实例

class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

let john = Person()

//将导致运行时错误
let roomCount = john.residence!.numberOfRooms

以上程序执行输出结果为:

fatal error: unexpectedly found nil while unwrapping an Optional value

想使用感叹号(!)强制解析获得这个人residence属性numberOfRooms属性值,将会引发运行时错误,因为这时没有可以供解析的residence值。

使用问号(?)可选链实例

class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

let john = Person()

// 链接可选residence?属性,如果residence存在则取回numberOfRooms的值
if let roomCount = john.residence?.numberOfRooms {
print("John 的房间号为 \(roomCount)。")
} else {
print("不能查看房间号")
}

以上程序执行输出结果为:

不能查看房间号

因为这种尝试获得numberOfRooms的操作有可能失败,可选链会返回Int?类型值,或者称作"可选Int"。当residence是空的时候(上例),选择Int将会为空,因此会出现无法访问numberOfRooms的情况。

要注意的是,即使numberOfRooms是非可选Int(Int?)时这一点也成立。只要是通过可选链的请求就意味着最后numberOfRooms总是返回一个Int?而不是Int。


为可选链定义模型类

你可以使用可选链来多层调用属性,方法,和下标脚本。这让你可以利用它们之间的复杂模型来获取更底层的属性,并检查是否可以成功获取此类底层属性。

实例

定义了四个模型类,其中包括多层可选链:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

通过可选链调用方法

你可以使用可选链的来调用可选值的方法并检查方法调用是否成功。即使这个方法没有返回值,你依然可以使用可选链来达成这一目的。

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()


if ((john.residence?.printNumberOfRooms()) != nil) {
print("输出房间号")
} else {
print("无法输出房间号")
}

以上程序执行输出结果为:

无法输出房间号

使用if语句来检查是否能成功调用printNumberOfRooms方法:如果方法通过可选链调用成功,printNumberOfRooms的隐式返回值将会是Void,如果没有成功,将返回nil。


使用可选链调用下标脚本

你可以使用可选链来尝试从下标脚本获取值并检查下标脚本的调用是否成功,然而,你不能通过可选链来设置下标脚本。

实例1

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()
if let firstRoomName = john.residence?[0].name {
print("第一个房间名 \(firstRoomName).")
} else {
print("无法检索到房间")
}

以上程序执行输出结果为:

无法检索到房间

在下标脚本调用中可选链的问号直接跟在 john.residence 的后面,在下标脚本括号的前面,因为 john.residence 是可选链试图获得的可选值。

实例2

实例中创建一个 Residence 实例给 john.residence,且在他的 rooms 数组中有一个或多个 Room 实例,那么你可以使用可选链通过 Residence 下标脚本来获取在 rooms 数组中的实例了:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()
let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

let johnsAddress = Address()
johnsAddress
.buildingName = "The Larches"
johnsAddress
.street = "Laurel Street"
john
.residence!.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
print("John 所在的街道是 \(johnsStreet)。")
} else {
print("无法检索到地址。 ")
}

以上程序执行输出结果为:

John 所在的街道是 Laurel Street

通过可选链接调用来访问下标

通过可选链接调用,我们可以用下标来对可选值进行读取或写入,并且判断下标调用是否成功。

实例

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("第一个房间名为\(firstRoomName)")
} else {
print("无法检索到房间")
}

以上程序执行输出结果为:

第一个房间名为客厅

访问可选类型的下标

如果下标返回可空类型值,比如Swift中Dictionary的key下标。可以在下标的闭合括号后面放一个问号来链接下标的可空返回值:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores
["Dave"]?[0] = 91
testScores
["Bev"]?[0]++
testScores
["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]

上面的例子中定义了一个testScores数组,包含了两个键值对, 把String类型的key映射到一个整形数组。

这个例子用可选链接调用把"Dave"数组中第一个元素设为91,把"Bev"数组的第一个元素+1,然后尝试把"Brian"数组中的第一个元素设为72。

前两个调用是成功的,因为这两个key存在。但是key"Brian"在字典中不存在,所以第三个调用失败。


连接多层链接

你可以将多层可选链连接在一起,可以掘取模型内更下层的属性方法和下标脚本。然而多层可选链不能再添加比已经返回的可选值更多的层。

如果你试图通过可选链获得Int值,不论使用了多少层链接返回的总是Int?。 相似的,如果你试图通过可选链获得Int?值,不论使用了多少层链接返回的总是Int?。

实例1

下面的例子试图获取john的residence属性里的address的street属性。这里使用了两层可选链来联系residence和address属性,它们两者都是可选类型:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

if let johnsStreet = john.residence?.address?.street {
print("John 的地址为 \(johnsStreet).")
} else {
print("不能检索地址")
}

以上程序执行输出结果为:

不能检索地址

实例2

如果你为Address设定一个实例来作为john.residence.address的值,并为address的street属性设定一个实际值,你可以通过多层可选链来得到这个属性值。

class Person {
var residence: Residence?
}

class Residence {

var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
get{
return rooms[i]
}
set {
rooms
[i] = newValue
}
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

class Room {
let name: String
init
(name: String) { self.name = name }
}

class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
john
.residence?[0] = Room(name: "浴室")

let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("第一个房间是\(firstRoomName)")
} else {
print("无法检索房间")
}

以上实例输出结果为:

第一个房间是客厅

对返回可选值的函数进行链接

我们还可以通过可选链接来调用返回可空值的方法,并且可以继续对可选值进行链接。

实例

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

if john.residence?.printNumberOfRooms() != nil {
print("指定了房间号)")
} else {
print("未指定房间号")
}

以上程序执行输出结果为:

未指定房间号
收起阅读 »

Swift 析构过程

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类类型。析构过程原理Swift 会自动释放不再需要的实例以释放资源。Swift 通过自动引用计数(ARC)处理实例的内存管理。通...
继续阅读 »

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类类型。


析构过程原理

Swift 会自动释放不再需要的实例以释放资源。

Swift 通过自动引用计数(ARC)处理实例的内存管理。

通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。

例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前关闭该文件。

语法

在类的定义中,每个类最多只能有一个析构函数。析构函数不带任何参数,在写法上不带括号:

deinit {
// 执行析构过程
}

实例

var counter = 0;  // 引用计数器
class BaseClass {
init
() {
counter
+= 1;
}
deinit
{
counter
-= 1;
}
}

var show: BaseClass? = BaseClass()
print(counter)
show
= nil
print(counter)

以上程序执行输出结果为:

1
0

当 show = nil 语句执行后,计算器减去 1,show 占用的内存就会释放。

var counter = 0;  // 引用计数器

class BaseClass {
init
() {
counter
+= 1;
}

deinit
{
counter
-= 1;
}
}

var show: BaseClass? = BaseClass()

print(counter)
print(counter)

以上程序执行输出结果为:

1
1
收起阅读 »

Swift 构造过程

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。Swift 构造函数使用 init() 方法。与 Objective-C 中的构造器不同,Swift 的构造器无需返回...
继续阅读 »

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。

Swift 构造函数使用 init() 方法。

与 Objective-C 中的构造器不同,Swift 的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。

类实例也可以通过定义析构器(deinitializer)在类实例释放之前执行清理内存的工作。


存储型属性的初始赋值

类和结构体在实例创建时,必须为所有存储型属性设置合适的初始值。

存储属性在构造器中赋值时,它们的值是被直接设置的,不会触发任何属性观测器。

存储属性在构造器中赋值流程:

  • 创建初始值。

  • 在属性定义中指定默认属性值。

  • 初始化实例,并调用 init() 方法。


构造器

构造器在创建某特定类型的新实例时调用。它的最简形式类似于一个不带任何参数的实例方法,以关键字init命名。

语法

init()
{
// 实例化后执行的代码
}

实例

以下结构体定义了一个不带参数的构造器 init,并在里面将存储型属性 length 和 breadth 的值初始化为 6 和 12:

struct rectangle {
var length: Double
var breadth: Double
init
() {
length
= 6
breadth
= 12
}
}
var area = rectangle()
print("矩形面积为 \(area.length*area.breadth)")

以上程序执行输出结果为:

矩形面积为 72.0

默认属性值

我们可以在构造器中为存储型属性设置初始值;同样,也可以在属性声明时为其设置默认值。

使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型。

以下实例我们在属性声明时为其设置默认值:

struct rectangle {
    
// 设置默认值
var length = 6
var breadth = 12
}
var area = rectangle()
print("矩形的面积为 \(area.length*area.breadth)")

以上程序执行输出结果为:

矩形面积为 72

构造参数

你可以在定义构造器 init() 时提供构造参数,如下所示:

struct Rectangle {
var length: Double
var breadth: Double
var area: Double

init
(fromLength length: Double, fromBreadth breadth: Double) {
self.length = length
self.breadth = breadth
area
= length * breadth
}

init
(fromLeng leng: Double, fromBread bread: Double) {
self.length = leng
self.breadth = bread
area
= leng * bread
}
}

let ar = Rectangle(fromLength: 6, fromBreadth: 12)
print("面积为: \(ar.area)")

let are = Rectangle(fromLeng: 36, fromBread: 12)
print("面积为: \(are.area)")

以上程序执行输出结果为:

面积为: 72.0
面积为: 432.0

内部和外部参数名

跟函数和方法参数相同,构造参数也存在一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。

然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。所以在调用构造器时,主要通过构造器中的参数名和类型来确定需要调用的构造器。

如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名。

struct Color {
let red, green, blue: Double
init
(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init
(white: Double) {
red
= white
green
= white
blue
= white
}
}

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)

print("red 值为: \(magenta.red)")
print("green 值为: \(magenta.green)")
print("blue 值为: \(magenta.blue)")

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let halfGray = Color(white: 0.5)
print("red 值为: \(halfGray.red)")
print("green 值为: \(halfGray.green)")
print("blue 值为: \(halfGray.blue)")

以上程序执行输出结果为:

red 值为: 1.0
green
值为: 0.0
blue
值为: 1.0
red
值为: 0.5
green
值为: 0.5
blue
值为: 0.5

没有外部名称参数

如果你不希望为构造器的某个参数提供外部名字,你可以使用下划线_来显示描述它的外部名。

struct Rectangle {
var length: Double

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}
//不提供外部名字
init
(_ area: Double) {
length
= area
}
}

// 调用不提供外部名字
let rectarea = Rectangle(180.0)
print("面积为: \(rectarea.length)")

// 调用不提供外部名字
let rearea = Rectangle(370.0)
print("面积为: \(rearea.length)")

// 调用不提供外部名字
let recarea = Rectangle(110.0)
print("面积为: \(recarea.length)")

以上程序执行输出结果为:

面积为: 180.0
面积为: 370.0
面积为: 110.0

可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性,你都需要将它定义为可选类型optional type(可选属性类型)。

当存储属性声明为可选时,将自动初始化为空 nil。

struct Rectangle {
var length: Double?

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}

init
(_ area: Double) {
length
= area
}
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)

构造过程中修改常量属性

只要在构造过程结束前常量的值能确定,你可以在构造过程中的任意时间点修改常量属性的值。

对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

尽管 length 属性现在是常量,我们仍然可以在其类的构造器中设置它的值:

struct Rectangle {
let length: Double?

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}

init
(_ area: Double) {
length
= area
}
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)

默认构造器

默认构造器将简单的创建一个所有属性值都设置为默认值的实例:

以下实例中,ShoppingListItem类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个可以为所有属性设置默认值的默认构造器

class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()


print("名字为: \(item.name)")
print("数理为: \(item.quantity)")
print("是否付款: \(item.purchased)")

以上程序执行输出结果为:

名字为: nil
数理为: 1
是否付款: false

结构体的逐一成员构造器

如果结构体对所有存储型属性提供了默认值且自身没有提供定制的构造器,它们能自动获得一个逐一成员构造器。

我们在调用逐一成员构造器时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

下面例子中定义了一个结构体 Rectangle,它包含两个属性 length 和 breadth。Swift 可以根据这两个属性的初始赋值100.0 、200.0自动推导出它们的类型Double。

struct Rectangle {
var length = 100.0, breadth = 200.0
}
let area = Rectangle(length: 24.0, breadth: 32.0)

print("矩形的面积: \(area.length)")
print("矩形的面积: \(area.breadth)")

由于这两个存储型属性都有默认值,结构体 Rectangle 自动获得了一个逐一成员构造器 init(width:height:)。 你可以用它来为 Rectangle 创建新的实例。

以上程序执行输出结果为:

矩形的面积: 24.0
矩形的面积: 32.0

值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。

以下实例中,Rect 结构体调用了 Size 和 Point 的构造过程:

struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}

struct Rect {
var origin = Point()
var size = Size()
init
() {}
init
(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init
(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}


// origin和size属性都使用定义时的默认值Point(x: 0.0, y: 0.0)和Size(width: 0.0, height: 0.0):
let basicRect = Rect()
print("Size 结构体初始值: \(basicRect.size.width, basicRect.size.height) ")
print("Rect 结构体初始值: \(basicRect.origin.x, basicRect.origin.y) ")

// 将origin和size的参数值赋给对应的存储型属性
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size
: Size(width: 5.0, height: 5.0))

print("Size 结构体初始值: \(originRect.size.width, originRect.size.height) ")
print("Rect 结构体初始值: \(originRect.origin.x, originRect.origin.y) ")


//先通过center和size的值计算出origin的坐标。
//然后再调用(或代理给)init(origin:size:)构造器来将新的origin和size值赋值到对应的属性中
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size
: Size(width: 3.0, height: 3.0))

print("Size 结构体初始值: \(centerRect.size.width, centerRect.size.height) ")
print("Rect 结构体初始值: \(centerRect.origin.x, centerRect.origin.y) ")

以上程序执行输出结果为:

Size 结构体初始值: (0.0, 0.0) 
Rect 结构体初始值: (0.0, 0.0)
Size 结构体初始值: (5.0, 5.0)
Rect 结构体初始值: (2.0, 2.0)
Size 结构体初始值: (3.0, 3.0)
Rect 结构体初始值: (2.5, 2.5)

构造器代理规则

值类型类类型
不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给本身提供的其它构造器。 你可以使用self.init在自定义的构造器中引用其它的属于相同值类型的构造器。它可以继承自其它类,这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。

类的继承和构造过程

Swift 提供了两种类型的类构造器来确保所有类实例中存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。

指定构造器便利构造器
类中最主要的构造器类中比较次要的、辅助型的构造器
初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。可以定义便利构造器来调用同一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入的实例。
每一个类都必须拥有至少一个指定构造器只在必要的时候为类提供便利构造器
Init(parameters) {
statements
}
convenience init(parameters) {
statements
}

指定构造器实例

class mainClass {
var no1 : Int // 局部存储变量
init
(no1 : Int) {
self.no1 = no1 // 初始化
}
}
class subClass : mainClass {
var no2 : Int // 新的子类存储变量
init
(no1 : Int, no2 : Int) {
self.no2 = no2 // 初始化
super.init(no1:no1) // 初始化超类
}
}

let res = mainClass(no1: 10)
let res2 = subClass(no1: 10, no2: 20)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")

以上程序执行输出结果为:

res 为: 10
res
为: 10
res
为: 20

便利构造器实例

class mainClass {
var no1 : Int // 局部存储变量
init
(no1 : Int) {
self.no1 = no1 // 初始化
}
}

class subClass : mainClass {
var no2 : Int
init
(no1 : Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 便利方法只需要一个参数
override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}
let res = mainClass(no1: 20)
let res2 = subClass(no1: 30, no2: 50)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")

以上程序执行输出结果为:

res 为: 20
res2
为: 30
res2
为: 50

构造器的继承和重载

Swift 中的子类不会默认继承父类的构造器。

父类的构造器仅在确定和安全的情况下被继承。

当你重写一个父类指定构造器时,你需要写override修饰符。

class SuperClass {
var corners = 4
var description: String {
return "\(corners) 边"
}
}
let rectangle = SuperClass()
print("矩形: \(rectangle.description)")

class SubClass: SuperClass {
override init() { //重载构造器
super.init()
corners
= 5
}
}

let subClass = SubClass()
print("五角型: \(subClass.description)")

以上程序执行输出结果为:

矩形: 4 
五角型: 5

指定构造器和便利构造器实例

接下来的例子将在操作中展示指定构造器、便利构造器和自动构造器的继承。

它定义了包含两个个类MainClass、SubClass的类层次结构,并将演示它们的构造器是如何相互作用的。

class MainClass {
var name: String

init
(name: String) {
self.name = name
}

convenience init
() {
self.init(name: "[匿名]")
}
}
let main = MainClass(name: "Runoob")
print("MainClass 名字为: \(main.name)")

let main2 = MainClass()
print("没有对应名字: \(main2.name)")

class SubClass: MainClass {
var count: Int
init
(name: String, count: Int) {
self.count = count
super.init(name: name)
}

override convenience init(name: String) {
self.init(name: name, count: 1)
}
}

let sub = SubClass(name: "Runoob")
print("MainClass 名字为: \(sub.name)")

let sub2 = SubClass(name: "Runoob", count: 3)
print("count 变量: \(sub2.count)")

以上程序执行输出结果为:

MainClass 名字为: Runoob
没有对应名字: [匿名]
MainClass 名字为: Runoob
count
变量: 3

类的可失败构造器

如果一个类,结构体或枚举类型的对象,在构造自身的过程中有可能失败,则为其定义一个可失败构造器。

变量初始化失败可能的原因有:

  • 传入无效的参数值。

  • 缺少某种所需的外部资源。

  • 没有满足特定条件。

为了妥善处理这种构造过程中可能会失败的情况。

你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init关键字后面加添问号(init?)。

实例

下例中,定义了一个名为Animal的结构体,其中有一个名为species的,String类型的常量属性。

同时该结构体还定义了一个,带一个String类型参数species的,可失败构造器。这个可失败构造器,被用来检查传入的参数是否为一个空字符串,如果为空字符串,则该可失败构造器,构建对象失败,否则成功。

struct Animal {
let species: String
init
?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}

//通过该可失败构造器来构建一个Animal的对象,并检查其构建过程是否成功
// someCreature 的类型是 Animal? 而不是 Animal
let someCreature = Animal(species: "长颈鹿")

// 打印 "动物初始化为长颈鹿"
if let giraffe = someCreature {
print("动物初始化为\(giraffe.species)")
}

以上程序执行输出结果为:

动物初始化为长颈鹿

枚举类型的可失败构造器

你可以通过构造一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。

实例

下例中,定义了一个名为TemperatureUnit的枚举类型。其中包含了三个可能的枚举成员(Kelvin,Celsius,和 Fahrenheit)和一个被用来找到Character值所对应的枚举成员的可失败构造器:

enum TemperatureUnit {
    
// 开尔文,摄氏,华氏
case Kelvin, Celsius, Fahrenheit
init
?(symbol: Character) {
switch symbol {
case "K":
self = .Kelvin
case "C":
self = .Celsius
case "F":
self = .Fahrenheit
default:
return nil
}
}
}


let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("这是一个已定义的温度单位,所以初始化成功。")
}

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("这不是一个已定义的温度单位,所以初始化失败。")
}

以上程序执行输出结果为:

这是一个已定义的温度单位,所以初始化成功。
这不是一个已定义的温度单位,所以初始化失败。

类的可失败构造器

值类型(如结构体或枚举类型)的可失败构造器,对何时何地触发构造失败这个行为没有任何的限制。

但是,类的可失败构造器只能在所有的类属性被初始化后和所有类之间的构造器之间的代理调用发生完后触发失败行为。

实例

下例子中,定义了一个名为 StudRecord 的类,因为 studname 属性是一个常量,所以一旦 StudRecord 类构造成功,studname 属性肯定有一个非nil的值。

class StudRecord {
let studname: String!
init
?(studname: String) {
self.studname = studname
if studname.isEmpty { return nil }
}
}
if let stname = StudRecord(studname: "失败构造器") {
print("模块为 \(stname.studname)")
}

以上程序执行输出结果为:

模块为 失败构造器

覆盖一个可失败构造器

就如同其它构造器一样,你也可以用子类的可失败构造器覆盖基类的可失败构造器。

者你也可以用子类的非可失败构造器覆盖一个基类的可失败构造器。

你可以用一个非可失败构造器覆盖一个可失败构造器,但反过来却行不通。

一个非可失败的构造器永远也不能代理调用一个可失败构造器。

实例

以下实例描述了可失败与非可失败构造器:

class Planet {
var name: String

init
(name: String) {
self.name = name
}

convenience init
() {
self.init(name: "[No Planets]")
}
}
let plName = Planet(name: "Mercury")
print("行星的名字是: \(plName.name)")

let noplName = Planet()
print("没有这个名字的行星: \(noplName.name)")

class planets: Planet {
var count: Int

init
(name: String, count: Int) {
self.count = count
super.init(name: name)
}

override convenience init(name: String) {
self.init(name: name, count: 1)
}
}

以上程序执行输出结果为:

行星的名字是: Mercury
没有这个名字的行星: [No Planets]

可失败构造器 init!

通常来说我们通过在init关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以使用通过在init后面添加惊叹号的方式来定义一个可失败构造器(init!)。实例如下:

struct StudRecord {
let stname: String

init
!(stname: String) {
if stname.isEmpty {return nil }
self.stname = stname
}
}

let stmark = StudRecord(stname: "Runoob")
if let name = stmark {
print("指定了学生名")
}

let blankname = StudRecord(stname: "")
if blankname == nil {
print("学生名为空")
}

以上程序执行输出结果为:

指定了学生名
学生名为空
收起阅读 »

Swift 继承

继承我们可以理解为一个类获取了另外一个类的方法和属性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。我们也可以为类中继承来的属性添加属性观察器。基类没有继承其它类的...
继续阅读 »

继承我们可以理解为一个类获取了另外一个类的方法和属性。

当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)

在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。

我们也可以为类中继承来的属性添加属性观察器。


基类

没有继承其它类的类,称之为基类(Base Class)。

以下实例中我们定义了基类 StudDetails ,描述了学生(stname)及其各科成绩的分数(mark1、mark2、mark3):

class StudDetails {
var stname: String!
var mark1: Int!
var mark2: Int!
var mark3: Int!
init
(stname: String, mark1: Int, mark2: Int, mark3: Int) {
self.stname = stname
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}
let stname = "swift"
let mark1 = 98
let mark2 = 89
let mark3 = 76

let sds = StudDetails(stname:stname, mark1:mark1, mark2:mark2, mark3:mark3);

print(sds.stname)
print(sds.mark1)
print(sds.mark2)
print(sds.mark3)

以上程序执行输出结果为:

swift
98
89
76

子类

子类指的是在一个已有类的基础上创建一个新的类。

为了指明某个类的超类,将超类名写在子类名的后面,用冒号(:)分隔,语法格式如下

class SomeClass: SomeSuperclass {
// 类的定义
}

实例

以下实例中我们定义了超类 StudDetails,然后使用子类 Tom 继承它:

class StudDetails
{
var mark1: Int;
var mark2: Int;

init
(stm1:Int, results stm2:Int)
{
mark1
= stm1;
mark2
= stm2;
}

func show
()
{
print("Mark1:\(self.mark1), Mark2:\(self.mark2)")
}
}

class Tom : StudDetails
{
init
()
{
super.init(stm1: 93, results: 89)
}
}

let tom = Tom()
tom
.show()

以上程序执行输出结果为:

Mark1:93, Mark2:89

重写(Overriding)

子类可以通过继承来的实例方法,类方法,实例属性,或下标脚本来实现自己的定制功能,我们把这种行为叫重写(overriding)。

我们可以使用 override 关键字来实现重写。

访问超类的方法、属性及下标脚本

你可以通过使用super前缀来访问超类的方法,属性或下标脚本。

重写访问方法,属性,下标脚本
方法super.somemethod()
属性super.someProperty()
下标脚本super[someIndex]

重写方法和属性

重写方法

在我们的子类中我们可以使用 override 关键字来重写超类的方法。

以下实例中我们重写了 show() 方法:

class SuperClass {
func show
() {
print("这是超类 SuperClass")
}
}

class SubClass: SuperClass {
override func show() {
print("这是子类 SubClass")
}
}

let superClass = SuperClass()
superClass
.show()

let subClass = SubClass()
subClass
.show()

以上程序执行输出结果为:

这是超类 SuperClass
这是子类 SubClass

重写属性

你可以提供定制的 getter(或 setter)来重写任意继承来的属性,无论继承来的属性是存储型的还是计算型的属性。

子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。所以你在重写一个属性时,必需将它的名字和类型都写出来。

注意点:

  • 如果你在重写属性中提供了 setter,那么你也一定要提供 getter。

  • 如果你不想在重写版本中的 getter 里修改继承来的属性值,你可以直接通过super.someProperty来返回继承来的值,其中someProperty是你要重写的属性的名字。

以下实例我们定义了超类 Circle 及子类 Rectangle, 在 Rectangle 类中我们重写属性 area:

class Circle {
var radius = 12.5
var area: String {
return "矩形半径 \(radius) "
}
}

// 继承超类 Circle
class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}

let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("Radius \(rect.area)")

以上程序执行输出结果为:

Radius 矩形半径 25.0  ,但现在被重写为 3

重写属性观察器

你可以在属性重写中为一个继承来的属性添加属性观察器。这样一来,当继承来的属性值发生改变时,你就会监测到。

注意:你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。

class Circle {
var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}

class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}


let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("半径: \(rect.area)")

class Square: Rectangle {
override var radius: Double {
didSet
{
print = Int(radius/5.0)+1
}
}
}


let sq = Square()
sq
.radius = 100.0
print("半径: \(sq.area)")
半径: 矩形半径为 25.0  ,但现在被重写为 3
半径: 矩形半径为 100.0 ,但现在被重写为 21

防止重写

我们可以使用 final 关键字防止它们被重写。

如果你重写了final方法,属性或下标脚本,在编译时会报错。

你可以通过在关键字class前添加final特性(final class)来将整个类标记为 final 的,这样的类是不可被继承的,否则会报编译错误。

final class Circle {
final var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}
class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}

let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("半径: \(rect.area)")

class Square: Rectangle {
override var radius: Double {
didSet
{
print = Int(radius/5.0)+1
}
}
}

let sq = Square()
sq
.radius = 100.0
print("半径: \(sq.area)")

由于以上实例使用了 final 关键字不允许重写,所以执行会报错:

error: var overrides a 'final' var
override var area: String {
^
note
: overridden declaration is here
var area: String {
^
error
: var overrides a 'final' var
override var radius: Double {
^
note
: overridden declaration is here
final var radius = 12.5
^
error
: inheritance from a final class 'Circle'
class Rectangle: Circle {
^
收起阅读 »

Swift 下标脚本

下标脚本 可以定义在类(Class)、结构体(structure)和枚举(enumeration)这些目标中,可以认为是访问对象、集合或序列的快捷方式,不需要再调用实例的特定的赋值和访问方法。举例来说,用下标脚本访问一个数组(Array)实例中的元素可以这样写...
继续阅读 »

下标脚本 可以定义在类(Class)、结构体(structure)和枚举(enumeration)这些目标中,可以认为是访问对象、集合或序列的快捷方式,不需要再调用实例的特定的赋值和访问方法。

举例来说,用下标脚本访问一个数组(Array)实例中的元素可以这样写 someArray[index] ,访问字典(Dictionary)实例中的元素可以这样写 someDictionary[key]。

对于同一个目标可以定义多个下标脚本,通过索引值类型的不同来进行重载,而且索引值的个数可以是多个。


下标脚本语法及应用

语法

下标脚本允许你通过在实例后面的方括号中传入一个或者多个的索引值来对实例进行访问和赋值。

语法类似于实例方法和计算型属性的混合。

与定义实例方法类似,定义下标脚本使用subscript关键字,显式声明入参(一个或多个)和返回类型。

与实例方法不同的是下标脚本可以设定为读写或只读。这种方式又有点像计算型属性的getter和setter:

subscript(index: Int) -> Int {
get {
// 用于下标脚本值的声明
}
set(newValue) {
// 执行赋值操作
}
}

实例 1

import Cocoa

struct subexample {
let decrementer: Int
subscript
(index: Int) -> Int {
return decrementer / index
}
}
let division = subexample(decrementer: 100)

print("100 除以 9 等于 \(division[9])")
print("100 除以 2 等于 \(division[2])")
print("100 除以 3 等于 \(division[3])")
print("100 除以 5 等于 \(division[5])")
print("100 除以 7 等于 \(division[7])")

以上程序执行输出结果为:

100 除以 9 等于 11
100 除以 2 等于 50
100 除以 3 等于 33
100 除以 5 等于 20
100 除以 7 等于 14

在上例中,通过 subexample 结构体创建了一个除法运算的实例。数值 100 作为结构体构造函数传入参数初始化实例成员 decrementer。

你可以通过下标脚本来得到结果,比如 division[2] 即为 100 除以 2。

实例 2

import Cocoa

class daysofaweek {
private var days = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "saturday"]
subscript
(index: Int) -> String {
get {
return days[index] // 声明下标脚本的值
}
set(newValue) {
self.days[index] = newValue // 执行赋值操作
}
}
}
var p = daysofaweek()

print(p[0])
print(p[1])
print(p[2])
print(p[3])

以上程序执行输出结果为:

Sunday
Monday
Tuesday
Wednesday

用法

根据使用场景不同下标脚本也具有不同的含义。

通常下标脚本是用来访问集合(collection),列表(list)或序列(sequence)中元素的快捷方式。

你可以在你自己特定的类或结构体中自由的实现下标脚本来提供合适的功能。

例如,Swift 的字典(Dictionary)实现了通过下标脚本对其实例中存放的值进行存取操作。在下标脚本中使用和字典索引相同类型的值,并且把一个字典值类型的值赋值给这个下标脚来为字典设值:

import Cocoa

var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs
["bird"] = 2

print(numberOfLegs)

以上程序执行输出结果为:

["ant": 6, "bird": 2, "cat": 4, "spider": 8]

上例定义一个名为numberOfLegs的变量并用一个字典字面量初始化出了包含三对键值的字典实例。numberOfLegs的字典存放值类型推断为Dictionary。字典实例创建完成之后通过下标脚本的方式将整型值2赋值到字典实例的索引为bird的位置中。


下标脚本选项

下标脚本允许任意数量的入参索引,并且每个入参类型也没有限制。

下标脚本的返回值也可以是任何类型。

下标脚本可以使用变量参数和可变参数。

一个类或结构体可以根据自身需要提供多个下标脚本实现,在定义下标脚本时通过传入参数的类型进行区分,使用下标脚本时会自动匹配合适的下标脚本实现运行,这就是下标脚本的重载

import Cocoa

struct Matrix {
let rows: Int, columns: Int
var print: [Double]
init
(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
print = Array(repeating: 0.0, count: rows * columns)
}
subscript
(row: Int, column: Int) -> Double {
get {
return print[(row * columns) + column]
}
set {
print[(row * columns) + column] = newValue
}
}
}
// 创建了一个新的 3 行 3 列的Matrix实例
var mat = Matrix(rows: 3, columns: 3)

// 通过下标脚本设置值
mat
[0,0] = 1.0
mat
[0,1] = 2.0
mat
[1,0] = 3.0
mat
[1,1] = 5.0

// 通过下标脚本获取值
print("\(mat[0,0])")
print("\(mat[0,1])")
print("\(mat[1,0])")
print("\(mat[1,1])")

以上程序执行输出结果为:

1.0
2.0
3.0
5.0

Matrix 结构体提供了一个两个传入参数的构造方法,两个参数分别是rows和columns,创建了一个足够容纳rows * columns个数的Double类型数组。为了存储,将数组的大小和数组每个元素初始值0.0。

你可以通过传入合适的row和column的数量来构造一个新的Matrix实例。

收起阅读 »

CSS 奇技淫巧 | 巧妙实现文字二次加粗再加边框

需求背景 - 文字的二次加粗 今天遇到这样一个有意思的问题: 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢? emm,不考虑兼容性的话,答案是可以利用文字的 -webkit...
继续阅读 »

需求背景 - 文字的二次加粗


今天遇到这样一个有意思的问题:



  1. 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢?


emm,不考虑兼容性的话,答案是可以利用文字的 -webkit-text-stroke 属性,给文字二次加粗。


MDN - webkit-text-stroke: 该属性为文本字符添加了一个边框(笔锋),指定了边框的颜色, 它是 -webkit-text-stroke-width-webkit-text-stroke-color 属性的缩写。


看下面的 DEMO,我们可以利用 -webkit-text-stroke,给文字二次加粗:


<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
}
p:nth-child(2) {
font-weight: bold;
}
p:nth-child(3) {
-webkit-text-stroke: 3px red;
}
p:nth-child(4) {
-webkit-text-stroke: 3px #000;
}

对比一下下面 4 种文字,最后一种利用了 font-weight: bold-webkit-text-stroke,让文字变得更为



CodePen Demo -- font-weight: bold 和 -webkit-text-stroke 二次加粗文字


如何给二次加粗的文字再添加边框?


OK,完成了上述第一步,事情还没完,更可怕的问题来了。


现在文字要在二次加粗的情况下,再添加一个不同颜色的边框。


我们把原本可能可以给文字添加边框的 -webkit-text-stroke 属性用掉了,这下事情变得有点棘手了。这个问题也可以转变为,如何给文字添加 2 层不同颜色的边框?


当然,这也难不倒强大的 CSS(SVG),让我们来尝试下。


尝试方法一:使用文字的伪元素放大文字


第一种尝试方法,有点麻烦。我们可以对每一个文字进行精细化处理,利用文字的伪元素稍微放大一点文字,将原文字和访达后的文字贴合在一起。



  1. 将文字拆分成一个一个独立元素处理

  2. 利用伪元素的 attr() 特性,利用元素的伪元素实现同样的字

  3. 放大伪元素的字

  4. 叠加在原文字之下


上代码:


<ul>
<li data-text="文">文</li>
<li data-text="字">字</li>
<li data-text="加">加</li>
<li data-text="粗">粗</li>
<li data-text="C">C</li>
<li data-text="S">S</li>
<li data-text="S">S</li>
</ul>

ul {
display: flex;
flex-wrap: nowrap;
}

li {
position: relative;
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 3px #000;

&::before {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
color: red;
-webkit-text-stroke: 3px #f00;
z-index: -1;
transform: scale(1.15);
}
}

可以简单给上述效果加个动画,一看就懂:



CodePen Demo -- 利用伪元素给加粗文字添加边框


看着不错,但是实际上仔细观察,边框效果很粗糙,文字每一处并非规则的被覆盖,效果不太能接受:



尝试方法二:利用 text-shadow 模拟边框


第一种方法宣告失败,我们继续尝试第二种方式,利用 text-shadow 模拟边框。


我们可以给二次加粗的文字添加一个文字阴影:


<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
text-shadow: 0 0 2px red;
}

看看效果:


image


好吧,这和边框差的也太远了,它就是阴影。


不过别着急,text-shadow 是支持多重阴影的,我们把上述的 text-shadow 多叠加几次:


p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
- text-shadow: 0 0 2px red;
+ text-shadow: 0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red;
}


Wow,不仔细看的话,利用这种叠加多层 text-shadow 的方式,还真的非常像边框!


当然,如果我们放大来看,瑕疵就比较明显了,还是能看出是阴影:



CodePen Demo -- 利用 text-shadow 给文字添加边框


尝试方法四:利用多重 drop-shadow()


在尝试了 text-shadow 之后,自然而然的就会想到多重 filter: drop-shadow(),主观上认为会和多重 text-shadow 的效果应该是一致的。


不过,实践出真知。


在实际测试中,发现利用 filter: drop-shadow() 的效果比多重 text-shadow 要好,模糊感会弱一些:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red);
}

效果如下:


image


我们甚至可以利用它制作文字二次加粗后的多重边框:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.2px red)
// 重复 N 次
drop-shadow(0 0 0.2px red)
drop-shadow(0 0 0.25px blue)
// 重复 N 次
drop-shadow(0 0 0.25px blue);
}

效果如下:



然而,在不同屏幕下(高清屏和普通屏),drop-shadow() 的表现效果差别非常之大,实则也难堪重用。


我们没有办法了吗?不,还有终极杀手锏 SVG。


尝试方法四:利用 SVG feMorphology 滤镜给文字添加边框


其实利用 SVG 的 feMorphology 滤镜,可以非常完美的实现这个需求。


这个技巧,我在 有意思!不规则边框的生成方案 这篇文章中也有提及。


借用 feMorphology 的扩张能力给不规则图形添加边框


直接上代码:


<p>文字加粗CSS</p>

<svg width="0" height="0">
<filter id="dilate">
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="2"></feMorphology>
<feFlood flood-color="red" flood-opacity="1" result="flood"></feFlood>
<feComposite in="flood" in2="DILATED" operator="in" result="OUTLINE"></feComposite>

<feMerge>
<feMergeNode in="OUTLINE" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>

p {
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 2px #000;
filter: url(#dilate);
}

效果如下:



我们可以通过 SVG feMorphology 滤镜中的 radius 控制边框大小,feFlood 滤镜中的 flood-color 控制边框颜色。并且,这里的 SVG 代码可以任意放置,只需要在 CSS 中利用 filter 引入即可。


本文不对 SVG 滤镜做过多的讲解,对 SVG 滤镜原理感兴趣的,可以翻看我上述提到的文章。


至此,我们就完美的实现了在已经利用 font-weight: bold-webkit-text-stroke 的基础上,再给文字添加不一样颜色的边框的需求。


放大了看,这种方式生成的边框,是真边框,不带任何的模糊:



CodePen Demo -- 利用 SVG feMorphology 滤镜给文字添加边框


最后


OK,本文到此结束,介绍了一些 CSS 中的奇技淫巧去实现文字二次加粗后加边框的需求,实际需求中,如果不是要求任意字都要有这个效果,其实我更推荐切图大法,高保真,不丢失细节。


当然,可能还有更便捷更有意思的解法,欢迎在评论区不吝赐教。


希望本文对你有所帮助 :)


想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄


更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。


如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:chokcoco
链接:https://juejin.cn/post/7023940690476269605

收起阅读 »

大道至简,繁在人心:在浏览器控制台安装npm包是什么操作?

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然...
继续阅读 »

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然如日中天,还是世界上最大的软件注册表


  通常,我们通过npm install xxx在 React、Vue、Angular 等现代前端项目中安装依赖,但是前端项目在本质上还是运行在浏览器端的 HTML、JavaScript 和 CSS,那么,我们有办法在浏览器控制台直接安装 npm 包并使用吗?


  如果你对这个问题感兴趣,不妨跟着我通过本文一探究竟,也许最终你会发现:越是“复杂”的东西,其原理越趋向“简单”


通过 <script /> 引入 cdn 资源


  在浏览器控制台安装 npm 包,看起来是个天马行空的想法,让人觉得不太切实际。如果我换一个方式进行提问:如何在浏览器/HTML 中引入 JavaScript 呢?也许你马上就有了答案:<script />标签。没错,我们的第一步就是通过 <script />标签在 HTML 页面上引入 cdn 资源。


  那么,又该如果在控制台在页面上插入<script />标签来引入 CDN 资源呢?这个问题可难不倒你


// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
};

  我们还得在资源引入后以及出现错误时,给用户一些提示:


script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};

  这么以来,我们就可以直接在控制台引入 cdn 资源了,你可以再额外补充一些善后工作的处理逻辑,比如把<script />标签移除。当然,你也完全可以通过创建<link />标签来引入css样式库,这里不过多赘述。


根据包名安装 npm 包


  上面实现了通过<script /> 引入 cdn 资源,但是我们安装 npm 包一般都是通过npm install后面直接跟包名来完成的,显然单靠<script />的方式难以达到我们的饿预期,那么,有没有一种方式,可以将我们的包名直接转换成 cdn 资源地址呢?


  答案当然是:有。否则我写个屁啊 🤔,cdnjs就提供了这样的能力。


  cdnjs 提供了一个简单的 API,允许任何人快速查询 CDN 上的资源。具体使用读者可参考官方链接,这里给出一个根据包名查询 CDN 资源链接的示例,可以直接在浏览器地址栏打开这个链接查看:https://api.cdnjs.com/libraries?search=jquery,这是一个 get 请求,你将看到类似下面的页面,数组的第一项为名称/功能最相近的资源的最新 CDN 资源地址


jquery


  是以,根据包名搜索 cdn 资源 URL 便有如下的实现:


const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最相关的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
// 如果名称和你传进来的不一样
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

安装特定版本的 npm 包


  我们在 npm 中还可以通过类似npm install jquery@3.5.1的语法安装特定版本的 npm 包,而 cdnjs 只能返回特定版本的详细信息(不含 cdn 资源链接)。


  UNPKG在此时可以帮我们一个大忙。unpkg 是一个快速的全球内容分发网络,适用于 npm 上的所有内容。使用它可以使用以下 URL 快速轻松地从任何包加载任何文件unpkg.com/:package@:version/:file


  例如,访问https://unpkg.com/jquery@3.5.1会自动重定向到https://unpkg.com/jquery@3.5.1/dist/jquery.js,并返回v3.5.1版本的jQuery文件内容(如果不带版本号,会返回最新的资源):


jquery_unpkg


  也就是说,我们可以将https://unpkg.com/包名直接丢给<script />标签来加载资源:


const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

完整代码


  将上面的代码简单整理,并通过一个统一的入口方法npmInstall进行调用:


// 存储原始传入的名称
let pkg_name_origin = null;
const npmInstall = (originName) => {
// Trim string
const name = originName.trim();
pkg_name_origin = name;
// 三种引入方式
// 如果是一个有效的URL,直接通过<script />标签插入
if (/^https?:\/\//.test(name)) return injectScript(name);
// 如果指定了版本,尝试使用unpkg加载
if (name.indexOf('@') !== -1) return unpkg(name);
// 否则,尝试使用cdnjs搜索
return cdnjs(name);
};

// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};
document.body.appendChild(script);
// document.body.removeChild(script);
};

const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最新的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

  我们可以使用类似npmInstall('moment')的方式在控制台进行调用:


console


  下面这些调用方式自然也是支持的:


npmInstall('jquery'); // 直接引入
npmInstall('jquery@2'); // 指定版本
npmInstall('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'); // cdn地址

不每次都写这些函数行不行


  看了上面的操作,确实很简单,但是也许你会说:每次要使用时,我都得在控制台定义和调用函数,有些麻烦,不每次都写这些函数行不行?那自然是行的啦,你完全可以自己写一个浏览器插件,将这些JS代码注入页面,详情可参考7分钟学会写一个浏览器插件——突破某SDN未登录禁止复制的限制


  如果你实在不想写,其实有人已经为你写好了,那便是Console Importer,它可以让你的浏览器控制台成为更强大的实验场



  • 使用示例:


import



  • 效果图:


Console Importer



链接:Console Importer | Chrome 插件地址



可以干什么


  那么,本文介绍的方法和工具到底有什么用呢?


  平时开发中,我们经常会想要在项目里尝试一些操作或者验证一些库的方法、打印结果,通过本文的学习,以后你完全可以直接在控制台引入loadsh、moment、jQuery、React 等来进行使用和验证,减少在项目中进行console.log验证后再删除的频次。



  • 你可以通过引入jQuery方便的进行一些项目、页面中的DOM操作;

  • 你可以通过引入axios进行一些简单的接口请求;

  • 你可以通过引入moment.js来验证一些时间格式化方法的使用;

  • 你可以通过引入loadsh并调用它的方法完成一些便捷的计算;

  • ...


可以学到什么


unpkg


  unpkg 是一个内容源自 npm 的前端常用全球快速 CDN,它能以快速、简洁、优雅的方式提供任意包、任意文件的访问,在流行的类库、框架文档中常常能看到它的身影。使用方式一般是unpkg.com/:package@:version/:file。或者更简洁一点:https://unpkg.com/包名,包名包含版本号时,你将获得对应版本的js文件,不包含版本号时,你将获得这个库的最新版js文件。


cdnjs


  cdnjs是一种免费的开源 CDN 服务,受到超过 12.5% 的网站的信任,每月处理超过 2000 亿次请求,由 Cloudflare 提供支持。它类似 Google CDN 和微软CDN服务,但是速度比这二者更加快。CDNJS 上提供了众多 JavaScript 库,你可以直接在网页上引用这些 JS 文件,实现用户浏览网站的最佳速度体验。


  你还可以通过它的查询APIhttps://api.cdnjs.com/libraries?search=xxx进行特定库的cdn地址的查找,这个API还会给你返回一些你所查询的库的替代品


大道至简,繁在人心


  越是“复杂”的东西,其原理也许越是趋向“简单”,大道至简,繁在人心,祝每一个努力攀登者,终能豁然开朗,释然于心。


作者:獨釣寒江雪
链接:https://juejin.cn/post/7023916328637431816

收起阅读 »

微信小程序统一分享,全局接管页面分享消息的一些技巧

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。 全局接管分享事件 而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗? onShareAppMessage: function () {...
继续阅读 »

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。


全局接管分享事件


而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗?


onShareAppMessage: function () {
return {
title: '分享的标题',
path: '分享的页面路径'
}
},

有没有办法能全局统一接管分享呢?写一次,所有页面就都可以分享了。


能!


由于onShareAppMessage是一个函数,在用户点击右上角...时触发,或者<button open-type='share'>时触发。所以我们只要在这之前替换掉这个函数就可以了。


通过wx.onAppRoute(cb)这个方法,我们可以监听到微信小程序页面栈的变化。


//在小程序启动时添加全局路由变化的监听
onLaunch(){
wx.onAppRoute(()=>{
console.log('page stack changed');
console.log(getCurrentPages());
});
}

onAppRoute会在页面栈改变后被触发,这个时候通过getCurrentPages()方法,我们可以拿到小程序中全部的页面栈。


数组最后一个就是当前页面


image.png


现在直接给当前页面这个对象赋值onShareAppMessage即可


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了"
}
}

再分享时我们就会发现被接管了


image.png


获取当前页面的地址


page参数不传的话,默认转发出去就是当前页面的地址。当然通过curPage.route也可以获取该页面地址。


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:curPage.route
}
}

小技巧


如果就这样分享出去,用户打开的时候,就会直接展示这个分享的页面。直接返回,或者左滑屏幕,都会直接退出到聊天界面。用户主动分享一次产生的裂变不容易,我希望这个分享带来的价值最大化,让接到分享的微信用户看到更多页面的话怎么办呢?


永远先进首页,首页检查启动参数后再跳转相关页面


curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:"/pages/home/home?url="+curPage.route
}
}


作者:大帅老猿
链接:https://juejin.cn/post/7024046727820738573

收起阅读 »

我阅读源码的五步速读法

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。 我的阅读源码的方法分为五步: 第一步,通过文档和测试用例了解代码的功能 阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解...
继续阅读 »

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。


我的阅读源码的方法分为五步:


第一步,通过文档和测试用例了解代码的功能


阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解代码做了什么,输入和输出是什么。


了解功能是阅读源码的基础,后面才会有方向感。


第二步,自己思考功能的实现方式


知道了源码有啥功能之后,要先思考下如果自己实现会怎么做。有个大概的思路就行。


如果想不通可以看下源码用到了哪些依赖库,这些依赖库都有啥功能,再想下应该怎么实现。


如果还想不通也没关系,重要的是要先自己思考下实现方式。


第三步,粗读源码理清实现思路


你已经有了一个大概的实现思路,然后再去读源码,看下它是怎么实现的。和你思路类似的地方很快就可以掠过去,而且印象也很深,和你思路不一样的地方,通过读代码搞清楚它的实现思路。


这一步不用关心细节,知道某段代码是干啥的就行,关键是和自己的思路做 diff,理清它的整体实现思路。


第四步,通过 debugger 理清实现细节


粗读源码理清了实现思路之后,对于一些部分的具体实现可能还不是很清楚,这时候就可以通过 debugger 来断点调试了。


构造一个能触发该功能的测试用例,在关心的代码处打一个断点,通过 debugger 运行代码。


这时候你已经知道这部分代码是干啥的了,单步调试也很容易理清每一条语句的功能,这样一条语句一条语句的搞懂之后,你就很容易能把这部分代码的实现细节理清楚。


这样一部分一部分的通过 debugger 理清细节实现之后,你就对整体代码的思路和细节的实现都有了比较好的掌握。


第五步,输出文章来讲述源码实现思路


当你觉得对源码的实现有了比较好的掌握的时候,可以输出一篇文章的方式来讲述源码的整体思路。


因为可能会有一些部分是你没注意到的,而在输出的过程中,会进行更全面的思考,这时候如果发现了一些没有读到的点,可以再通过前面几步去阅读源码,直到能清晰易懂的把源码的实现讲清楚。这样才算真正的把代码读懂了。


这就是我觉得比较高效的阅读源码的方法。


总结


我阅读源码的方法分为五步:



  1. 通过文档和测试用例了解代码的功能

  2. 自己思考功能的实现方式

  3. 粗读源码理清实现思路

  4. 通过 debugger 理清实现细节

  5. 输出文章来讲述源码实现思路


这五步缺一不可:



  • 缺了第一步,不了解功能就开始读源码,那读代码会没有方向感

  • 缺了第二步,不经过思考直接读源码,理解代码实现思路的效率会降低

  • 缺了第三步,不理清整体思路就开始 debugger,会容易陷入细节,理不清整体的思路

  • 缺了第四步,不 debugger 只大概理下整体思路,这样不能从细节上真正理清楚

  • 缺了第五步,不通过输出文章来检验,那是否真的理清了整体思路和实现细节是没底的


当然,这是我个人的阅读源码的方法,仅供参考。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7024084789929967646

收起阅读 »

复杂web动画,不慌,选择 web Animations API

说动前端动画,我们熟知的有两种 CSS 动画 (requestAnimation/setTimeout/setInterval + 属性改变) 动画 当然有人可能会说canvas动画,从运动本质了还是第二种。 今天说的是第三种 Web Animations...
继续阅读 »

说动前端动画,我们熟知的有两种



  1. CSS 动画

  2. (requestAnimation/setTimeout/setInterval + 属性改变) 动画


当然有人可能会说canvas动画,从运动本质了还是第二种。


今天说的是第三种 Web Animations API, 也有简称为 WAAPI 的。


与纯粹的声明式CSS不同,JavaScript还允许我们动态地将属性值设置为持续时间。 对于构建自定义动画库和创建交互式动画,Web动画API可能是完成工作的完美工具。


举两个栗子


落球


点击之后,球体下落


ballFall2.gif


const ballEl = document.querySelector(".ball");
ballEl.addEventListener("click", function () {
let fallAni = ballEl.animate({
transform: ['translate(0, 0)', 'translate(20px, 8px)', 'translate(50px, 200px)']
}, {
easing: "cubic-bezier(.68,.08,.89,-0.05)",
duration: 2000,
fill: "forwards"
})
});

直播的世界消息或者弹幕


这是一个我们项目中一个实际的例子, 直播的弹幕。

我们需要消息先运动到屏幕中间,消息最少需要在停留2秒,如果消息过长,消息还需要 匀速滚动 ,之后再滑出屏幕。



  1. 滑入

  2. 暂停,如果消息过长,消息还需要匀速滚动

  3. 滑出


难点就在于,暂停阶段,消息滚动的时间并不是确定的,需要计算。 这个时候,纯CSS3的动画,难度就有些高了,采用 Web Animations API,天然的和JS亲和,那就简单多了。


先看看效果
longDan2.gif


shortDan.gif


代码也就简单的分为三段:滑入,暂停,滑出。

因为其天然支持Promise, 代码很简洁,逻辑也很清晰。


async function startAnimate() {
// 滑入
const totalWidth = stageWidth + DANMU_WITH;
const centerX = stageWidth * 0.5 - DANMU_WITH * 0.5;
const kfsIn = {
transform: [`translateX(${totalWidth}px)`, `translateX(${centerX}px)`]
}
await danmuEl.animate(kfsIn, {
duration: 2000,
fill: 'forwards',
easing: 'ease-out'
}).finished;

// 暂停部分
const contentEl = danmuEl.querySelector(".danmu-content");
const itemWidth = contentEl.getBoundingClientRect().width;
const gapWidth = Math.max(0, itemWidth - DANMU_WITH);
const duration = Math.max(0, Math.floor(gapWidth / 200) * 1000);

const translateX = duration > 0 ? gapWidth : 0;
const kfsTxt = {
transform: [`translateX(0px)`, `translateX(-${gapWidth}px)`]
};
await contentEl.animate(kfsTxt, {
duration,
delay: 2000,
fill: 'forwards',
easing: 'linear',
}).finished;

// 滑出
const kfsOut = {
transform: [`translateX(${centerX}px)`, `translateX(-${DANMU_WITH}px)`]
};
await danmuEl.animate(kfsOut, {
duration: 2000,
fill: "forwards",
easing: 'ease-in'
}).finished;

if (danmuEl) {
stageEl.removeChild(danmuEl);
}
isAnimating = false
}

web Animations API 两个核心的对象



  1. KeyframeEffect 描述动画属性

  2. Animation 控制播放


KeyframeEffect


描述动画属性的集合,调用keyframesAnimation Effect Timing Properties。 然后可以使用 Animation 构造函数进行播放。


其有三种构建方式,着重看第二种,参数后面说。



new KeyframeEffect(target, keyframes);

new KeyframeEffect(target, keyframes, options)

new KeyframeEffect(source)



当然我们可以显示的去创建 KeyframeEffect, 然后交付给Animation去播放。 但是我们通常不需要这么做, 有更加简单的API, 这就是接后面要说的 Element.animate


看一个KeyframeEffect复用的例子,new KeyframeEffect(kyEffect)基于当前复制,然后多处使用。


const box1ItemEl = document.querySelector(".box1");
const box2ItemEl = document.querySelector(".box2");

const kyEffect = new KeyframeEffect(null, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ky1 = new KeyframeEffect(kyEffect);
ky1.target = box1ItemEl;

const ky2 = new KeyframeEffect(kyEffect);
ky2.target = box2ItemEl;

new Animation(ky1).play();
new Animation(ky2).play();


kf2.gif


Animation


提供播放控制、动画节点或源的时间轴。 可以接受使用 KeyframeEffect 构造函数创建的对象作为参数。


const box1ItemEl = document.querySelector(".box1");

const kyEffect = new KeyframeEffect(box1ItemEl, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ani1 = new Animation(kyEffect);
ani1.play();

ani1.gif


常用的方法



Animation 事件监听


监听有两种形式:



  1. event 方式


因其继承于EventTarget,所有依旧有两种形式


animation.onfinish = function() {
element.remove();
}

animation.addEventListener("finish", function() {
element.remove();
}


  1. Promise形式


animation.finished.then(() =>
element.remove()
)

比如一个很有用的场景,所有动画完成后:


Promise.all( element.getAnimations().map(ani => ani.finished)
).then(function() {
// do something cool
})

常用事件回调




便捷的 Element.animate


任何 Element都具备该方法, 其语法:



animate(keyframes, options)



其参数和 new KeyframeEffect(target, keyframes, options)的后两个参数基本一样, 返回的是一个Animation对象。


第一个参数 keyframes


keyframes有两种形式,一种是数组形式,一种是对象形式。


数组形式


一组对象(关键帧) ,由要迭代的属性和值组成。

关键帧的偏移可以通过提供一个offset来指定 ,值必须是在 [0.0, 1.0] 这个区间内,且须升序排列。简单理解就是进度的百分比的小数值。


element.animate([ { opacity: 1 },
{ opacity: 0.1, offset: 0.7 },
{ opacity: 0 } ],
2000);

并非所有的关键帧都需要设置offset。 没有指定offset的关键帧将与相邻的关键帧均匀间隔。


对象形式


一个包含key-value键值的对象需要包含动画的属性和要循环变化的值数组


element.animate({
opacity: [ 0, 0.9, 1 ],
offset: [ 0, 0.8 ], // [ 0, 0.8, 1 ] 的简写
easing: [ 'ease-in', 'ease-out' ],
}, 2000);

第二个参数 options


new KeyframeEffect(target, keyframes, options)的第三个参数基本一致,但是多了一个可选属性,就是id,用来标记动画,也方便 在Element.getAnimations结果中精确的查找。





















































后续四个特性相对高级,掌握好了可以玩出花来,本章主要讲基本知识,后续会出高级版本。


更多细节可以参见 KeyframeEffect


Element.getAnimations


我们通过Element.animate或者创建Animation给Element添加很多动画,通过这个方法可以获得所有Animation的实例。


在需要批量修改参数,或者批量停止动画的时候,那可是大杀器。


比如批量暂停动画:


box1ItemEl.getAnimations()
.forEach(el=> el.pause()) // 暂停全部动画

优势



  1. 相对css动画更加灵活

  2. 相对requestAnimation/setTimeout/setInterval 动画,性能更好,代码更简洁

  3. 天然支持Promise,爽爽爽!!!


你有什么理由拒绝她呢?


对比 CSS Animation


动画参数属性键对照表


参数设置值上的区别



  1. duration 参数只支持毫秒

  2. 迭代次数无限使用的是 JS的Infinity,不是字符串 "infinite"

  3. 默认动画的贝塞尔是linear,而不是css的ease


兼容性


整体还不错,Safari偏差。

如果不行, 加个垫片 web-animations-js


我们在实际的桌面项目上已经使用,非常灵活, nice!
image.png


总结


web Animations API 和 css动画,不是谁替换谁。结合使用,效果更佳。


复杂的逻辑动画,因为web Animations API和JS天然的亲和力,是更优的选择。



收起阅读 »

List常用操作比for循环更优雅的写法

list常用的lamada表达式-单list操作 引言 使用JDK1.8之后,大部分list的操作都可以使用lamada表达式去写,可以让代码更简洁,开发更迅速。以下是我在工作中常用的lamada表达式对list的常用操作,喜欢建议收藏。 以用户表为例,用户实...
继续阅读 »

list常用的lamada表达式-单list操作


引言


使用JDK1.8之后,大部分list的操作都可以使用lamada表达式去写,可以让代码更简洁,开发更迅速。以下是我在工作中常用的lamada表达式对list的常用操作,喜欢建议收藏。

以用户表为例,用户实体代码如下:


public class User {
private Integer id; //id

private String name; //姓名

private Integer age; //年龄

private Integer departId; //所属部门id
}

List<User> list = new ArrayList<>();

简单遍历


使用lamada表达式之前,如果需要遍历list时,一般使用增强for循环,代码如下:


List<User> list = new ArrayList<>();
for (User u:list) {
System.out.println(u.toString());
}

使用lamada表达式之后,可以缩短为一行代码:


list.forEach(u-> System.out.println(u.toString()));

筛选符合某属性条件的List集合


以筛选年龄在15-17之间的用户为例,for循环写法为:


List<User> users = new ArrayList<>();
for (User u : list) {
if (u.getAge() >= 15 && u.getAge() <= 17) {
users.add(u);
}
}

使用lamada表达式写法为:


List<User> users = list.stream()
.filter(u -> u.getAge() >= 15 && u.getAge() <= 17)
.collect(Collectors.toList());

获取某属性返回新的List集合


以获取id为例,项目中有时候可能会需要根据用户id的List进行查询或者批量更新操作,这时候就需要用户id的List集合,for循环写法为:


List<Integer> ids = new ArrayList<>();
for (User u:list) {
ids.add(u.getId());
}

lamada表达式写法为:


List<Integer> ids = list.stream()
.map(User::getId).collect(Collectors.toList());

获取以某属性为key,其他属性或者对应对象为value的Map集合


以用户id为key(有时可能需要以用户编号为key),以id对应的user作为value构建Map集合,for循环写法为:


Map<Integer,User> userMap = new HashMap<>();
for (User u:list) {
if (!userMap.containsKey(u.getId())){
userMap.put(u.getId(),u);
}
}

lamada表达式写法为:


Map<Integer,User> map = list.stream()
.collect(Collectors.toMap(User::getId,
Function.identity(),
(m1,m2)->m1));


Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

(m1,m2)-> m1此处的意思是当转换map过程中如果list中有两个相同id的对象,则map中存放的是第一个对象,此处可以根据项目需要自己写。



以某个属性进行分组的Map集合


以部门id为例,有时需要根据部门分组,筛选出不同部门下的人员,如果使用for循环写法为:


Map<Integer,List<User>> departGroupMap = new HashMap<>();
for (User u:list) {
if (departGroupMap.containsKey(u.getDepartId())){
departGroupMap.get(u.getDepartId()).add(u);
}else {
List<User> users1 = new ArrayList<>();
users1.add(u);
departGroupMap.put(u.getDepartId(),users1);
}
}

使用lamada表达式写法为:


Map<Integer,List<User>> departGroupMap = list.stream()
.collect(Collectors
.groupingBy(User::getDepartId));

其他情况


可以根据需要结合stream()进行多个操作,比如筛选出年龄在15-17岁的用户,并且根据部门进行分组分组,如果使用for循环,代码如下:


Map<Integer,List<User>> departGroupMap = new HashMap<>();
for (User u:list) {
if (u.getAge() >= 15 && u.getAge() <= 17) {
if (departGroupMap.containsKey(u.getDepartId())){
departGroupMap.get(u.getDepartId()).add(u);
}else {
List<User> users1 = new ArrayList<>();
users1.add(u);
departGroupMap.put(u.getDepartId(),users1);
}
}
}

使用lamada表达式,代码如下:


Map<Integer,List<User>> departGroupMap = list.stream()
.filter(u->u.getAge() >= 15 && u.getAge() <= 17)
.collect(Collectors.groupingBy(User::getDepartId));

总结


上述部分是小编在工作中遇到的常用的单个List的操作,可能在项目中还会遇到更复杂的场景,可以根据需要进行多个方法的组合使用,我的感觉是使用lamada表达式代码更加简洁明了,当然各人有各人的编码习惯,不喜勿喷。


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

还在傻乎乎得背MyISAM与InnoDB 的区别?一篇文章让你理解的明明白白

序言     相信不少的小伙伴在准备面试题的时候,必定会遇到这个面试题,MyISAM与InnoDB 的区别是什么?我们当时可谓是背一次忘一次,以至于很多的同学去找实习工作的时候,经常被这个问题卡脖子,那么今天我就系统的来...
继续阅读 »

序言


    相信不少的小伙伴在准备面试题的时候,必定会遇到这个面试题,MyISAM与InnoDB 的区别是什么?我们当时可谓是背一次忘一次,以至于很多的同学去找实习工作的时候,经常被这个问题卡脖子,那么今天我就系统的来说一说MyISAM与InnoDB 的区别,一问让你们彻底整明白!


🧡MySQL默认存储引擎的变迁


    熟悉MySQL的小伙伴们都知道,在MySQL 5.1之前的版本中,默认的搜索引擎是MyISAM,从MySQL 5.5之后的版本中,默认的搜索引擎变更为InnoDB。这也间接说明了,MySQL官方更推荐使用InnoDB。


💛MyISAM与InnoDB存储引擎的主要特点


💚MyISAM


    MyISAM存储引擎的特点是:表级锁、不支持事务和全文索引,适合一些CMS内容管理系统作为后台数据库使用,但是使用大并发、重负荷生产系统上,表锁结构的特性就显得力不从心。下图是MySQL 5.7 MyISAM存储引擎的版本特性。
image.png


💙InnoDB


    InnoDB存储引擎的特点是:行级锁、事务安全(ACID兼容)、支持外键、不支持FULLTEXT类型的索引(5.6.4以后版本开始支持FULLTEXT类型的索引)。InnoDB存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全存储引擎。InnoDB是为处理巨大量时拥有最大性能而设计的。它的CPU效率可能是任何其他基于磁盘的关系数据库引擎所不能匹敌的。以下是MySQL 5.7 InnoDB存储引擎的版本特性。


image.png


💜MyISAM与InnoDB性能测试


    MyISAM与InnoDB谁的性能更高,其实官方已经给了压测图
image.png


image.png


其实瞎眼可见的结果是:MyISAM被InnoDB直接按在地上摩擦!


🤎是否支持事务


    MyISAM是一种非事务性的引擎(不支持事务),使得MyISAM引擎的MySQL可以提供高速存储和检索,以及全文搜索能力,适合数据仓库等查询频繁的应用。


    InnoDB是事务安全的(支持事务),事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而MyISAM就不可以了。


🖤MyISAM与InnoDB表锁和行锁


    MySQL表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。什么意思呢,就是说对MyISAM表进行读操作时,它不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写操作;而对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作。


    InnoDB行锁是通过给索引项加锁来实现的,即只有通过索引条件检索数据,InnoDB才使用行级锁,否则将使用表锁!行级锁在每次获取锁和释放锁的操作需要消耗比表锁更多的资源。在InnoDB两个事务发生死锁的时候,会计算出每个事务影响的行数,然后回滚行数少的那个事务。当锁定的场景中不涉及Innodb的时候,InnoDB是检测不到的。只能依靠锁定超时来解决。


💔是否保存数据库表中表的具体行数


    InnoDB 中不保存表的具体行数,也就是说,执行select count(*) from table 时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。


💕如何选择


    虽然InnoDB很好,但是也不是无脑选,有些情况下MyISAM比InnoDB更好!


MyISAM适合:



  1. 做很多count 的计算;

  2. 插入不频繁,查询非常频繁,如果执行大量的SELECT,MyISAM是更好的选择;

  3. 没有事务。


InnoDB适合:



  1. 可靠性要求比较高,或者要求事务;

  2. 表更新和查询都相当的频繁,并且表锁定的机会比较大的情况指定数据引擎的创建;

  3. 如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表;

  4. DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的 删除;

  5. LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

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

android常用命令介绍

adb命令 安装apk -f 表示强制安装 adb install -f apk 获取当前顶部activity名称方式 windows: adb shell dumpsys window | findstr mCurrentFocus mac: adb s...
继续阅读 »

adb命令


安装apk


-f 表示强制安装


adb install -f apk

获取当前顶部activity名称方式


windows:  adb shell dumpsys window | findstr mCurrentFocus
mac: adb shell dumpsys window | grep mCurrentFocus

apksigner命令


查看apk签名方式命令


apksigner verify -v apk

给apk进行签名



  1. 大于28的版本


apksigner sign --ks your_keystore_file --v1-signing-enabled 

true --v2-signing-enabled true --v3-signing-enabled false your_apk_file


  1. 小于28的版本


apksigner sign --ks your_keystore_file --v1-signing-enabled 

true --v2-signing-enabled true your_apk_file

apktool命令


反编译


apktool d apk

正编译


不指定输出的话,默认apk在目录的dist文件夹下


apktool b 反编译的目录名

keytool命令


查看证书信息


keytool -list -v -keystore your.keystore -storepass yourpassword

-storepass yourpassword不输入的话,执行命令的时候回提示输入密码


修改别名方法


keytool -changealias -keystore your.keystore -alias yourcurrentalias -destalias cert

其它


apk对齐命令


zipalign -v 4 源apk  目的apk

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

Moshi踩坑之HashMap

Moshi 之HashMap就是这个错 moshi让你写自定义Adapter呢,报错摘要:No JsonAdapter for class java.util.HashMap, you should probably use Map ins...
继续阅读 »

Moshi 之HashMap

就是这个错 moshi让你写自定义Adapter呢,

报错摘要:

No JsonAdapter for class java.util.HashMap, you should probably use Map instead of HashMap (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.

解决方法

自定义 HashMap Adapter代码如下

class HashMapJsonAdapter<K, V>(moshi: Moshi, keyType: Type?, valueType: Type?) :
JsonAdapter<HashMap<K?, V?>?>() {
private val keyAdapter: JsonAdapter<K>
private val valueAdapter: JsonAdapter<V>

@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: HashMap<K?, V?>?) {
writer.beginObject()
for (entry: Map.Entry<K?, V?> in value!!.entries) {
if (entry.key == null) {
throw JsonDataException("Map key is null at " + writer.path)
}
writer.promoteValueToName()
keyAdapter.toJson(writer, entry.key)
valueAdapter.toJson(writer, entry.value)
}
writer.endObject()
}

@Throws(IOException::class)
override fun fromJson(reader: JsonReader): HashMap<K?, V?> {
val result = HashMap<K?, V?>()
reader.beginObject()
while (reader.hasNext()) {
reader.promoteNameToValue()
val name = keyAdapter.fromJson(reader)
val value = valueAdapter.fromJson(reader)
val replaced = result.put(name, value)
if (replaced != null) {
throw JsonDataException(
"Map key '"
+ name
+ "' has multiple values at path "
+ reader.path
+ ": "
+ replaced
+ " and "
+ value
)
}
}
reader.endObject()
return result
}

override fun toString(): String {
return "JsonAdapter($keyAdapter=$valueAdapter)"
}

companion object {
val FACTORY: Factory =
Factory { type, annotations, moshi ->
val rawType = Types.getRawType(type)
if (annotations.isNotEmpty()) return@Factory null
if (rawType != java.util.HashMap::class.java) return@Factory null
val keyAndValue = if (type === java.util.Properties::class.java) arrayOf<Type>(
String::class.java,
String::class.java
) else {
arrayOf<Type>(Any::class.java, Any::class.java)
}
HashMapJsonAdapter<Any?, Any>(
moshi,
keyAndValue[0],
keyAndValue[1]
).nullSafe()
}
}

init {
keyAdapter = moshi.adapter(keyType)
valueAdapter = moshi.adapter(valueType)
}
}

其实完全就是在抄MoshiMapJsonAdapter 然后略微修改一下FACTORY

如何使用

Moshi.Builder()
.add(HashMapJsonAdapter.FACTORY)
.build()

完美解决

收起阅读 »

Adnroid 卡顿分析与布局优化

1 卡顿分析1 SystraceSystrace是Android平台提供的一款工具,用于记录短期内的设备活动,其中汇总了Android内核中的数据,例如CPU调度程序,磁盘活动和应用程序,Systrace主要用来分析绘制性能方面的问题,在发生卡顿时,通过这份报...
继续阅读 »

1 卡顿分析

1 Systrace

Systrace是Android平台提供的一款工具,用于记录短期内的设备活动,其中汇总了Android内核中的数据,例如CPU调度程序,磁盘活动和应用程序,Systrace主要用来分析绘制性能方面的问题,在发生卡顿时,通过这份报告,可以知道当前整个系统所处的状态,从而帮助开发者更直观的分析系统瓶颈,改进系统性能

2 android profile 中的cpu监测

App层面监测卡顿 1 利用UI线程的Looper打印日志匹配 

2 使用Choreographer.FrameCallback 

Looper日志监测卡顿** 

Android 主线程更新UI,如果界面1室内刷新少于60次,即FPS小于60,用户就会产生卡顿的感觉,简单来说Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断去除message,调用其他绑定的UI线程执行,如果在_handler的dispatchMessage_方法里面有耗时操作,就会发生卡顿.

只要监测_msg.target.dispatchmessage_的执行时间,就能检车就能检测到部分UI线程是否有耗时的操作。

注意到这行 执行代码的前后,有两个logging.println函数,如果设置了logging,会分别打印出>>>>> Dispatching to和 

<<<<< Finished to 这样的日志,

这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时 间,从而设置阈值判断是否发生了卡顿。 

Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我们可以自己实现一个Printer,在 通过setMessageLogging()方法传入即可

package com.dy.safetyinspectionforengineer.block;

import android.os.Looper;
public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}


package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Printer;
import java.util.List;

public class LogMonitor implements Printer {

private StackSampler mStackSampler;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
// 卡顿阈值
private long mBlockThresholdMillis = 3000;
//采样频率
private long mSampleInterval = 1000;

private Handler mLogHandler;

public LogMonitor() {
mStackSampler = new StackSampler(mSampleInterval);
HandlerThread handlerThread = new HandlerThread("block-canary-io");
handlerThread.start();
mLogHandler = new Handler(handlerThread.getLooper());
}
@Override
public void println(String x) {
//从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息
if (!mPrintingStarted) {
//记录开始时间
mStartTimestamp = System.currentTimeMillis();
mPrintingStarted = true;
mStackSampler.startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//出现卡顿
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
mStackSampler.stopDump();
}
}
private void notifyBlockEvent(final long endTime) {
mLogHandler.post(new Runnable() {
@Override
public void run() {
//获得卡顿时主线程堆栈
List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
for (String stack : stacks) {
Log.e("block-canary", stack);
}
}
});
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
}


package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class StackSampler {
public static final String SEPARATOR = "\r\n";
public static final SimpleDateFormat TIME_FORMATTER =
new SimpleDateFormat("MM-dd HH:mm:ss.SSS");

private Handler mHandler;
private Map<Long, String> mStackMap = new LinkedHashMap<>();
private int mMaxCount = 100;
private long mSampleInterval;
//是否需要采样
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

public StackSampler(long sampleInterval) {
mSampleInterval = sampleInterval;
HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
/**
* 开始采样 执行堆栈
*/
public void startDump() {
//避免重复开始
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mSampleInterval);
}
public void stopDump() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
mHandler.removeCallbacks(mRunnable);
}
public List<String> getStacks(long startTime, long endTime) {
ArrayList<String> result = new ArrayList<>();
synchronized (mStackMap) {
for (Long entryTime : mStackMap.keySet()) {
if (startTime < entryTime && entryTime < endTime) {
result.add(TIME_FORMATTER.format(entryTime)
+ SEPARATOR
+ SEPARATOR
+ mStackMap.get(entryTime));
}
}
}
return result;
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString()).append("\n");
}
synchronized (mStackMap) {
//最多保存100条堆栈信息
if (mStackMap.size() == mMaxCount) {
mStackMap.remove(mStackMap.keySet().iterator().next());
}
mStackMap.put(System.currentTimeMillis(), sb.toString());
}
if (mShouldSample.get()) {
mHandler.postDelayed(mRunnable, mSampleInterval);
}
}
};
}


public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
BlockCanary.install();
}
}

Choreographer.FrameCallback

Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧 的刷新频率。通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调 FrameCallback.doFrame (long frameTimeNanos) 函数。frameTimeNanos是底层VSYNC信号到达的时间戳 。

import android.os.Build;
import android.view.Choreographer;

import java.util.concurrent.TimeUnit;

public class ChoreographerHelper {

static long lastFrameTimeNanos = 0;

public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {

@Override
public void doFrame(long frameTimeNanos) {
//上次回调时间
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (diff > 16.6f) {
//掉帧数
int droppedCount = (int) (diff / 16.6);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}

通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自 动保存现场堆栈信息。 Looper比较适合在发布前进行测试或者小范围灰度测试然后定位问题,ChoreographerHelper适合监控线上环境 的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。

布局优化
1 层级优化

可以使用工具layoutinspector 查看层级,或这看源码查看层级
Tools - layoutINspector

2 使用merge标签

当我们有一些布局元素需要被多处使用时,我们可以将其抽取成一个单独的布局文件,在需要的地方include加载,这是就可以使用merge标签,吧这些抽离的标签进行包裹

3 使用viewstub标签

在不显示及不可见的情况下 用viewstub来包裹,被包裹后,如果visible=gone 则该view不会立即加载,等到需要显示的时候,设置viewstub为visible 或调用其inflater()方法,该view才会初始化

过度渲染
1进入开发则选项
2调用调试GPU过度绘制
3 选择显示过度绘制区域
3.1 蓝色 为一次绘制 绿色为两次绘制 粉色为3次绘制,红色为4次或更多次绘制

解决过度绘制问题
1 移除不需要的背景
2 使视图层次结构扁平化
3 降低透明度

布局加载优化
1 异步加载
setContentView 时 可以异步加载

implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" 

new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null,
new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid,
@Nullable ViewGroup parent) {
setContentView(view);
}
});

收起阅读 »

Android - Binder通信架构

Java应用层:  对于上层应用通过调用AMP.startService, 完全可以不用关心底层,经过层层调用,最终必然会调用到AMS.startService.Java IPC层:  Binder通信是采用C/S架构,...
继续阅读 »


image.png

  • Java应用层:  对于上层应用通过调用AMP.startService, 完全可以不用关心底层,经过层层调用,最终必然会调用到AMS.startService.
  • Java IPC层:  Binder通信是采用C/S架构, Android系统的基础架构便已设计好,Binder在Java framework层的Binder客户类,BinderProxy和服务类Binder;
  • Native IPC层:  对于Native层,如果需要直接使用Binder(比如media相关), 则可以直接使用BpBinder和BBinder(当然这里还有JavaBBinder)即可, 对于上一层Java IPC的通信也是基于这个层面.
  • Kernel物理层:  这里是Binder Driver, 前面3层都跑在用户空间,对于用户空间的内存资源是不共享的,每个Android的进程只能运行在自己进程所拥有的虚拟地址空间, 而内核空间却是可共享的. 真正通信的核心环节还是在Binder Driver.

通过startService的流程分析如图

image.png

AMP和AMN都是实现了IActivityManager接口,AMS继承于AMN. 其中AMP作为Binder的客户端,运行在各个app所在进程, AMN(或AMS)运行在系统进程system_server.

Binder IPC原理

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务.

image.png

可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,Client端和Server端通信时都需要先获取Service Manager接口,才能开始通信服务, 当然查找到目标信息可以缓存起来则不需要每次都向ServiceManager请求。

  1. 注册服务:首先AMS注册到ServiceManager。该过程:AMS所在进程(system_server)是客户端,ServiceManager是服务端。
  2. 获取服务:Client进程使用AMS前,须先向ServiceManager中获取AMS的代理类AMP。该过程:AMP所在进程(app process)是客户端,ServiceManager是服务端。
  3. 使用服务: app进程根据得到的代理类AMP,便可以直接与AMS所在进程交互。该过程:AMP所在进程(app process)是客户端,AMS所在进程(system_server)是服务端。

Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder Driver进行交互的,从而实现IPC通信方式.

Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层.

通信原理

image.png

  1. 发起端线程向Binder Driver发起binder ioctl请求后, 便采用环不断talkWithDriver,此时该线程处于阻塞状态, 直到收到如下BR_XXX命令才会结束该过程.

    • BR_TRANSACTION_COMPLETE: oneway模式下,收到该命令则退出
    • BR_REPLY: 非oneway模式下,收到该命令才退出;
    • BR_DEAD_REPLY: 目标进程/线程/binder实体为空, 以及释放正在等待reply的binder thread或者binder buffer;
    • BR_FAILED_REPLY: 情况较多,比如非法handle, 错误事务栈, security, 内存不足, buffer不足, 数据拷贝失败, 节点创建失败, 各种不匹配等问题
    • BR_ACQUIRE_RESULT: 目前未使用的协议;
  2. 左图中waitForResponse收到BR_TRANSACTION_COMPLETE,则直接退出循环, 则没有机会执行executeCommand()方法, 故将其颜色画为灰色. 除以上5种BR_XXX命令, 当收到其他BR命令,则都会执行executeCommand过程.

  3. 目标Binder线程创建后, 便进入joinThreadPool()方法, 采用循环不断地循环执行getAndExecuteCommand()方法, 当bwr的读写buffer都没有数据时,则阻塞在binder_thread_read的wait_event过程. 另外,正常情况下binder线程一旦创建则不会退出.

通信协议

image.png

  • Binder客户端或者服务端向Binder Driver发送的命令都是以BC_开头,例如本文的BC_TRANSACTIONBC_REPLY, 所有Binder Driver向Binder客户端或者服务端发送的命令则都是以BR_开头, 例如本文中的BR_TRANSACTIONBR_REPLY.
  • 只有当BC_TRANSACTION或者BC_REPLY时, 才调用binder_transaction()来处理事务. 并且都会回应调用者一个BINDER_WORK_TRANSACTION_COMPLETE事务, 经过binder_thread_read()会转变成BR_TRANSACTION_COMPLETE.
  • startService过程便是一个非oneway的过程, 那么oneway的通信过程如下所述.

image.png

  • Java应用层:  对于上层应用通过调用AMP.startService, 完全可以不用关心底层,经过层层调用,最终必然会调用到AMS.startService.
  • Java IPC层:  Binder通信是采用C/S架构, Android系统的基础架构便已设计好,Binder在Java framework层的Binder客户类,BinderProxy和服务类Binder;
  • Native IPC层:  对于Native层,如果需要直接使用Binder(比如media相关), 则可以直接使用BpBinder和BBinder(当然这里还有JavaBBinder)即可, 对于上一层Java IPC的通信也是基于这个层面.
  • Kernel物理层:  这里是Binder Driver, 前面3层都跑在用户空间,对于用户空间的内存资源是不共享的,每个Android的进程只能运行在自己进程所拥有的虚拟地址空间, 而内核空间却是可共享的. 真正通信的核心环节还是在Binder Driver.

通过startService的流程分析如图

image.png

AMP和AMN都是实现了IActivityManager接口,AMS继承于AMN. 其中AMP作为Binder的客户端,运行在各个app所在进程, AMN(或AMS)运行在系统进程system_server.

Binder IPC原理

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务.

image.png

可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,Client端和Server端通信时都需要先获取Service Manager接口,才能开始通信服务, 当然查找到目标信息可以缓存起来则不需要每次都向ServiceManager请求。

  1. 注册服务:首先AMS注册到ServiceManager。该过程:AMS所在进程(system_server)是客户端,ServiceManager是服务端。
  2. 获取服务:Client进程使用AMS前,须先向ServiceManager中获取AMS的代理类AMP。该过程:AMP所在进程(app process)是客户端,ServiceManager是服务端。
  3. 使用服务: app进程根据得到的代理类AMP,便可以直接与AMS所在进程交互。该过程:AMP所在进程(app process)是客户端,AMS所在进程(system_server)是服务端。

Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder Driver进行交互的,从而实现IPC通信方式.

Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层.

通信原理

image.png

  1. 发起端线程向Binder Driver发起binder ioctl请求后, 便采用环不断talkWithDriver,此时该线程处于阻塞状态, 直到收到如下BR_XXX命令才会结束该过程.

    • BR_TRANSACTION_COMPLETE: oneway模式下,收到该命令则退出
    • BR_REPLY: 非oneway模式下,收到该命令才退出;
    • BR_DEAD_REPLY: 目标进程/线程/binder实体为空, 以及释放正在等待reply的binder thread或者binder buffer;
    • BR_FAILED_REPLY: 情况较多,比如非法handle, 错误事务栈, security, 内存不足, buffer不足, 数据拷贝失败, 节点创建失败, 各种不匹配等问题
    • BR_ACQUIRE_RESULT: 目前未使用的协议;
  2. 左图中waitForResponse收到BR_TRANSACTION_COMPLETE,则直接退出循环, 则没有机会执行executeCommand()方法, 故将其颜色画为灰色. 除以上5种BR_XXX命令, 当收到其他BR命令,则都会执行executeCommand过程.

  3. 目标Binder线程创建后, 便进入joinThreadPool()方法, 采用循环不断地循环执行getAndExecuteCommand()方法, 当bwr的读写buffer都没有数据时,则阻塞在binder_thread_read的wait_event过程. 另外,正常情况下binder线程一旦创建则不会退出.

通信协议

image.png

  • Binder客户端或者服务端向Binder Driver发送的命令都是以BC_开头,例如本文的BC_TRANSACTIONBC_REPLY, 所有Binder Driver向Binder客户端或者服务端发送的命令则都是以BR_开头, 例如本文中的BR_TRANSACTIONBR_REPLY.
  • 只有当BC_TRANSACTION或者BC_REPLY时, 才调用binder_transaction()来处理事务. 并且都会回应调用者一个BINDER_WORK_TRANSACTION_COMPLETE事务, 经过binder_thread_read()会转变成BR_TRANSACTION_COMPLETE.
  • startService过程便是一个非oneway的过程, 那么oneway的通信过程如下所述.

收起阅读 »