注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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

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

前言

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

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

StateFlow

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

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

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

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

SharedFlow

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

class MainViewModel :  ViewModel() {

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

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

}

class MainFragment : Fragment() {

private val viewModel: MainViewModel by viewModels()

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

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

}

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

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

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

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

SharedFlowBus的使用

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

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

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

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

SharedFlowBus的实现

object SharedFlowBus {

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

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

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

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

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

}

源码说明

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

收起阅读 »

iOS 面试题 八股文 1.3

82.找错题 试题1: void test1() { char string[10]; char* str1 = "0123456789"; strcpy( string, str1 ); } 试题2: void test2() { char string[1...
继续阅读 »


82.找错题
试题1:
void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
试题2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1 = 'a';
}
strcpy( string, str1 );
}
试题3:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
试题1字符串str1需要11个字节才能存放下(包括末尾的’\0’),而string只有10个字节的空间,strcpy会导致数组越界;
对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;
对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计’\0’所占用的1个字节。
剖析:
考查对基本功的掌握:
(1)字符串以’\0’结尾;
(2)对数组越界把握的敏感度;
(3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案:
2分
void strcpy( char *strDest, char *strSrc )
{
 while( (*strDest++ = * strSrc++) != ‘\0’ );
}
4分
void strcpy( char *strDest, const char *strSrc ) 
//将源字符串加const,表明其为输入参数,加2分
{
 while( (*strDest++ = * strSrc++) != ‘\0’ );
}
7分
void strcpy(char *strDest, const char *strSrc) 
{
//对源地址和目的地址加非0断言,加3分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
10分
//为了实现链式操作,将目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc ) 
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest; 
while( (*strDest++ = * strSrc++) != ‘\0’ ); 
return address;
}
从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!
(4)对strlen的掌握,它没有包括字符串末尾的'\0'。
读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const
{
assert( strt != NULL ); //断言字符串地址非0
int len;
while( (*str++) != '\0' ) 

len++; 

return len;
}
试题4:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void ) 
{
char *str = NULL;
GetMemory( str ); 
strcpy( str, "hello world" );
printf( str );
}
试题5:
char *GetMemory( void )

char p[] = "hello world"; 
return p; 
}
void Test( void )

char *str = NULL; 
str = GetMemory(); 
printf( str ); 
}
试题6:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" ); 
printf( str ); 
}
试题7:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str ); 
... //省略的其它语句
}
解答:
试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完
char *str = NULL;
GetMemory( str ); 
后的str仍然为NULL;
试题5中
char p[] = "hello world"; 
return p; 
的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句
*p = (char *) malloc( num );
后未判断内存是否申请成功,应加上:
if ( *p == NULL )
{
...//进行申请内存失败处理
}
试题7存在与试题6同样的问题,在执行
char *str = (char *) malloc(100);
后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上:
str = NULL;
试题6的Test函数中也未对malloc的内存进行释放。
剖析:
试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
再看看下面的一段程序有什么错误:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为:
swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}[img=12,12]file:///D:/鱼鱼软件/鱼鱼多媒体***本/temp/{56068A28-3D3B-4D8B-9F82-AC1C3E9B128C}_arc_d[1].gif[/img] 3.内功题
试题1:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)
解答:
BOOL型变量:if(!var)
int型变量: if(var==0)
float型变量:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
指针变量:  if(var==NULL)
剖析:
考查对0值判断的“内功”,BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。 
一般的,如果想让if判断一个变量的“真”、“假”,应直接使用if(var)、if(!var),表明其为“逻辑”判断;如果用if判断一个数值型变量(short、int、long等),应该用if(var==0),表明是与0进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。
浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if (x == 0.0),则判为错,得0分。
试题2:以下为Windows NT下的32位C++程序,请计算sizeof的值
void Func ( char str[100] )
{
sizeof( str ) = ?
}
void *p = malloc( 100 );
sizeof ( p ) = ?
解答:
sizeof( str ) = 4
sizeof ( p ) = 4
剖析:
Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
数组名的本质如下:
(1)数组名指代一种数据结构,这种数据结构就是数组;
例如:
char str[10];
cout << sizeof(str) << endl;
输出结果为10,str指代数据结构char[10]。
(2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改;
char str[10]; 
str++; //编译出错,提示str不是左值
(3)数组名作为函数形参时,沦为普通指针。
Windows NT 32位平台下,指针的长度(占用内存的大小)为4字节,故sizeof( str ) 、sizeof ( p ) 都为4。
试题3:写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
解答:
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
MIN(*p++, b)会产生宏的副作用
剖析:
这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
(1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来。所以,严格地讲,下述解答:
#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )
都应判0分;
(2)防止宏的副作用。
宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:
((*p++) <= (b) ? (*p++) : (*p++))
这个表达式会产生副作用,指针p会作三次++自增操作。
除此之外,另一个应该判0分的解答是:
#define MIN(A,B) ((A) <= (B) ? (A) : (B)); 
这个解答在宏定义的后面加“;”,显示编写者对宏的概念模糊不清,只能被无情地判0分并被面试官淘汰。
试题4:为什么标准头文件都有类似以下的结构? 
#ifndef __INCvxWorksh
#define __INCvxWorksh 
#ifdef __cplusplus
extern "C" {
#endif 
/*...*/ 
#ifdef __cplusplus
}
#endif 
#endif /* __INCvxWorksh */
解答:
头文件中的编译宏
#ifndef __INCvxWorksh
#define __INCvxWorksh
#endif 
的作用是防止被重复引用。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在symbol库中的名字与C语言的不同。例如,假设某个函数的原型为: 
void foo(int x, int y);
该函数被C编译器编译后在symbol库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。_foo_int_int这样的名字包含了函数名和函数参数数量及类型信息,C++就是考这种机制来实现函数重载的。
为了实现C和C++的混合编程,C++提供了C连接交换指定符号extern "C"来解决名字匹配问题,函数声明前加上extern "C"后,则编译器就会按照C语言的方式将该函数编译为_foo,这样C语言中就可以调用C++的函数了。[img=12,12]file:///D:/鱼鱼软件/鱼鱼多媒体***本/temp/{C74A38C4-432E-4799-B54D-73E2CD3C5206}_arc_d[1].gif[/img] 
试题5:编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh” 
函数头是这样的:
//pStr是指向以'\0'结尾的字符串的指针
//steps是要求移动的n
void LoopMove ( char * pStr, int steps )
{
//请填充...
}
解答:
正确解答1:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN]; 
strcpy ( tmp, pStr + n ); 
strcpy ( tmp + steps, pStr); 
*( tmp + strlen ( pStr ) ) = '\0';
strcpy( pStr, tmp );
}
正确解答2:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN]; 
memcpy( tmp, pStr + n, steps ); 
memcpy(pStr + steps, pStr, n ); 
memcpy(pStr, tmp, steps ); 
}
剖析:
这个试题主要考查面试者对标准库函数的熟练程度,在需要的时候引用库函数可以很大程度上简化程序编写的工作量。
最频繁被使用的库函数包括:
(1) strcpy
(2) memcpy
(3) memset

收起阅读 »

iOS 面试题 八股文 1.2

12 怎样防止指针的越界使用问题?    必须让指针指向一个有效的内存地址,  1 防止数组越界  2 防止向一块内存中拷贝过多的内容  3 防止使用空...
继续阅读 »


12 怎样防止指针的越界使用问题? 

  必须让指针指向一个有效的内存地址, 

1 防止数组越界 

2 防止向一块内存中拷贝过多的内容 

3 防止使用空指针 

4 防止改变const修改的指针 

5 防止改变指向静态存储区的内容 

6 防止两次释放一个指针 

7 防止使用野指针. 

 

 

13 指针的类型转换? 

指针转换通常是指针类型和void * 类型之前进行强制转换,从而与期望或返回void指针的函数进行正确的交接. 

63static有什么用途?(请至少说明两种)
            1.限制变量的作用域
            2.设置变量的存储域
            7. 引用与指针有什么区别?
            1) 引用必须被初始化,指针不必。
            2) 引用初始化以后不能被改变,指针可以改变所指的对象。
            2) 不存在指向空值的引用,但是存在指向空值的指针。
            8. 描述实时系统的基本特性
            在特定时间内完成特定的任务,实时性与可靠性

64全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
            全局变量储存在静态数据库,局部变量在堆栈
            10. 什么是平衡二叉树
            左右子树都是平衡二叉树且左右子树的深度差值的绝对值不大于1

65堆栈溢出一般是由什么原因导致的?
            没有回收垃圾资源
            12. 什么函数不能声明为虚函数?
            constructor
            13. 冒泡排序算法的时间复杂度是什么?
            O(n^2)
            14. 写出float x 与“零值”比较的if语句。
            if(x>0.000001&&x<-0.000001)
            16. Internet采用哪种网络协议?该协议的主要层次结构?
            tcp/ip 应用层/传输层/网络层/数据链路层/物理层
            17. Internet物理地址和IP地址转换采用什么协议?
            ARP (Address Resolution Protocol)(地址解析協議)
            18.IP地址的编码分为哪俩部分?
            IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区
            分哪些是网络位哪些是主机位。
            2.用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。写
            出C程序。
            循环链表,用取余操作做
            3.不能做switch()的参数类型是:
            switch的参数不能为实型。
            華為
            1、局部变量能否和全局变量重名?
            答:能,局部会屏蔽全局。要用全局变量,需要使用"::"
            局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而
            不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变
            量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那
            个循环体内
            2、如何引用一个已经定义过的全局变量?
            答:extern
            可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个
            在头文件中声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你
            用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期
            间报错
            3、全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
            答:可以,在不同的C文件中以static形式来声明同名全局变量。
            可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋
            初值,此时连接不会出错
            4、语句for( ;1 ;)有什么问题?它是什么意思?
            答:和while(1)相同。
            5、do……while和while……do有什么区别?
            答:前一个循环一遍再判断,后一个判断以后再循环

661.IP Phone的原理是什么?
            IPV6
            2.TCP/IP通信建立的过程怎样,端口有什么作用?
            三次握手,确定是哪个应用程序使用该协议
            3.1号信令和7号信令有什么区别,我国某前广泛使用的是那一种?
            4.列举5种以上的电话新业务?
            微软亚洲技术中心的面试题!!!
            1.进程和线程的差别。
            线程是指进程内的一个执行单元,也是进程内的可调度实体.
            与进程的区别:
            (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
            (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
            (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属
            于进程的资源.
            (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开
            销明显大于创建或撤消线程时的开销。
            2.测试方法
            人工测试:个人复查、抽查和会审
            机器测试:黑盒测试和白盒测试
            2.Heap与stack的差别。
            Heap是堆,stack是栈。
            Stack的空间由操作系统自动分配/释放,Heap上的空间手动分配/释放。
            Stack空间有限,Heap是很大的自由存储区
            C中的malloc函数分配的内存空间即在堆上,C++中对应的是new操作符。
            程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的
            传递也在栈上进行
            3.Windows下的内存是如何管理的?
            4.介绍.Net和.Net的安全性。
            5.客户端如何访问.Net组件实现Web Service?
            6.C/C++编译器中虚表是如何完成的?
            7.谈谈COM的线程模型。然后讨论进程内/外组件的差别。
            8.谈谈IA32下的分页机制
            小页(4K)两级分页模式,大页(4M)一级
            9.给两个变量,如何找出一个带环单链表中是什么地方出现环的?
            一个递增一,一个递增二,他们指向同一个接点时就是环出现的地方
            10.在IA32中一共有多少种办法从用户态跳到内核态?
            通过调用门,从ring3到ring0,中断从ring3到ring0,进入vm86等等
            11.如果只想让程序有一个实例运行,不能运行两个。像winamp一样,只能开一个窗
            口,怎样实现?
            用内存映射或全局原子(互斥变量)、查找窗口句柄..
            FindWindow,互斥,写标志到文件或注册表,共享内存。

67如何截取键盘的响应,让所有的‘a’变成‘b’?

            键盘钩子SetWindowsHookEx
            13.Apartment在COM中有什么用?为什么要引入?
            14.存储过程是什么?有什么用?有什么优点?
            我的理解就是一堆sql的集合,可以建立非常复杂的查询,编译运行,所以运行一次后,
            以后再运行速度比单独执行SQL快很多
            15.Template有什么特点?什么时候用?
            16.谈谈Windows DNA结构的特点和优点。
            网络编程中设计并发服务器,使用多进程与多线程,请问有什么区别?
            1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。
            2,线程:相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其
            他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
            两者都可以提高程序的并发度,提高程序运行效率和响应时间。
            线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源管理和保护;而进程
            正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
            思科

收起阅读 »

iOS 面试题 八股文 1.1

54多线程 多线程编程是防止主线程堵塞,增加运行效率等等的最佳方法。而原始的多线程方法存在很多的毛病,包括线程锁死等。在Cocoa中,Apple提供了NSOperation这个类,提供了一个优秀的多线程编程方法。 本次介绍NSOperation的子集,简易...
继续阅读 »


54多线程

多线程编程是防止主线程堵塞,增加运行效率等等的最佳方法。而原始的多线程方法存在很多的毛病,包括线程锁死等。在Cocoa中,Apple提供了NSOperation这个类,提供了一个优秀的多线程编程方法。

本次介绍NSOperation的子集,简易方法的NSInvocationOperation:

 

一个NSOperationQueue 操作队列,就相当于一个线程管理器,而非一个线程。因为你可以设置这个线程管理器内可以并行运行的的线程数量等等

55oc语法里的@perpoerty不用写@synzhesize了,自动填充了。并且的_name;

写方法时候不用提前声明。llvm 全局方法便利。

枚举类型。enum hello:Integer{  } 冒号后面直接可以跟类型,以前是:

enum hello{} 后面在指定为Integer .

桥接。ARC 自动release retain 的时候 CFString CFArray . Core Fountion. 加上桥接_brige  才能区分CFString 和NSString 而现在自动区分了,叫固定桥接。

 

下拉刷新封装好了。

UICollectionViewController. 可以把表格分成多列。

 

Social Framework(社交集成)

UIActivityViewController来询问用户的社交行为

 

缓存:就是存放在临时文件里,比如新浪微博请求的数据,和图片,下次请求看这里有没有值。

56Singleton(单例模式),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。 

代码如下: 

static ClassA *classA = nil;//静态的该类的实例 

+ (ClassA *)sharedManager 

{ 

@synchronized(self) { 

if (!classA) { 

classA = [[super allocWithZone:NULL]init]; 

return classA; 

} 

+ (id)allocWithZone:(NSZone *)zone { 

return [[self sharedManager] retain]; 

- (id)copyWithZone:(NSZone *)zone { 

return self; 

- (id)retain { 

return self; 

- (NSUIntger)retainCount { 

return NSUIntgerMax; 

- (oneway void)release { 

- (id)autorelease { 

return self; 

-(void)dealloc{ 

57请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1 int checkCPU( ) {   

     {           

       union w      

            {        

                     int a;      

                     char b;         

             } c;             

            c.a = 1;    

        return  (c.b ==1);      

  } 

剖析:嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节, Big-endian  模式的CPU对操作数的存放方式是从高字节到低字节。在弄清楚这个之前要弄清楚这个问题:字节从右到坐为从高到低! 假设从地址0x4000开始存放: 0x12345678,是也个32位四个字节的数据,最高字节是0x12,最低字节是0x78:在Little-endian模式CPU内存中的存放方式为: (高字节在高地址,低字节在低地址) 

内存地址0x4000 0x4001 0x4002 0x4003 

存放内容 0x78 0x56 0x34 0x12 

大端机则相反。 

 

有的处理器系统采用了小端方式进行数据存放,如Intel的奔腾。有的处理器系统采用了大端方式进行数据存放,如IBM半导体和Freescale的PowerPC处理器。不仅对于处理器,一些外设的设计中也存在着使用大端或者小端进行数据存放的选择。因此在一个处理器系统中,有可能存在大端和小端模式同时存在的现象。这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师,必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器,指令集,系统总线等各个层次中。   联合体union的存放顺序是所有成员都从低地址开始存放的。以上是网上的原文。让我们看看在ARM处理器上union是如何存储的呢?   地址A ---------------- |A     |A+1   |A+2   |A+3    |int a; |      |         |         |          -------------------- |A     |char b; |      | ---------                                                                            如果是小端如何存储c.a的呢?  

                                         地址A ----------- 

------------------- |A    |A+1   |A+2    |A+3 | int a; 

|0x01 |0x00   |0x00   |0x00 | ------------------------------------- |A    |char b; |     | ---------                                  

                                如果是大端如何存储c.a的呢?   

  地址A --------------------- 

--------- |A      |A+1    |A+2     |A+3     |int a; |0x00   |0x00   |0x00    |0x01    | ------------------------------------------ |A      |char b; |       | ---------                                                                                                                                                        现在知道为什么c.b==0的话是大端,c.b==1的话就是小端了吧。

58

堆和栈上的指针 

指针所指向的这块内存是在哪里分配的,在堆上称为堆上的指针,在栈上为栈上的指针. 

在堆上的指针,可以保存在全局数据结构中,供不同函数使用访问同一块内存. 

在栈上的指针,在函数退出后,该内存即不可访问. 

59什么是指针的释放? 

具体来说包括两个概念. 

1 释放该指针指向的内存,只有堆上的内存才需要我们手工释放,栈上不需要. 

2 将该指针重定向为NULL. 

60数据结构中的指针? 

其实就是指向一块内存的地址,通过指针传递,可实现复杂的内存访问. 

7 函数指针? 

指向一块函数的入口地址. 

 

8 指针作为函数的参数? 

比如指向一个复杂数据结构的指针作为函数变量 

这种方法避免整个复杂数据类型内存的压栈出栈操作,提高效率. 

注意:指针本身不可变,但指针指向的数据结构可以改变. 

 

9 指向指针的指针? 

指针指向的变量是一个指针,即具体内容为一个指针的值,是一个地址. 

此时指针指向的变量长度也是4位. 

61指针与地址的区别? 

区别: 

1指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在. 

2 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址. 

   地址表示内存空间的一个位置点,他是用来赋给指针的,地址本身是没有大小概念,指针指向变量的大小,取决于地址后面存放的变量类型. 

62指针与数组名的关系? 





































































































  其值都是一个地址,但前者是可以移动的,后者是不可变的. 

收起阅读 »

iOS HTTP协议详解

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中。  http(超文本传输协议)是一个基于请求与响应模式的、无状态...
继续阅读 »


HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中。

 http(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。
HTTP协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

48URL

HTTP URL (URL是一种特殊类型的URI是他的子类,包含了用于查找某个资源的足够的信息)的格式如下:
http://host[":"port][abs_path ]
http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用缺省端口80;abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。

49TCP/UDP区别联系

TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。 

UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快 

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,我们来看看这三次对话的简单过程:1.主机A向主机B发出连接请求数据包;2.主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包;3.主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。 

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!  UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。 

tcp协议和udp协议的差别 

是否连接面向连接面向非连接 

传输可靠性可靠不可靠 

应用场合传输大量数据少量数据 

速度慢快

50 socket 连接和 http 连接的区别

简单说,你浏览的网页(网址以http://开头)都是http协议传输到你的浏览器的, 而http是基于socket之上的。socket是一套完成tcp,udp协议的接口。

HTTP协议:简单对象访问协议,对应于应用层  ,HTTP协议是基于TCP连接的

tcp协议:    对应于传输层

ip协议:     对应于网络层 
TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

http连接:http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉;

socket连接:socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源。所以当一个socket连接中没有数据的传输,那么为了维持连接需要发送心跳消息~~具体心跳消息格式是开发者自己定义的

我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
1)Socket是一个针对TCP和UDP编程的接口,你可以借助它建立TCP连接等等。而TCP和UDP协议属于传输层 。
  而http是个应用层的协议,它实际上也建立在TCP协议之上。 

 (HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。)

 2)Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口。

51 什么是 TCP 连接的三次握手

第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)

52 利用 Socket 建立网络连接的步骤

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

1。服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

2。客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

3。连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

53进程与线程

进程(process)是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。

进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。

通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。

在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。

由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。

简而言之 , 一个程序至少有一个进程 , 一个进程至少有一个线程 .一个程序就是一个进程,而一个程序中的多个任务则被称为线程。

 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。应用程序(application)是由一个或多个相互协作的进程组成的。

另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

收起阅读 »

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

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

你好呀,我是歪歪。


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


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


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


他还给我抱怨:



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


确实是存在感非常低。


要不我就安排一下?



mainLock


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


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


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


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


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



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


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


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



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



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


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


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


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



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


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


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


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


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


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


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



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


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



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


同样也是线程安全的。


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


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


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



为什么 largestPoolSize 不用呢?


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


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


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


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


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


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


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


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



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


说个有意思的。


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


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


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


就在 mainLock 的注释上写着:



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


首先看这句:



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



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


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


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


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


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


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


第一个原因是这样说的:



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



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


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


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


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



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


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


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


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


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


因为 shutdown 方法调用了 interruptIdleWorkers:



所以上面啥意思呢?


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


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


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


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


额,有点像是绕口令了。


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


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


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


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


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


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


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


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



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



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


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


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



另一把锁


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


那就是 Worker 对象。



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



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


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



java.util.concurrent.ThreadPoolExecutor#runWorker




那么问题就来了:


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


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


答案还是写在注释里面:



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


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


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



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



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


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


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


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


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


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



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


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



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



java.util.concurrent.ThreadPoolExecutor#getTask




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


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


我们看一下判断条件:



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


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



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


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


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



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


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


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


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


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


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


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


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


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



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



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


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



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


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



-1 是从哪里来的呢?


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


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



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


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


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


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



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



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


接着往下看注释:



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



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


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


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



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


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



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


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



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


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


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


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


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



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


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


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


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


怎么减少呢?


调用 interruptIdleWorkers 方法。


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



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


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


这合适吗?



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



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



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


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


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


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


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



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


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



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


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



最后说一句


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


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



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

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

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

1、简介


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


public static void fruitsHandle(String fruits) {

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

}

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


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

public static void fruitsHandle(String fruits) {

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

}

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


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




2、优势


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




2.1 类型安全


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


// 肉类枚举
public enum MeetEnums {

BEEF,
PORK,
FISH;

}

// 水果枚举
public enum FruitsEnums {

APPLE,
BANANA,
ORANGE;

}

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


public static void fruitsHandle(FruitsEnums fruits) {

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

}

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



2.2 枚举能够提供更多信息


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


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


public enum HttpStatus {

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

private final int value;
private final String reasonPhrase;

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

}

2.3 通过函数提供更多服务


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


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

private final int value;

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

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

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

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

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

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

return null;
}
}

2.4 获取所有定义的类型


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


public enum MeetEnums {

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

String name;

public String getName() {
return name;
}

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

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

}

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


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

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

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

前言


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


image.png


拆分原则


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


image.png


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



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

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

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

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

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

  • 可以使用 key 提高性能。

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


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


示例对比


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


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

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

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


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

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

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

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


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

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

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

class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();

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

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


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


解密 Flutter 的 const 关键字


总结


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


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

总结 scripts 阻塞 HTML 解析

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树。 内联 scripts <html> <head&...
继续阅读 »

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树


内联 scripts


<html>
<head></head>
<body>
 <script>
console.log('irene')
 </script>
</body>
</html>

解析 HTML 过程中遇到 内联 scripts 会暂停解析,先执行 scripts,然后继续解析 HTML。


普通外联 scripts


<script src="index.js"></script>

解析 HTML 过程中遇到 普通外联 scripts 会暂停解析,发送请求并执行 scripts,然后继续解析 HTML。如下图所示,绿色表示 HTML 解析;灰色表示 HTML 解析暂停;蓝色表示 scripts 下载;粉色表示 scripts 执行。


image.png


defer scripts


<script defer src="index.js"></script>

解析 HTML 过程中遇到 defer scripts 不会停止解析,scripts 也会并行下载;等整个 HTML 解析完成后按引用 scripts 的顺序执行。defer scripts 在 DOMContentLoaded 事件触发之前执行。defer 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 defer scripts,文件小的 scripts 很可能先下载完,defer 属性除了告诉浏览器不去阻塞 HTML 解析,同时还保证了defer scripts 的相对顺序。即使 small.js 先下载完,它还是得等到 long.js 执行完再去执行。


async scripts


<script async src="index.js"></script>

解析 HTML 过程中遇到 async scripts 不会停止解析,scripts 也会并行下载;scripts 下载完之后开始执行,阻塞 HTML 解析。async scripts 的执行顺序和它的引用顺序不一定相同。async scripts 可能在 DOMContentLoaded 事件触发之前或之后执行。如果 HTML 先解析完 async scripts 才下载完成,此时 DOMContentLoaded 事件已经触发, async scripts 很有可能来不及监听 DOMContentLoaded 事件。async 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 async scripts,文件小的 scripts 很可能先下载完,先下载完就先执行了,它无法保证按 async scripts 的引用顺序执行。


defer VS async


在实践中,defer 用于需要整个 DOM 或其相对执行顺序很重要的 scripts。而 async 则用于独立的 scripts,如计数器或广告,而它们的相对执行顺序并不重要。


dynamic scripts


let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script);

脚本一旦被 append 到文档中就开始下载,动态脚本在默认情况下表现的像 async scripts,即先下载完先执行;可以显示设置 script.async = false,这样 scripts 的执行顺序就会和 defer scripts 表现的一致。


这两篇文章中,文一说 defer scripts 会阻塞 HTML 解析,文二说 defer scripts 不会阻塞 HTML 解析。其实两者的想法是一致的:即 defer scripts 的下载不会阻塞 HTML 解析,且执行是在构建完 DOM 之后;之所以有两种不同的表述是因为文一定义阻塞 HTML 解析的标准:是否在 DOMContentLoaded 之前执行,在之前执行就是阻塞 HTML 解析,否则就是不会;defer scripts 是在构建完 DOM 之后,DOMContentLoaded 之前执行的,所有文一认为 defer scripts 会阻塞 HTML 解析。文二说 defer scripts 不会阻塞 HTML 解析就很好理解了。


作者:小被子
链接:https://juejin.cn/post/7027673904927735822

收起阅读 »

手把手教你封装一个日期格式化的工具函数

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西? "createTime" : "2021-01-17T13:32:06.381Z", "lastLogi...
继续阅读 »

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西?


"createTime" : "2021-01-17T13:32:06.381Z",
"lastLoginTime" : "2021-01-17T13:32:06.381Z"

直接CV到百度,查出来这一串是一种时间格式,下面放上它的解释:



T表示分隔符,Z表示的是UTC.
UTC:世界标准时间,在标准时间上加上8小时,即东八区时间,也就是北京时间。


另:还有别的时间格式和时间戳,想了解的小伙伴可以百度了解一下哦,免得跟我一样,看到了才想着去百度了解,事先了解一下,没坏处的。



了解完了,现在我应该做的,就是将这个时间变成我们大家看得懂的那种格式,并将它渲染到页面上。


开始上手


JavaScript中,处理日期和时间,当然要用到我们的Date对象,所以我们先来写出这个函数的雏形:


const formateDate = (value)=>{
let date = new Date(value)
}

下面要做的应该是定义日期的格式了,这里我用的是yyyy-MM-dd hh:mm:ss


let fmt = 'yyyy-MM-dd hh:mm:ss'

因为年月日时分秒这里都是两位或者两位以上的,所以在获取的时候我是这样定义的:


const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}

首先先解释一下getMonth()+1,去查看Date文档就知道,这个函数的返回是0-11,我们正常月份都是1-12,所以加上1,才是正确的月份。


定义了规则之后,我们循环它,应该就可以得到我们想要的结果了吧。


for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,val)
}
}

我们继续来解释一下代码,首先fmt.replace是代表我们要做一个替换,RegExp.$1就是获取到上面的值表达式内容,将这个内容,换成val中的值,之所以上面加了一个空字符串,是为了将val变成字符串的形式,以防再出纰漏。


$1.png


我们渲染上去,看看结果如何?


秒未补零.png


日期被我们成功的转化为了,我们能看得懂的东西,但是我们可以看到,秒这里,只有一位,也就是说,在秒只有个位数的情况下,我们应该给予它一个补零的操作。



不光是秒,其他也应该是这个道理哦!



关于补零


补零的话,有两种方式,先来说说笨笨的这种吧:


我们去判断这个字符串的长度,如果是1,我们就加个零,如果不是1,那么就不用加。


var a = '6'
a.length = 1?'0'+a:a // '06'

再来说个略微比这个高级一点的:


我们需要两位,所以直接给字符串补上两个零,再用substr去分割一下字符串,就能得到我们想要的了。


var b = '6'
var result = ('00'+b).substr(b.length) // '06'

那么我们去改一下上面的代码,就得到了下面的函数:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

在刷新一下网页,看看我们成功了没!


补零结束.png


成功是成功了,但是我们发现,前面的年竟然被干掉了,他也变成了两位的样子,这可不行啊,我们定义的年份格式可是四位的。


这可咋整.webp


但是别慌,这个只需要把年份单独的去做判断,不与其他2位的格式一起进行操作就能解决啦,所以我们最终的函数是这样的:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
if(/(y+)/.test(fmt)){
fmt = fmt.replace(RegExp.$1,date.getFullYear())
}
const o = {
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

看一下结果吧:


image.png


如果再严谨一点,可以再给函数加个参数,传递一个rule,这样方便我们后期进行调整数据格式,在定义格式的时候用||就好了。


let fmt = '传入的新格式' || '默认的格式
收起阅读 »

用CSS告诉你为何大橘为重!!

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~ 还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无...
继续阅读 »

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~


VID_20211030_184225.gif


还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无精打采的。但当鼠标(鱼)移入,橘子一看见最喜欢的鱼立马就开心了,连天气都变好了,对,这只橘子就是这么馋,变成胖橘是有原因的。


好了,我们马上就要进入正文了,我们会从基础搭建,太阳,云,猫的绘制和动画去了解制作这个动画的流程。


正文


1.搭建与结构


yarn add vite sass sass-loader

我们是用vite和sass去完成项目的构建,和样式的书写,所以我们先安装下他们。


<div id="app">
<div class="warrper">
<div class="sun"></div>
<div class="cloud"></div>
<div class="cat">
<div class="eye left"><div class="eye-hide"></div></div>
<div class="eye right"><div class="eye-hide"></div></div>
<div class="nose"></div>
<div class="mouth"></div>
</div>
</div>
</div>

在html我们先写出结构来。div#app作为主界面去填满一屏,而div.warrper就作为主要内容的展示区域也就是那个圆圈。然后,在圆圈里面我们放太阳div.sun,云朵div.cloud,猫div.cat,当然猫里面还有眼睛鼻子嘴巴这些,至于猫的耳朵就用两个伪类做个三角形去实现。


2.变量与界面


$cat:rgb(252, 180, 125);

:root{
--bgColor:rgb(81, 136, 168);
--eyeHideTop:0px;
--cloudLeft:45%;
--mouthRadius:10px 10px 0 0;
}
#app{
width: 100%;
height: 100vh;
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-image: repeating-linear-gradient(0deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),repeating-linear-gradient(90deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),linear-gradient(90deg, rgb(255,255,255),rgb(255,255,255));
}

.warrper{
width: 320px;
height: 320px;
border-radius: 50%;
border: 10px solid white;
position: relative;
overflow: hidden;
background-color: var(--bgColor);
transition: background-color 1s linear;
cursor:url("./assets/fish.png"),default;
&:hover{
--bgColor:rgb(178, 222, 247);
--eyeHideTop:-20px;
--cloudLeft:100%;
--mouthRadius:0 0 10px 10px;
}
}

我们先定义猫的主色调,还有一些要变化的颜色和距离,因为我们移入将通过css3去改变这些属性,来达到某些动画的实现。


我们期望的是,当鼠标移入圆圈后,天空变晴,云朵退散,猫开心充满精神,所以,bgColor:天空颜色,eyeHideTop猫的眼皮y轴距离,cloudLeft云朵x轴偏移距离,mouthRadius猫嘴巴的圆角值。目前来说,当鼠标移入div.warrper后,这些值都会发生变化。另外,我自定义了鼠标图标移入圆圈变成了一条鱼(即cursor:url(图片地址))。这里的hover后的值是我事先算好的,如果大家重新开发别的动画可以一边做一边算。


微信截图_20211030200310.png


3.太阳与云朵


.sun{
width: 50px;
height: 50px;
position: absolute;
background-color: rgb(255, 229, 142);
border:7px solid rgb(253, 215, 91);
border-radius: 50%;
left: 55%;
top: 14%;
box-shadow: 0 0 6px rgb(255, 241, 48);
}

太阳我们就画个圆圈定好位置,然后用box-shadow投影去完成一点发光的效果。


微信截图_20211030200343.png


然后,我们再开始画云朵~


.cloud{
width: 100px;
height: 36px;
background-color: white;
position: absolute;
transition: left .6s linear;
left: var(--cloudLeft);
top: 23%;
border-radius: 36px;
animation: bouncy 2s ease-in-out infinite;
&::before{
content: '';
width: 50px;
height: 50px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -23px;
left: 18px;
}
&::after{
content: '';
width: 26px;
height: 26px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -16px;
left: 56px;
}
}

@keyframes bouncy {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}

云朵很简单,我们就是画一个圆角矩形,然后用两个伪类画一个大圆和小圆叠在一起就非常像云了,另外,我们再加个animation动画,让他时大时小,有动的感觉。


微信截图_20211030200357.png


4.橘猫与动画


.cat{
width: 180px;
height: 160px;
background-color: $cat;
position: absolute;
bottom: -20px;
left: 50%;
margin-left: -90px;
animation: wait 2s ease-in-out infinite;
&::after,
&::before{
content: '';
display: block;
border-style: solid;
border-width: 20px 30px;
position: absolute;
top: -30px;
}
&::after{
right: 0;
border-color: transparent $cat $cat transparent;
}
&::before{
left: 0;
border-color: transparent transparent $cat $cat;
}
.eye{
width: 42px;
height: 42px;
border-radius: 50%;
position: absolute;
top: 30px;
background:white;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
.eye-hide{
height: 20px;
position: absolute;
top: var(--eyeHideTop);
left: -2px;
right:-2px;
background-color: $cat;
transition: top .5s ease-in-out;
z-index: 2;
}
&::before{
content: "";
height: 36px;
width: 36px;
background-color:black;
border-radius: 50%;
}
&::after{
content: "";
width: 24px;
height: 24px;
background-color: white;
border-radius: 50%;
position: absolute;
right: 0px;
top: 0px;
}
&.left{
left: 24px;
}
&.right{
right: 24px;
}
}
.nose{
width: 0;
height: 0;
border-top: 7px solid rgb(248, 226, 226);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
position: absolute;
left: 50%;
margin-left: -7px;
top: 70px;
}
.mouth{
width: 26px;
height: 20px;
background-color: rgb(255, 217, 217);
position: absolute;
top: 85px;
left: 50%;
margin-left: -13px;
border-radius: var(--mouthRadius);
transition: border-radius .2s linear;
overflow: hidden;
&::after,
&::before{
content: "";
position: absolute;
display: block;
top: 0;
border-top: 7px solid white;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
}
&::after{
right: 5px;
}
&::before{
left: 5px;
}
}
}

@keyframes wait{
0% {
bottom: -20px;
}
50% {
bottom: -25px;
}
100% {
bottom: -20px;
}
}

我们可以实现分解出,耳朵(伪类)+ 一双眼睛 + 鼻子 + 嘴(包含两颗尖牙) = 猫。


通过以上代码就不难看出主要都是在使用绝对定位来完成,面部器官的摆放。绝大部分都是css基础代码来实现的。唯一可以注意的点,就是耳朵这个三角形,我们是通过伪类实现,将它不设置宽高,而主是通过border-width+boder-color这个技巧去绘制出三角形的,算是个css小技巧吧,后面的鼻子和嘴巴里的尖牙都是这个小技巧来实现的。


另外,还要说的是那双眼睛,我们用先填充白底再分别用伪类去实现里面的黑底圆和白色小圆,肯定有同学问了为什么不用border是实现白色圆框,就不用浪费一个伪类去完成黑底圆了?因为我们用了overflow: hidden,他多余隐藏的内容是border以下的元素,而border边框可以无损,那么他的伪类能盖不住他的border,这样显得眼皮垂下的圆圈还是很大不自然,所以我们又造了一个伪类去实现他的黑底,让外圆不使用border了。


剩下的就是做一个等待的animation动画给猫,让他上下移动着,来实现不停的呼吸的效果。


微信截图_20211030200539.png


这样一直无精打采的橘猫就完成了。因为在第一部分,我们事先已经把移入后改变的变量算好了,现在把鼠标移入,效果就出现咯~


微信截图_20211030200546.png


结语


讲到这里我们就已经完成了这个动画了,不得不说,看见食物这么激动不愧都叫他胖橘!


这里有我这个动画【I Like Fish】codepen地址可以看到演示和代码,有兴趣的小伙伴可以康康。


本期还是比较侧重基础和动画创意的,主要是新手向,大佬勿喷,经常用css写写动画挺有意思的,不仅可以熟悉基本功,而且会迸发出很多创意来,也是一种锻炼自己的学习方式吧,多练习下,大家一起加油鸭~



收起阅读 »

你需要知道的 19 个 console 实用调试技巧

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。 如今,我们项目的开发通常会使用React、Vue等前端框...
继续阅读 »

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。


如今,我们项目的开发通常会使用React、Vue等前端框架,前端调试也变得更加有难度,除了使用React Dev Tools,Vue Dev Tools等插件之外,我们使用最多的就是console.log(),当然多数情况下,console.log()就能满足我们的需求,但是当数据变得比较复杂时,console.log()就显得有些单一。其实console对象为我们提供了很多打印的方法,下面是console对象包含的方法(这里使用的是Chrome浏览器,版本为 95.0.4638.54(正式版本) (arm64)):


image.png


console 对象提供了浏览器控制台调试的接口,我们可以从任何全局对象中访问到它,如果你平时只是用console.log()来输出一些变量,那你可能没有用过console那些强大的功能。下面带你用console玩玩花式调试。


一、基本打印


1. console.log()


console.log()就是最基本、最常用的用法了。它可以用在JavaScript代码的任何地方,然后就可以浏览器的控制台中看到打印的信息。其基本使用方法如下:


let name = "CUGGZ";
let age = 18;
console.log(name) // CUGGZ
console.log(`my name is: ${name}`) // CUGGZ
console.log(name, age) // CUGGZ 18
console.log("message:", name, age) // message: CUGGZ 18

除此之外,console.log()还支持下面这种输出方式:


let name = "CUGGZ";
let age = 18;
let height = 180;
console.log('Name: %s, Age: %d', name, age) // Name: CUGGZ, Age: 18
console.log('Age: %d, Height: %d', age, height) // Age: 18, Height: 180

这里将后面的变量赋值给了前面的占位符的位置,他们是一一对应的。这种写法在复杂的输出时,能保证模板和数据分离,结构更加清晰。不过如果是简单的输出,就没必要这样写了。在console.log中,支持的占位符格式如下:



  • 字符串:%s

  • 整数:%d

  • 浮点数:%f

  • 对象:%o或%O

  • CSS样式:%c


可以看到,除了最基本的几种类型之外,它还支持定义CSS样式:


let name = "CUGGZ";
console.log('My Name is %cCUGGZ', 'color: skyblue; font-size: 30px;')

打印结果如下(好像并没有什么卵用):


image.png


这个样式打印可能有用的地方就是打印图片,用来查看图片是否正确:


console.log('%c ','background-image:url("http://iyeslogo.orbrand.com/150902Google/005.gif");background-size:120% 120%;background-repeat:no-repeat;background-position:center center;line-height:60px;padding:30px 120px;');

打印结果如下:


image.png


严格地说,console.log()并不支持打印图片,但是可以使用CSS的背景图来打印图片,不过并不能直接打印,因为是不支持设置图片的宽高属性,所以就需要使用line-heigh和padding来撑开图片,使其可以正常显示出来。


我们可以使用console.log()来打印字符画,就像知乎的这样:


image.png


可以使用字符画在线生成工具,将生成的字符粘贴到console.log()即可。在线工具:mg2txt。我的头像生成效果如下,中间的就是生成的字符:


image.png


除此之外,可以看到,当占位符表示一个对象时,有两种写法:%c或者%C,那它们两个有什么区别呢?当我们指定的对象是普通的object对象时,它们两个是没有区别的,如果是DOM节点,那就有有区别了,来看下面的示例:


image.png


可以看到,使用 %o 打印的是DOM节点的内容,包含其子节点。而%O打印的是该DOM节点的对象属性,可以根据需求来选择性的打印。


2. console.warn()


console.warn() 方法用于在控制台输出警告信息。它的用法和console.log是完全一样的,只是显示的样式不太一样,信息最前面加一个黄色三角,表示警告:


const app = ["facebook", "google", "twitter"];
console.warn(app);

打印样式如下:


image.png


3. console.error()


console.error()可以用于在控制台输出错误信息。它和上面的两个方法的用法是一样的,只是显示样式不一样:


const app = ["facebook", "google", "twitter"];
console.error(app)

image.png


需要注意,console.exception() 是 console.error() 的别名,它们功能是相同的。


当然,console.error()还有一个console.log()不具备的功能,那就是打印函数的调用栈:


function a() {
b();
}
function b() {
console.error("error");
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里打印出来了函数函数调用栈的信息:b→a→c。


console对象提供了专门的方法来打印函数的调用栈(console.trace()),这个下面会介绍到。


4. console.info()


console.info()可以用来打印资讯类说明信息,它和console.log()的用法一致,打印出来的效果也是一样的:


image.png


二、打印时间


1. console.time() & console.timeEnd()


如果我们想要获取一段代码的执行时间,就可以使用console对象的console.time() 和console.timeEnd()方法,来看下面的例子:


console.time();

setTimeout(() => {
console.timeEnd();
}, 1000);

// default: 1001.9140625 ms

它们都可以传递一个参数,该参数是一个字符串,用来标记唯一的计时器。如果页面只有一个计时器时,就不需要传这个参数 ,如果有多个计时器,就需要使用这个标签来标记每一个计时器:


console.time("timer1");
console.time("timer2");

setTimeout(() => {
console.timeEnd("timer1");
}, 1000);

setTimeout(() => {
console.timeEnd("timer2");
}, 2000);

// timer1: 1004.666259765625 ms
// timer2: 2004.654052734375 ms

2. console.timeLog()


这里的console.timeLog()上面的console.timeEnd()类似,但是也有一定的差别。他们都需要使用console.time()来启动一个计时器。然后console.timeLog()就是打印计时器当前的时间,而console.timeEnd()是打印计时器,直到结束的时间。下面来看例子:


console.time("timer");

setTimeout(() => {
console.timeLog("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);

// timer: 1002.80224609375 ms
// timer: 3008.044189453125 ms

而使用console.timeEnd()时:


console.time("timer");

setTimeout(() => {
console.timeEnd("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);


打印结果如下:


image.png


可以看到,它会终止当前的计时器,所以里面的timeLog就无法在找到timer计数器了。
所以两者的区别就在于,是否会终止当前的计时。


三、分组打印


1. console.group() & console.groupEnd()


这两个方法用于在控制台创建一个信息分组。 一个完整的信息分组以 console.group() 开始,console.groupEnd() 结束。来看下面的例子:


console.group();
console.log('First Group');
console.group();
console.log('Second Group')
console.groupEnd();
console.groupEnd();

打印结果如下:


image.png


再来看一个复杂点的:


console.group("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.group("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

打印结果如下:


image.png


可以看到,这些分组是可以嵌套的。当前我们需要调试一大堆调试输出,就可以选择使用分组输出,


2. console.groupCollapsed()


console.groupCollapsed()方法类似于console.group(),它们都需要使用console.groupEnd()来结束分组。不同的是,该方法默认打印的信息是折叠展示的,而group()是默认展开的。来对上面的例子进行改写:


console.groupCollapsed("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.groupCollapsed("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

其打印结果如下:


image.png


可以看到,和上面方法唯一的不同就是,打印的结果被折叠了,需要手动展开来看。


四、打印计次


1. console.count()


可以使用使用console.count()来获取当前执行的次数。来看下面的例子:


for (i = 0; i < 5; i++) {
console.count();
}

// 输出结果如下
default: 1
default: 2
default: 3
default: 4
default: 5

它也可以传一个参数来进行标记(如果为空,则为默认标签default):


for (i = 0; i < 5; i++) {
console.count("hello");
}

// 输出结果如下
hello: 1
hello: 2
hello: 3
hello: 4
hello: 5

这个方法主要用于一些比较复杂的场景,有时候一个函数被多个地方调用,就可以使用这个方法来确定是否少调用或者重复调用了该方法。


2. console.countReset()


顾名思义,console.countReset()就是重置计算器,它会需要配合上面的console.count()方法使用。它有一个可选的参数label:



  • 如果提供了参数label,此函数会重置与label关联的计数,将count重置为0。

  • 如果省略了参数label,此函数会重置默认的计数器,将count重置为0。


console.count(); 
console.count("a");
console.count("b");
console.count("a");
console.count("a");
console.count();
console.count();

console.countReset();
console.countReset("a");
console.countReset("b");

console.count();
console.count("a");
console.count("b");

打印结果如下:


default:1
a:1
b:1
a:2
a:3
default:2
default:3
default:1
a:1
b:1

五、其他打印


1. console.table()


我们平时使用console.log较多,其实console对象还有很多属性可以使用,比如console.table(),使用它可以方便的打印数组对象的属性,打印结果是一个表格。console.table() 方法有两个参数,第一个参数是需要打印的对象,第二个参数是需要打印的表格的标题,这里就是数组对象的属性值。来看下面的例子:


const users = [ 
{
"first_name":"Harcourt",
"last_name":"Huckerbe",
"gender":"Male",
"city":"Linchen",
"birth_country":"China"
},
{
"first_name":"Allyn",
"last_name":"McEttigen",
"gender":"Male",
"city":"Ambelókipoi",
"birth_country":"Greece"
},
{
"first_name":"Sandor",
"last_name":"Degg",
"gender":"Male",
"city":"Mthatha",
"birth_country":"South Africa"
}
]

console.table(users, ['first_name', 'last_name', 'city']);

打印结果如下:


image.png


通过这种方式,可以更加清晰的看到数组对象中的指定属性。


除此之外,还可以使用console.table()来打印数组元素:


const app = ["facebook", "google", "twitter"];
console.table(app);

打印结果如下:
image.png
通过这种方式,我们可以更清晰的看到数组中的元素。


需要注意,console.table() 只能处理最多1000行,因此它可能不适合所有数据集。但是也能适用于多数场景了。


2. console.clear()


console.clear() 顾名思义就是清除控制台的信息。当清空控制台之后,会打印一句:“Console was clered”:


image.png


当然,我们完全可以使用控制台的清除键清除控制台:


image.png


3. console.assert()


console.assert()方法用于语句断言,当断言为 false时,则在信息到控制台输出错误信息。它的语法如下:


console.assert(expression, message)

它有两个参数:



  • expression: 条件语句,语句会被解析成 Boolean,且为 false 的时候会触发message语句输出;

  • message: 输出语句,可以是任意类型。



该方法会在expression条件语句为false时,就会打印message信息。当在特定情况下才输出语句时,就可以使用console.assert()方法。


比如,当列表元素的子节点数量大于等于100时,打印错误信息:


console.assert(list.childNodes.length < 100, "Node count is > 100");

其输出结果如下图所示:


image.png


4. console.trace()


console.trace()方法可以用于打印当前执行的代码在堆栈中的调用路径。它和上面的console.error()的功一致,不过打印的样式就和console.log()是一样的了。来看下面的例子:


function a() {
b();
}
function b() {
console.trace();
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里输出了调用栈的信息:b→a→c,这个堆栈信息是从调用位置开始的。


5. console.dir()


console.dir()方法可以在控制台中显示指定JavaScript对象的属性,并通过类似文件树样式的交互列表显示。它的语法如下:


console.dir(object);

它的参数是一个对象,最终会打印出该对象所有的属性和属性值。


在多数情况下,使用consoledir()和使用console.log()的效果是一样的。但是当打印元素结构时,就会有很大的差异了,console.log()打印的是元素的DOM结构,而console.dir()打印的是元素的属性:


image.png


image.png


6. console.dirxml()


console.dirxml()方法用于显示一个明确的XML/HTML元素的包括所有后代元素的交互树。 如果无法作为一个element被显示,那么会以JavaScript对象的形式作为替代。 它的输出是一个继承的扩展的节点列表,可以让你看到子节点的内容。其语法如下:


console.dirxml(object);

该方法会打印输出XML元素及其后代元素,对于XML和HTML元素调用console.log()和console.dirxml()是等价的。


image.png


7. console.memory


console.memory是console对象的一个属性,而不是一个方法。它可以用来查看当前内存的使用情况,如果使用过多的console.log()会占用较多的内存,导致浏览器出现卡顿情况。


image.png



收起阅读 »

淦,为什么 "???".length !== 3

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。 '吉'.length // 1 '𠮷'.length // 2 '❤'.le...
继续阅读 »

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。


'吉'.length
// 1

'𠮷'.length
// 2

'❤'.length
// 1

'💩'.length
// 2

要解释这个问题要从 UTF-16 编码说起。


UTF-16


ECMAScript® 2015 规范中可以看到,ECMAScript 字符串使用的是 UTF-16 编码。



定与不定: UTF-16 最小的码元是两个字节,即使第一个字节可能都是 0 也要占位,这是固定的。不定是对于基本平面(BMP)的字符只需要两个字节,表示范围 U+0000 ~ U+FFFF,而对于补充平面则需要占用四个字节 U+010000~U+10FFFF



在上一篇文章中,我们有介绍过 utf-8 的编码细节,了解到 utf-8 编码需要占用 1~4 个字节不等,而使用 utf-16 则需要占用 2 或 4 个字节。来看看 utf-16 是怎么编码的。


UTF-16 的编码逻辑


UTF-16 编码很简单,对于给定一个 Unicode 码点 cp(CodePoint 也就是这个字符在 Unicode 中的唯一编号):



  1. 如果码点小于等于 U+FFFF(也就是基本平面的所有字符),不需要处理,直接使用。

  2. 否则,将拆分为两个部分 ((cp – 65536) / 1024) + 0xD800((cp – 65536) % 1024) + 0xDC00 来存储。



Unicode 标准规定 U+D800...U+DFFF 的值不对应于任何字符,所以可以用来做标记。



举个具体的例子:字符 A 的码点是 U+0041,可以直接用一个码元表示。


'\u0041'
// -> A

A === '\u0041'
// -> true

Javascript 中 \u 表示 Unicode 的转义字符,后面跟着一个十六进制数。


而字符 💩 的码点是 U+1f4a9,处于补充平面的字符,经过 👆 公式计算得到两个码元 55357, 56489 这两个数字用十六进制表示为 d83d, dca9,将这两个编码结果组合成代理对。


'\ud83d\udca9'
// -> '💩'

'💩' === '\ud83d\udca9'
// -> true

由于 Javascript 字符串使用 utf-16 编码,所以可以正确将代理对 \ud83d\udca9 解码得到码点 U+1f4a9


还可以使用 \u + {},大括号中直接跟码点来表示字符。看起来长得不一样,但他们表示的结果是一样的。


'\u0041' === '\u{41}'
// -> true

'\ud83d\udca9' === '\u{1f4a9}'
// -> true


可以打开 Dev Tool 的 console 面板,运行代码验证结果。



所以为什么 length 判断会有问题?


要解答这个问题,可以继续查看 规范,里面提到:在 ECMAScript 操作解释字符串值的地方,每个元素都被解释为单个 UTF-16 代码单元。



Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit.



所以像💩 字符实际上占用了两个 UTF-16 的码元,也就是两个元素,所以它的 length 属性就是 2。(这跟一开始 JS 使用 USC-2 编码有关,当初以为 65536 个字符就可以满足所有需求了)


但对于普通用户而言,这就完全没办法理解了,为什么明明只填了一个 '𠮷',程序上却提示占用了两个字符长度,要怎样才能正确识别出 Unicode 字符长度呢?


我在 Antd Form 表单使用的 async-validator 包中可以看到下面这段代码


const spRegexp = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g;

if (str) {
val = value.replace(spRegexp, '_').length;
}

当需要进行字符串长度的判断时,会将码点范围在补充平面的字符全部替换为下划线,这样长度判断就和实际显示的一致了!!!


ES6 对 Unicode 的支持


length 属性的问题,主要还是最初设计 JS 这门语言的时候,没有考虑到会有这么多字符,认为两个字节就完全可以满足。所以不止是 length,字符串常见的一些操作在 Unicode 支持上也会表现异常


下面的内容将介绍部分存在异常的 API 以及在 ES6 中如何正确处理这些问题。


for vs for of


例如使用 for 循环打印字符串,字符串会按照 JS 理解的每个“元素”遍历,辅助平面的字符将会被识别成两个“元素”,于是出现“乱码”。


var str = '👻yo𠮷'
for (var i = 0; i < str.length; i ++) {
console.log(str[i])
}

// -> �
// -> �
// -> y
// -> o
// -> �
// -> �

而使用 ES6 的 for of 语法就不会。


var str = '👻yo𠮷'
for (const char of str) {
console.log(char)
}

// -> 👻
// -> y
// -> o
// -> 𠮷

展开语法(Spread syntax)


前面提到了使用正则表达式,将辅助平面的字符替换的方式来统计字符长度。使用展开语法也可以得到同样的效果。


[...'💩'].length
// -> 1

slice, split, substr 等等方法也存在同样的问题。


正则表达式 u


ES6 中还针对 Unicode 字符增加了 u 描述符。


/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true

charCodeAt/codePointAt


对于字符串,我们还常用 charCodeAt 来获取 Code Point,对于 BMP 平面的字符是可以适用的,但是如果字符是辅助平面字符 charCodeAt 返回结果就只会是编码后第一个码元对于的数字。


'羽'.charCodeAt(0)
// -> 32701
'羽'.codePointAt(0)
// -> 32701

'😸'.charCodeAt(0)
// -> 55357
'😸'.codePointAt(0)
// -> 128568

而使用 codePointAt 则可以将字符正确识别,并返回正确的码点。


String.prototype.normalize()


由于 JS 中将字符串理解成一串两个字节的码元序列,判断是否相等是根据序列的值来判断的。所以可能存在一些字符串看起来长得一模一样,但是字符串相等判断结果确是 false


'café' === 'café'
// -> false

上面代码中第一个 café 是有 cafe 加上一个缩进的音标字符\u0301组成的,而第二个 café 则是由一个 caf + é 字符组成的。所以两者虽然看上去一样,但码点不一样,所以 JS 相等判断结果为 false


'cafe\u0301'
// -> 'café'

'cafe\u0301'.length
// -> 5

'café'.length
// -> 4

为了能正确识别这种码点不一样,但是语意一样的字符串判断,ES6 增加了 String.prototype.normalize 方法。


'cafe\u0301'.normalize() === 'café'.normalize()
// -> true

'cafe\u0301'.normalize().length
// -> 4

总结


这篇文章主要是我最近重新学习编码的学习笔记,由于时间仓促 && 水平有限,文章中必定存在大量不准确的描述、甚至错误的内容,如有发现还请善意指出。❤️



收起阅读 »

可折叠式标题栏

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

CollapsingToolbarLayout

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

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

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

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

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

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

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

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

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

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

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

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

</androidx.coordinatorlayout.widget.CoordinatorLayout>

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

NestedScrollView

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

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

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

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

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

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

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

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

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

MainActivity→FruitActivity

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

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

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

FruitActivity进行数据处理。

       ImageView imageView=findViewById(R.id.fruit_image_view);
TextView textView=findViewById(R.id.fruit_content_text);
Toolbar toolbar=findViewById(R.id.toolbar);
//将ActionBar换成toolbar
setSupportActionBar(toolbar);
ActionBar actionBar=getSupportActionBar();
if(actionBar!=null){
actionBar.setDisplayHomeAsUpEnabled(true);
}
CollapsingToolbarLayout collapsingToolbarLayout=findViewById(R.id.collapsing_toolbar);
Intent intent=getIntent();
String name=intent.getStringExtra(fruitName);
int id=intent.getIntExtra(fruitId,0);
//用Glide来加载图片
Glide.with(this).load(id).into(imageView);
textView.setText(ExtraText(name));
//为可折叠标题栏设置标题。
collapsingToolbarLayout.setTitle(name);

充分利用状态栏空间。

为ImageView的父布局和其本身添加fitsSystemWindows属性。

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

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

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

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

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

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

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

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

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

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

</androidx.coordinatorlayout.widget.CoordinatorLayout>

为FruitActivity自定义一个主题。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FruitActivityTheme" parent="Theme.ListView">
<item name="android:statusBarColor">
@android:color/transparent
</item>
</style>
</resources>

引入这个主题

 <activity android:name=".FruitActivity" android:theme="@style/FruitActivityTheme">
收起阅读 »

基础巩固——多线程

多线程编程Android沿用了Java的线程模型,一个Android应用在创建时会开启一个线程,常称作主线程,也叫做UI线程,如果有请求网络等耗时操作时,就需要开启子线程去处理。因此,此文对多线程进行梳理总结线程基础1. 进程与线程这两个的区分在我的另一篇文章...
继续阅读 »

多线程编程

Android沿用了Java的线程模型,一个Android应用在创建时会开启一个线程,常称作主线程,也叫做UI线程,如果有请求网络等耗时操作时,就需要开启子线程去处理。因此,此文对多线程进行梳理总结


线程基础

1. 进程与线程

这两个的区分在我的另一篇文章# Android面向面试复习-操作系统+计网篇中已经提及,简单复习一下。进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。进程可以看作程序的实体,也是线程的容器。线程是操作系统调度的最小单元,一个进程中可以创建多个线程。这些线程拥有各自的计数器,堆栈,局部变量等属性,并且能够访问共享的内存变量


2. 线程的状态

Java线程在运行的生命周期可能会处于六种不同的状态,如下

  • New:新创建状态,线程被创建,还没有调用start方法,运行之前还有一些基础工作
  • Runnable:可运行状态,一旦调用start方法,就会处于Runnable状态。处于这个状态的线程可能正在运行,也可能没有,取决于操作系统的调度
  • Blocked:阻塞状态,表示线程被锁阻塞,暂不能活动
  • Waiting:等待状态,线程暂时不活动,并且不运行任何代码,这消耗最少的资源,知道调度器重新激活这个线程
  • Timed Waiting:超时等待,可以在指定的时间自行返回
  • Terminated:终止状态,表示当前线程已经执行完毕,比如run方法正常执行退出,或者因为没有被捕获的异常而终止

3. 创建线程

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法
  • 实现Callable接口,重写call方法

4. 理解中断

当线程的run方法执行完毕,或者方法里出现没有捕获的异常时,线程就要终止。早期Java版本中有stop方法可以终止线程,现在已经被弃用。现版本用interrupt来中断线程,当一个线程调用interrupt方法时,它的中断标志位将被置为true。线程会时不时的检测这个中断标记位,以判断线程是否应该被中断,要想知道线程是否被置位,可以调用isInterrupted方法查看返回值。还可以调用静态方法interrupted来对中断标志位进行复位。但是如果一个线程被阻塞,就无法检测中断状态。如果一个线程处于阻塞状态,那么线程在检查中断标志位时若发现中断标志位为true,就会在阻塞方法调用处抛出阻塞异常,并且在抛出异常前将线程中断标志位复位,即重新设置为false。需要注意的是被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定如何响应中断。如果是比较重要的线程,则不会理会中断。而大部分情况是线程会将中断作为一个终止的请求。另外,不要在底层代码里捕获InterruptedException不做处理,这里介绍两种合适的处理方式

  1. 在catch子句中,调用Thread.currentThread().interrupt()来设置中断状态。因为在抛出异常后中断标志位会复位,让外界通过判断isInterrupted()来决定是终止还是继续下去
void test(){
try{
sleep(50);
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
  1. 更好的做法是直接抛出异常,方便调用者捕获
void test() throw InterruptedException{
sleep(50);
}

5. 安全的终止线程

上一点我们提到了中断,首先用中断来终止线程,如下

public class test{
public static void main(String[] args) throws InterruptedException {
MyRunner runner = new MyRunner();
Thread thread = new Thread(runner, "MyRunner");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
thread.interrupt();
}

public static class MyRunner implements Runnable{
private long i;

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
i++;
System.out.println("i=" + i);
}
System.out.println("stop");
}
}
}

代码里用sleep方法使得main线程沉睡10ms,留给MyRunner足够的时间来感知中断从而结束,还可以采用boolean变量来控制是否需要停止线程,如下

public class test{
public static void main(String[] args) throws InterruptedException {
MyRunner runner = new MyRunner();
Thread thread = new Thread(runner, "MyRunner");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
runner.cancel();
}

public static class MyRunner implements Runnable{
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on){
i++;
System.out.println("i=" + i);
}
System.out.println("stop");
}

public void cancel(){
on = false;
}
}
}

结果如下,两段代码是类似的

image.png

此处说明线程执行到了run方法的末尾,即将终止


线程同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取,如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况被称为竞态条件。此时如果不用同步,是无法保证数据原子性的,所以我们就需要用到锁


1. 重入锁与条件对象

synchronized关键字自动提供了锁以及相关条件。大多数需要显示锁的情况使用synchronized非常方便。但是等我们了解了重入锁和条件对象时,能更好的理解synchronized关键字。重入锁ReentrantLock是Java SE 5.0引入的,就是支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。具体结构如下

Lock mLock = new ReentrantLock();
mLock.lock();
try {

}catch (){

}finally {
mLock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是同一时刻只有一个任务访问的代码区域。一旦一个线程封锁了锁对象,其他线程都无法进入。把解锁操作放到finally区域内是十分必要的,如果因为某些异常,锁资源是必须要释放的,否则其他资源将被永久阻塞。进入临界区时,却发现在某一个条件满足之后它才能执行,这时可以用一个条件对象来管理那些已经获得了一把锁但是却不能做有用工作的线程,条件对象又被称作条件变量。通过下面例子来说明为何需条件对象。假设一个场景需要用支付宝转账,我们先写支付宝类,它的构造方法需传入支付宝账户的数量和每个账户的账户金额。

public class Alipay{
private double[] accounts;
private Lock alipayLock;
public Alipay(int n, double money){
accounts = new double[n];
alipayLock = new ReentrantLock();
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}
}

接下来实现转账,需要一个from转账方,和to接收方,amount是转账金额,如下

public void transfer(int from, int to, int amount){
alipayLock.lock();
try {
while (accounts[from] < amount){
//wait
}
}catch (){

}finally {
alipayLock.unlock();
}
}

有可能会出现转账方余额不足的情况,如果有其他线程给这个转账方再转足够的钱,就可以转账成功了,但是这个线程已经获取了锁,具有排他性,别的线程无法获取锁来进行存款操作,这时我们就需要引入对象锁。一个锁对象拥有多个相关条件对象,可以用new Condition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁,相关代码如下

public class Alipay{
private double[] accounts;
private Lock alipayLock;
private Condition condition;
public Alipay(int n, double money){
accounts = new double[n];
alipayLock = new ReentrantLock();
condition = alipayLock.newCondition();
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}

public void transfer(int from, int to, int amount) throws InterruptedException{
alipayLock.lock();
try {
while (accounts[from] < amount){
condition.await();
}
}catch (){

}finally {
alipayLock.unlock();
}
}
}

一旦一个线程调用await方法,就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的signalAll()方法时为止。当另一个线程转账给我们此前的转账方时,只重复调用singnalAll()方法,就会重新激活因为这一条件而等待的所有线程,代码如下

public void transfer(int from, int to, int amount) throws InterruptedException{
alipayLock.lock();
try {
while (accounts[from] < amount){
condition.await();
}
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
condition.signalAll();
}catch (){

}finally {
alipayLock.unlock();
}
}

当调用了signalAll时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞态实现对对象的访问,还有个方法是signal,它则是随机解除某个线程的阻塞。如果该线程仍然不能运行,则再次被阻塞,如果没有其他线程再次调用signal,那么系统就死锁了


2. 同步方法

Lock接口和Condition接口为程序设计提供了高度的锁定控制,然而大多数情况下并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。Java中每一个对象都有一个内部锁,如果一个方法用synchronized关键字修饰,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程必须获得内部的对象锁,如下

public synchronized void method(){
···
}

这段代码等价于

Lock mLock = new ReentrantLock();
public void method(){
mLock.lock();
try{
···
}finally{
mLock.unlock();
}
}

对于上面转账的例子,可以将Alipay的transfer方法声明为synchronized,而不是使用一个显示的锁。内部对象锁只有一个相关条件,wait方法将一个线程添加到等待集中,使用notifyAll或notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.await(),notifyAll等价于signalAll,所以前面例子里的transfer方法也可以这么写

public synchronized void transfer(int from, int to, int amount) throws InterruptedException{
while (accounts[from] < amount){
wait();
}
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
notifyAll();
}

在此可以看到,使用sychronized关键字来编码要简练很多,由该锁来管理那些试图进入synchronized方法的线程,由该锁中的条件来管理那些调用wait的线程


3. 同步代码块

除了调用同步方法来获得锁,还可以通过使用同步代码块,如下

synchronized(obj){
···
}

其获得了obj的锁,obj是一个对象,我们用同步代码块进行改写上面的例子

public class Alipay{
private double[] accounts;
private Object lock = new Object();
private Condition condition;
public Alipay(int n, double money){
accounts = new double[n];
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}

public synchronized void transfer(int from, int to, int amount) throws InterruptedException{
synchronized (lock){
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
}
}
}

在这里创建了一个名为lock的Object类,为的是使用Object类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用,一般实现同步最好用Java的并发包下的集合类,比如阻塞队列。如果同步方法适合自己的程序,尽量使用同步方法,这样可以减少编写代码的数量,减少出错的概率,如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition


4. volatile

有时,仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大,而volatile关键字为实例域的同步访问提供了免锁机制。如果声明一个域为volatile的话,那么编译器和虚拟机知道该域是可能被另一个线程并发更新的。当一个共享变量被volatile关键字修饰后,就具备了两个含义,一个含义是线程修改了变量的值时,变量的新值对于其他线程是立即可见的。另一个含义是禁止使用指令重排序,分为编译期重排序和运行时重排序。先来看一段代码,假设线程1先执行,2后执行,如下

//线程1
boolean stop = false;
while(!stop){
//doSomething
}

//线程2
stop = true;

这是一个线程中断的代码,但是这段代码不一定会将线程中断,虽说无法中断线程这个情况出现的概率很小,但是一旦发生便是死循环。因为每个线程都有私有的工作内存,因此线程1运行时会拷贝一份stop的值放入私有工作内存中,当线程2更改了stop的变量值并返回后,线程2突然需要做其他操作,这时就无法将更改的stop变量写入主存中,这样线程1就不知道线程2对stop变量进行了更改,因此线程1会一直执行下去。当stop用volatile修饰,线程2修改stop值时,会强制将修改的值立刻写入主存,这样使得线程1的工作内存中的stop变量缓存无效,这样线程1在此读取变量stop的值时就会去主存读取

volatile不保证原子性

另外volatile不保证原子性,可看如下代码演示

class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}

public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(() -> {
for(int j=0;j<1000;j++)
test.increase();
}).start();
}
//保证前面的线程都执行完
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(test.inc);
}
}

这段代码每次运行的结果都不一致,因为自增操作是不具备原子性的。自增操作里包含了读取原始值、加1、写入工作内存这三个子操作,也就是说这三个子操作可能被割裂执行。

volatile保证有序性

volatile关键字能禁止指令重排序,因此能保证有序性。禁止指令重排序有两层含义,其一是指代码运行到volatile变量操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还未执行。其二是进行指令优化时,在volatile变量之前的语句不能在volatile变量之后执行

正确使用volatile关键字

synchronized关键字可防止多个线程同时执行一段代码,但是这会很影响程序的执行效率。volatile关键字在有些时候会优于synchronized关键字。但是要注意volatile关键字时无法替代synchronized关键字的,因为其无法保证原子性,通常来说,使用volatile关键字需要具备以下两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

关于第一点,就是上面提到的自增自减操作。关于第二点,举个例子,包含一个不变式:下界总是小于或等于上界,代码如下

public class NumberRange {
private volatile int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}

这种方式定义的upper和lower并不能充分实现类的线程安全,如果两个线程在同一时间使用不一致的值执行setLower和setUpper的话,就会使范围处于不一致的状态。例如,如果初始状态是(0,5),同一时间内,两个线程分别调用setLower(4)和setUpper(3),虽然这两个交叉存入的值是不符合条件的,但是这两个线程都会通过用于保护不变式的检查,使得最后范围是(4,3),显然是不对的


收起阅读 »

使用DialogFragment代替Dialog

使用DialogFragment代替Dialog是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。DialogFra...
继续阅读 »

使用DialogFragment代替Dialog

是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。

DialogFragment是什么

DialogFragment从它的源码得知,它继承了Fragment,其实是一个比较特殊的Fragment。那么它相对于普通的Dialog有什么不同,谷歌又为什么推荐我们使用它呢,它相对于普通的Dialog有什么优点呢。

使用过它之后用自己的感受描述:

  • 它的生命周期很清晰,方便写复杂的逻辑
  • 它于Activity的生命周期是绑定的,Activity消失,DialogFragment也会消失。
  • 它可以很简单的控制弹窗的布局。

总结就是dialogfragment能更好的管理dialog的展示与消失,以及在屏幕旋转时的一些状态保存问题。

DialogFragment的踩坑

即使它有很多的优点,但使用不当时,仍然会有很多坑。
我遇到了很多奇奇怪怪的问题。 比如

  • Fragment already added 异常
  • 快速的显示消失,无法消失的异常

当然除此之外我们还可能有以下需求:

  • 设置对话框的大小
  • 设置弹出对话框时背景灰色或者透明

下面我们就来一一实现。

如何实现DialogFragment

重点来关注两个方法。

  • onCreateDialog 新建一个Dialog即可使用
  • onCreateView 自定义一个Dialog界面

onCreateDialog

Screenshot_2021-11-04-22-04-55-27_2a27335eaa331505125090a61677c0b2.jpg

做一个简单的对话框

public class ConfirmDialog extends DialogFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("提示")
.setMessage("确认要退出吗")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
}).create();
return dialog;
}
}

显示它

ConfirmDialog dialog = new ConfirmDialog();
dialog.show(getSupportFragmentManager(), "dialogTag");

onCreateView

使用自定义视图做一个加载框。
这里有一个非常重要的地方,我出现了 Fragment already added 的问题,意思就是重复添加了,那么为什么会出现重复添加呢,因为我最初的代码是使用isAddedisVisibility进行判断,但是当快速执行的时候,这两个方法并不准确。

正确的做法有两种。

  • 添加事务时先进行移除
beginTransaction().remove(this).commit()
  • 使用变量进行判断,不能使用isAdded
private boolean isShowFragment = false;

@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
// 解决bug:Android java.lang.IllegalStateException: Fragment already added
if (this.isShowFragment) {
return;
}
this.isShowFragment = true;
super.show(manager, tag);
}

@Override
public void dismiss() {
super.dismiss();
this.isShowFragment = false;
}

// 避免有些手机兼容性问题,isShowFragment未变成false而导致无法二次打开
@Override
public void onDestroy() {
super.onDestroy();
this.isShowFragment = false;
}

最后直接放代码,封装好的Loading框

LoadingDialog 对话框 可以看到代码中对bug的处理:在每个add事务前增加一个remove事务,防止连续的add。

public class LoadingDialog extends DialogFragment
implements DialogInterface.OnKeyListener {
/**
* 加载框提示信息 设置默认
*/
private final String hintMsg = "加载中...";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.MyDialog);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Dialog dialog = getDialog();
// 设置背景透明
if (dialog.getWindow() != null)
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
// 去掉标题
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(false);
View loadingView = inflater.inflate(R.layout.dialog_loading, container);
TextView hintTextView = loadingView.findViewById(R.id.tv_ios_loading_dialog_hint);
hintTextView.setText(hintMsg);
//不响应返回键
dialog.setOnKeyListener(this);
return loadingView;
}

@Override
public void show(FragmentManager manager, String tag) {
try {
//在每个add事务前增加一个remove事务,防止连续的add
manager.beginTransaction().remove(this).commit();
super.show(manager, tag);
} catch (Exception e) {
//同一实例使用不同的tag会异常,这里捕获一下
e.printStackTrace();
}
}

@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
// return keyCode == KeyEvent.KEYCODE_BACK;
// 允许按back键取消Loading
return false;
}

}

代理管理类

public class GlobalDialogManager {

private LoadingDialog mLoadingDialog;

private GlobalDialogManager() {
init();
}

public static GlobalDialogManager getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final GlobalDialogManager INSTANCE = new GlobalDialogManager();
}

public void init() {
if (mLoadingDialog == null) {
mLoadingDialog = new LoadingDialog();
}
}

/**
* 展示加载框
*/
public synchronized void show(FragmentManager manager) {
if (manager != null && mLoadingDialog != null) {
mLoadingDialog.show(manager, "tag");
}
}

/**
* 隐藏加载框
*/
public synchronized void dismiss(FragmentManager manager) {
if (mLoadingDialog != null && !manager.isDestroyed()) {
mLoadingDialog.dismissAllowingStateLoss();
}
}
}

使用它

if (getContext() != null)
GlobalDialogManager.getInstance().show(((Activity) getContext()).getFragmentManager());

if (getContext() != null)
GlobalDialogManager.getInstance().dismiss(((Activity) getContext()).getFragmentManager());

这里判断getContext()很有必要,避免Activity消失了,getContext为空的bug。

背景不变暗设置一个style属性行啦。

<item name="android:backgroundDimEnabled">false</item><!--activity不变暗-->

收起阅读 »

栈的实现

一、栈 💦 栈的概念及结构 栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。 栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对...
继续阅读 »

一、栈


💦 栈的概念及结构


栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对应多种出栈顺序

栈有两个经典的操作


1️⃣ 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。


2️⃣ 出栈:栈的删除操作叫做出栈。出数据也在栈顶 。


在这里插入图片描述


💦 栈的实现


这里对于栈的实现我们既可以选择数组也可以和选择链表两者的效率都差不多,但是还是建议使用数组
在这里插入图片描述


1.初始化

函数原型


在这里插入图片描述


函数实现


void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

2.插入

函数原型


在这里插入图片描述


函数实现


void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}

3.判空

函数原型


在这里插入图片描述


函数实现


bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}

4.删除

函数原型


在这里插入图片描述


函数实现


void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}

5.长度

函数原型


在这里插入图片描述


函数实现


int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}

6.栈顶

函数原型


在这里插入图片描述


函数实现


STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}

7.销毁

函数原型


在这里插入图片描述


函数实现


void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

💦 完整代码


这里需要三个文件


1️⃣ Static.h,用于函数的声明


2️⃣ Static.c,用于函数的定义


3️⃣ Test.c,用于测试函数




🧿 Stack.h

#pragma once

//头
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

//结构体
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a; //指向动态开辟的空间
int top; //栈顶
int capacicy; //容量
}ST;

//函数
//注意链表和顺序表我们写Print,但是栈不写,因为如果栈可以Print的话,就不符合后进先出了
//初始化
void StackInit(ST* ps);
//插入
void StackPush(ST* ps, STDatatype x);
//判空
bool StackEmpty(ST* ps);
//删除
void StackPop(ST* ps);
//长度
int StackSize(ST* ps);
//栈顶
STDatatype StackTop(ST* ps);
//销毁
void StackDestory(ST* ps);

🧿 Stack.c

#include"Stack.h"

void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}
void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}
bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}
void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}
int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}
STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}
void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

🧿 Test.c

#include"Stack.h"

int main()
{
ST st;
//初始化
StackInit(&st);
//插入+删除
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPop(&st);
StackPop(&st);
//长度
StackSize(&st);
//栈顶
StackTop(&st);
//销毁
StackDestory(&st);
return 0;
}

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

一篇文章了解Java之网络编程

一、网络基础知识 网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。 计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。 IP地址:具有全球唯一性,...
继续阅读 »

一、网络基础知识


网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。


计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。


IP地址:具有全球唯一性,相对于internet,IP为逻辑地址。


IP地址分类:


1. A类地址


A类地址第1字节为网络地址,其它3个字节为主机地址。另外第1个字节的最高位固定为0。


A类地址范围:1.0.0.1到126.155.255.254。


A类地址中的私有地址和保留地址:


10.0.0.0到10.255.255.255是私有地址(所谓的私有地址就是在互联网上不使用,而被用在局域网络中的地址)。


127.0.0.0到127.255.255.255是保留地址,用做循环测试用的。


2. B类地址


B类地址第1字节和第2字节为网络地址,其它2个字节为主机地址。另外第1个字节的前两位固定为10。


B类地址范围:128.0.0.1到191.255.255.254。


B类地址的私有地址和保留地址


172.16.0.0到172.31.255.255是私有地址


169.254.0.0到169.254.255.255是保留地址。如果你的IP地址是自动获取IP地址,而你在网络上又没有找到可用的DHCP服务器,这时你将会从169.254.0.0到169.254.255.255中临得获得一个IP地址。


3. C类地址


C类地址第1字节、第2字节和第3个字节为网络地址,第4个个字节为主机地址。另外第1个字节的前三位固定为110。


C类地址范围:192.0.0.1到223.255.255.254。


C类地址中的私有地址:


192.168.0.0到192.168.255.255是私有地址。


4. D类地址


D类地址不分网络地址和主机地址,它的第1个字节的前四位固定为1110。


D类地址范围:224.0.0.1到239.255.255.254


Mac地址:每个网卡专用地址,也是唯一的。


端口(port):OS中可以有65536(2^16)个端口,进程通过端口交换数据。连线的时候需要输入IP也需要输入端口信息。


计算机通信实际上的主机之间的进程通信,进程的通信就需要在端口进行联系。


192.168.0.23:21


协议:为了进行网络中的数据交换(通信)而建立的规则、标准或约定。


不同层的协议是完全不同的。


网络层:寻址、路由(指如何到达地址的过程)


传输层:端口连接


TCP模型:应用层/传输层/网络层/网络接口


端口是一种抽象的软件结构,与协议相关:TCP23端口和UDT23端口为两个不同的概念。


端口应该用1024以上的端口,以下的端口都已经设定功能。


TCP/IP模型


Application


(FTP,HTTP,TELNET,POP3,SMPT)


Transport


(TCP,UDP)


Network


(IP,ICMP,ARP,RARP)


Link


(Device driver,….)


注:


IP:寻址和路由


ARP(Address Resolution Protocol)地址解析协议:将IP地址转换成Mac地址


RARP(Reflect Address Resolution Protocol)反相地址解析协议:与上相反


ICMP(Internet Control Message Protocol)检测链路连接状况。利用此协议的工具:ping , traceroute


二、TCP Socket


TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。


   1) 服务器分配一个端口号,服务器使用accept()方法等待客户端的信号,信号一到打开socket连接,从socket中取得OutputStream和InputStream。


   2) 客户端提供主机地址和端口号使用socket端口建立连接,得到OutputStream和InputStream。


TCP/IP的传输层协议


1、 建立TCP服务器端


创建一个TCP服务器端程序的步骤:


    1). 创建一个ServerSocket


    2). 从ServerSocket接受客户连接请求


    3). 创建一个服务线程处理新的连接


    4). 在服务线程中,从socket中获得I/O流


    5). 对I/O流进行读写操作,完成与客户的交互


    6). 关闭I/O流


    7). 关闭Socket


ServerSocket server = new ServerSocket(post)


Socket connection = server.accept();


ObjectInputStream put=new ObjectInputStream(connection.getInputStream());


ObjectOutputStreamo put=newObjectOutputStream(connection.getOutputStream());  


处理输入和输出流;


关闭流和socket。


2、 建立TCP客户端


创建一个TCP客户端程序的步骤:


1).创建Socket


    2). 获得I/O流


    3). 对I/O流进行读写操作


    4). 关闭I/O流


    5). 关闭Socket


Socket connection = new Socket(127.0.0.1, 7777);

ObjectInputStream input=new ObjectInputStream(connection.getInputStream());

ObjectOutputStream utput=new ObjectOutputStream(connection.getOutputStream());


处理输入和输出流;


关闭流和socket。


三、 建立UDP连接


UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。


         比较:TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输;既然有了保证可靠传输的TCP协议,为什么还要非可靠传输的UDP协议呢?主要的原因有两个。一是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。二是在许多应用中并不需要保证严格的传输可靠性,比如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。


如:http://www.tarena.com.cn:80/teacher/zhu…


协议名://机器名+端口号+文件名


2 . URL类的常见方法


一个URL对象生成后,其属性是不能被改变的,但是我们可以通过类URL所提供的方法来获取这些属性:


   public String getProtocol() 获取该URL的协议名。


   public String getHost() 获取该URL的主机名。


   public int getPort() 获取该URL的端口号,如果没有设置端口,返回-1。


   public String getFile() 获取该URL的文件名。


   public String getRef() 获取该URL在文件中的相对位置。


   public String getQuery() 获取该URL的查询信息。


   public String getPath() 获取该URL的路径


   public String getAuthority() 获取该URL的权限信息


   public String getUserInfo() 获得使用者的信息


   public String getRef() 获得该URL的锚



  1. 例子,将tarena网站首页拷贝到本机上。


import java.net.*;

import java.io.*;

import java.util.*;



public class TestURL{



         public static void main(String[] arg){



                   System.out.println("http://www.tarena.com.cn:80/index.htm===>");

                   //System.out.println(getWebContent());

                   writeWebFile(getWebContent());

             }

   

             public static String getWebContent(){

   

                       URL url = null;

                       HttpURLConnection uc = null;

                       BufferedReader br = null;

                       final int buffLen = 2048;

                       byte[] buff = new byte[buffLen];

                       String message = "";

                       String tmp = "";

                       int len = -1;



                       String urlStr = "http://www.tarena.com.cn:80/index.htm";

                      

                       try{

                             url = new URL(urlStr);

                             //连接到web资源

                             System.out.println("before openConnection ====>"+new Date());

                             uc = (HttpURLConnection)url.openConnection();

                             System.out.println("end openConnection ====>"+new Date());

                             br = new BufferedReader( new InputStreamReader(uc.getInputStream()));

                             System.out.println("end getINputStream() ====>"+new Date());



                             while( ( tmp = br.readLine())!=null){

                                    message += tmp;

                             }

                             System.out.println("end set message ====>"+new Date());



                       }catch(Exception e){e.printStackTrace();System.exit(1);}

                       finally{



                             if(br!=null){

                                    try{

                                           br.close();

                                    }catch(Exception ioe){ioe.printStackTrace();}

                             }

                    }



                    return  message;

    }

   

    public static void writeWebFile(String content){

   

             FileWriter fw = null;

             try{

                       fw = new FileWriter("index.htm");

                       fw.write(content,0,content.length());

             }catch(Exception e){

                       e.printStackTrace();

             }finally{

                       if(fw!=null){

                                try{

                                         fw.close();

                                }catch(Exception e){}

                       }

             }

            

    }

}


四、UDP socket


这种信息传输方式相当于传真,信息打包,在接受端准备纸。


特点:




  1. 基于UDP无连接协议




  2. 不保证消息的可靠传输




  3. 它们由Java技术中的DatagramSocket和DatagramPacket类支持




DatagramSocket(邮递员):对应数据报的Socket概念,不需要创建两个socket,不可使用输入输出流。


DatagramPacket(信件):数据包,是UDP下进行传输数据的单位,数据存放在字节数组中,其中包括了目标地址和端口以及传送的信息(所以不用建立点对点的连接)。


DatagramPacket的分类:


用于接收:DatagramPacket(byte[] buf,int length)


                      DatagramPacket(byte[] buf,int offset,int length)


用于发送:DatagramPacket(byte[] buf,int length, InetAddress address,int port )


                      DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port)


注:InetAddress类网址用于封装IP地址


没有构造方法,通过


InetAddress.getByAddress(byte[] addr):InetAddress


InetAddress.getByName(String host):InetAddress


等。


1、建立UDP 发送端


创建一个UDP的发送方的程序的步骤:


    1). 创建一个DatagramPacket,其中包含发送的数据和接收方的IP地址和端口


号。


    2). 创建一个DatagramSocket,其中包含了发送方的IP地址和端口号。


    3). 发送数据


    4). 关闭DatagramSocket


byte[] buf = new byte[1024];

DatagramSocket datagramSocket = new DatagramSocket(13);// set port

DatagramPacket intputPacket = new DatagramPacket (buf,buf.length);

datagramSocket.receive(inputPacket);

DatagramPacket  outputPacket = new DatagramPacket (buf,buf.length,

inetAddress,port);

datagramSocket.send(outputPacket);


没建立流所以不用断开。


2、 建立UDP 接受端


创建一个UDP的接收方的程序的步骤:


    1). 创建一个DatagramPacket,用于存储发送方发送的数据及发送方的IP地址和端口号。


    2). 创建一个DatagramSocket,其中指定了接收方的IP地址和端口号。


    3). 接收数据


    4). 关闭DatagramSocket


byte[] buf = new byte[1024];

DatagramSocket datagramSocket = new DatagramSocket();//不用设端口,因为发送的包中端口

DatagramPacket outputPacket=new DatagramPacket(

Buf, buf.length, serverAddress, serverPort);

DatagramPacket inputPacket=new DatagramPacket(buf, buf.length);

datagramSocket.receive(inputPacket);


URL类:可直接送出或读入网络上的数据。


Socket类:可以想象为两个不同主机的程序通过网络的通信信道。


Datagram类:它把数据的目的记录放在数据包中,然后直接放到网络上。


InetAddress类:用来存储internet地址的类(ip地址,域名)。



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

熬夜再战Android-Button实现selector选择器

前提 这是小空熬夜写的Android新手向系列,欢迎品尝。 selector是按钮最常用的功能,对美化控件的作用很大。 上节我们说了selector和shape联合使用,但偏向shape的介绍,今天主要说selector。 👉实践过程 我们先按照上一节的sha...
继续阅读 »

前提


这是小空熬夜写的Android新手向系列,欢迎品尝。


selector是按钮最常用的功能,对美化控件的作用很大。


上节我们说了selector和shape联合使用,但偏向shape的介绍,今天主要说selector。


👉实践过程


我们先按照上一节的shape方式创建两个shape背景

btn_selector_shape1.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- 圆角 -->
    <corners android:radius="5dp" />
    <!--填充颜色-->
    <solid android:color="#00ff00" />
</shape>

btn_selector_shape2.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!--圆角-->
    <corners android:radius="5dp" />
    <!--填充颜色-->
    <solid android:color="#0000ff" />
</shape>

接着我们在【res-drawable】右键创建个Drawable Resource File ,弹出框写文件名创建文件,设置默认【Root element】为selector。


btn_selector0.xml


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/btn_selector_shape1" android:state_pressed="true" />
    <item android:drawable="@drawable/btn_selector_shape2" android:state_window_focused="false" />
</selector>

布局中引用


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".TextActivity">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="160dp"
        android:background="@drawable/btn_selector0"
        android:text="按下变色"
        android:textColor="@color/white" />
</RelativeLayout>

我们运行下看看


image.png


image.png


但是


我们回忆下,刚才是不是创建了三个文件,按钮少的情况下还好,自定义的按钮一多,这么多文件非常不容易管理,所以我们要用另外一种写法,将所有内容放到一个文件中。


我们在刚才的btn.selector0.xml中修改:


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--这是第一种方式,利用drwable引用文件-->
    <!--<item android:drawable="@drawable/btn_selector_shape1" android:state_pressed="true" />-->
    <!--<item android:drawable="@drawable/btn_selector_shape2" android:state_pressed="false" />-->
    <!--第二种方式如下-->
    <item android:state_pressed="false">
        <shape android:shape="rectangle">
            <!-- 圆角 -->
            <corners android:radius="5dp" />
            <!--填充颜色为白色-->
            <solid android:color="#0000ff" />
        </shape>
    </item>
    <!--单击时是一个带圆角,白色背景,绿色边框的矩形-->
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <!--圆角-->
            <corners android:radius="5dp" />
            <!--填充颜色为白色-->
            <solid android:color="#00ff00" />
        </shape>
    </item>
</selector>

我们运行起来看看,哎,效果很正确啊


Selector的属性不止这两个哦:



  • state_focused 布尔值,是否获得焦点

  • state_window_focused 布尔值,是否获得窗口焦点

  • state_enabled 布尔值,控件是否可用

  • state_checkable 布尔值,控件可否被勾选

  • state_checked 布尔值,控件是否被勾选

  • state_selected 布尔值,控件是否被选择,针对有滚轮的情况

  • state_pressed 布尔值,控件是否被按下

  • state_active 布尔值,控件是否处于活动状态

  • state_singlestate_firststate_middle很少使用,知道就行。

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

View 事件分发机制详解

View事件传递过程先从Activity-->Window-->View。public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; ...
继续阅读 »

View事件传递过程先从Activity-->Window-->View。

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
//1
if (onInterceptTouchEvent(ev)) {
//2
consume = onTouchEvent(ev);
} else {
//3
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

这个都是事件分发的伪代码,

  • 注释1调用自己ViewGroup的onInterceptTouchEvent方法是否需要拦截此事件
  • 拦截事件调用注释2此View自己消耗调用方法onTouchEvent方法
  • 注释1不拦截这调用注释3子View的dispatchTouchEvent方法

接着我们继续分Activity的dispatchTouchEvent方法,代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//1
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//2
return onTouchEvent(ev);
}
  • 注释1 可以看出,Activity把事件传递给Window的superDispatchTouchEvent方法让Window处理事件,可看出如果Window不消费事件
  • 注释2 Activity的onTouchEvent方法。我们继续看Window是如何实现的,Window是个抽象类,具体实现者是PhoneWindow

源码如下。

//1
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
//2
return mDecor.superDispatchTouchEvent(event);
}
  • 注释1 mDecor是DecorView类的引用,DecorView继承FrameLayout
  • 注释2 调用ViewGroup的superDispatchTouchEvent方法

接着我们继续分析superDispatchTouchEvent源码,其最终调用了ViewGroup的dispatchTouchEvent方法,代码如下:

 public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//2
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//3
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
return handled;
}
  • 注释1处当事件ACTION_DOWN时进行初始化(mFirstTouchTarget、mGroupFlags重置),事件是由ACTION_DOWN开始以ACTION_UP结束,这样做的目的在于每次事件不会因为之前设置受影响
  • 注释2处 mFirstTouchTarget(后面细讲)表示子元素是否处理事件,如果处理则不为空。如果ViewGroup拦截事件,在ACTION_MOVE和ACTION_UP状态时都不会调用onInterceptTouchEvent方法且intercept为true,ViewGroup消费这些事件
  • 注释3处 子View通过ViewGroup的requestDisallowInterceptTouchEvent方法告知父容器不要拦截事件,让子View处理。 接着继续分析dispatchTouchEvent方法代码如下:
    @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//1
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//2
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//3
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//4
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
return handled;
}
  • 注释1处 倒叙遍历ViewGroup子元素,最上面一层最先遍历
  • 注释2处 判断子View是否接收事件,触摸点是否在子View上
  • 注释3处 调用了dispatchTransformedTouchEvent方法,如果返回值为true,表示有子View消费事件
  • 注释4处 调用addTouchTarget方法具体源码如下
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
//1
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//2
target.next = mFirstTouchTarget;
//3
mFirstTouchTarget = target;
return target;
}

把需要消费事件的View包装成TouchTarget类的单链表结构。

  • 注释1处 从缓存池拿TouchTarget对象
  • 注释2处 TouchTarget类的target对象作为头指针,target的next指向mFirstTouchTarget引用
  • 注释3处 将target对象赋值给mFirstTouchTarget即成为单链表的表头

mFirstTouchTarget: 存储本轮(ACTION_DOWN到ACTION_UP)需要消耗事件的子View的单链表,也是单链表表头。
我们继续分析ViewGroup类dispatchTouchEvent方法最后一部分代码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
//2
while (target != null) {
final TouchTarget next = target.next;
//3
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//4
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
return handled;
}

根据mFirstTouchTarget链表头指针,判断事件交给谁来处理。

  • 注释1处 没有子View来处理事件,则交由自己处理
  • 注释2处 while循环遍历单链表
  • 注释3处 判断是否已经消费此事件
  • 注释4处 事件分发给子View的dispatchTouchEvent方法

接着我们继续分析这个神秘的方法dispatchTransformedTouchEvent方法源码如下:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (child == null) {
//1
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//2
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
  • 注释1处 如果ViewGroup没有子元素则调用父类View的dispatchTouchEvent方法,把事件交给自己处理
  • 注释2处 调用子元素的dispatchTouchEvent方法,实现事件的分发

继续看View的dispatchTouchEvent方法源码实现:

public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
//1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
  • 注释1处 判断mOnTouchListener不为空则调用onTouch方法,则不会调用onTouchEvent方法,说明onTouch方法调用的优先级高于它
  • 注释2处调用onTouchEvent方法

继续分析onTouchEvent方法源码如下:

 public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
break;
}
return true;
}
return false;
}

只要View是CLICKABLE和LONG_CLICKABLE这个状态,都会返回true表示要消费。接着我们看看点击操作,代码如下:

public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//1
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}

我们从注释1处看出,如果设置了点击事件这个回调onClick方法,说明onTouchEvent方法优先级高于onClick方法。

以上都是所有分析View事件分发机制


作者:程序员喵大人
链接:https://juejin.cn/post/7026690811085455397
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

学习笔记-Retrofit源码解析

挖掘Retrofit:2.8.0源码。介绍Retrofit如何完成对OkHttp的封装,以及Retrofit如何支持的协程。1. BuilderRetrofit通过Retrofit.Builder创建,主要是配置各种工厂Factory。val retrofit...
继续阅读 »

挖掘Retrofit:2.8.0源码。介绍Retrofit如何完成对OkHttp的封装,以及Retrofit如何支持的协程。

1. Builder

Retrofit通过Retrofit.Builder创建,主要是配置各种工厂Factory

val retrofit = Retrofit.Builder()
.baseUrl("this is baseUrl")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()

Builder.builder()主要有五个步骤:

  1. 获取Platform对象,Platform是平台适配器,主要为了跨平台使用,类如安卓平台实现就是Android
  2. 初始化CallFactroyCallFactroy的作用是生产realCall,网络请求由他发出。默认值是OkHttpOKHttpClient
  3. 初始化Executor,默认通过Platform获取。
  4. 初始化CallAdapter.FactroyCallAdapter.Factroy生产CallAdapterCallAdapterrealCall的适配器,通过对realCall的包装,实现Executorjava8Futruerxjava等调度方法。存在多个,顺序是自定义配置-->默认配置。默认配置通过Platform获得。
  5. 初始化Converter.FactoryConverter.Factory生产ConverterConverter是数据转换器,将返回的数据转换为需要的数据,一般转换为我们要用的对象。存在多个,顺序是内置配置-->自定义配置-->默认配置。默认配置通过Platform获得。
class Builder{

Builder(Platform platform) {
this.platform = platform;
}

public Builder() {
//step 0
this(Platform.get());
}

public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}

// step1
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();
}

//step2
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
callbackExecutor = platform.defaultCallbackExecutor();
}

//step3
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

//step4
List<Converter.Factory> converterFactories = new ArrayList<>(
1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
converterFactories.add(new BuiltInConverters());
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(platform.defaultConverterFactories());

return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}
}

到这里,Retrofit对象创建完成,一个大致的结构如下图:

retrofit结构.png

2. 创建API对象

Retrofit通过定义网络请求的接口设置请求参数和返回类型,通过调用retrofit.create()创建这个接口的对象,调用这个对象的方法生成最终的网络请求。

interface MyService {
@GET("/user")
fun getUser(): Observable<Response<User>>
}

val myServiceClass: MyService = retrofit.create(MyService::class.java)

进入到create方法,可以看到是直接调用Proxy.newProxyInstance()方法创建出对象。这是标准库提供的动态代理机制,在运行时创建接口的实例对象。

Proxy.newProxyInstance()方法有三个参数:

  • classLoader: 类加载器

  • interfaces:需要实现的接口

  • InvocationHandler:代理方法

public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

为了更好的理解动态代理,可以当做动态代理自动帮我们生成一个接口的实现类。将所有的方法都通过handler.invoke()代理,如下面的代码所示。只不过动态代理是运行时生成的这个类,并且是直接生成了字节码。

interface MyService {
@GET("/user")
fun getUser(): Observable<Response<User>>

@GET("/name")
fun getName(userId: String): Observable<Response<String>>
}

//自动生成的代码示例
class SuspendServiceProxy implements SuspendService {
InvocationHandler handler;

@NonNull
@Override
public Observable<Response<User>> getUser() {
return handler.invoke(
this,
SuspendService.class.getMethod("getUser", String.class),
new Object[]{}
);
}

@NonNull
@Override
public Observable<Response<String>> getName(@NonNull String userId) {
return handler.invoke(
this,
SuspendService.class.getMethod("getName", String.class),
new Object[]{userId}
);
}
}

3. 创建请求对象

创建好API对象之后,就可以调用它的方法创建请求对象。

val observable = myServiceClass.getUser()

根据前面可以知道,这个方法代理给了InvocationHandler,在这个方法首先判断这个方法对象是不是实体对象,如果是的话就直接调用就行。

如果不是一个对象,就把调用再代理给ServiceMethod。首先调用loadServiceMethod()创建ServiceMethod对象,然后调用invoke()方法得到返回值。

public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
// 判断这个方法对象是不是实体对象,如果是的话就直接调用
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
// 判断这个方法对象是不是实体对象,如果是的话就直接调用
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}

3.1. 创建ServiceMethod对象

ServiceMethodRetrofit内部的自定义的代理方法,实际的逻辑是交给它处理。

abstract class ServiceMethod<T> {
abstract @Nullable T invoke(Object[] args);
}

loadServiceMethod()方法获取ServiceMethod对象。

方法对象Method作为Key缓存ServiceMethod对象在Retrofit中,因为创建ServiceMethod是一个耗时过程,所以弄成单例模式。

如果拿不到缓存,就调用ServiceMethod.parseAnnotations()创建一个ServiceMethod对象。

private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();


ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;

synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = ServiceMethod.parseAnnotations(this, method);
serviceMethodCache.put(method, result);
}
}
return result;
}

ServiceMethod.parseAnnotations()中只做了一件事,创建RequestFactory然后将创建ServiceMethod的工作又交给了子类HttpServiceMethod处理。

RequestFactory是接口的参数配置,通过解析接口的注解,返回值,入参及其注解等获得这些参数。

之后将解析完成的数据传递给HttpServiceMethed,由它继续创建ServiceMethod

abstract class ServiceMethod<T> {
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
...
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}

abstract @Nullable T invoke(Object[] args);
}

3.1.1. 创建RequstFactory

RequestFactory内部也是一个Builder模式。主要做了两件事:

  1. 遍历接口注解,初始化配置参数,这里读取的是POSTGET等注释。
  2. 遍历入参及其注解,将入参转换为ParameterHandler对象,将每个参数的设置配置的逻辑代理给了它处理。
final class RequestFactory {
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new RequestFactory.Builder(retrofit, method).build();
}


}
}

3.1.2.2. OkHttpCall

最终创建的网络请求对象是OkHttpCall

OkHttpCall实现了Retrofit.Call接口,这个接口与OkHttp.Call基本一致,这里只介绍它的三个方法:

  1. execute()同步发起请求并且返回请求体Response
  2. enqueue()异步发起请求,通过Callback通信,需要注意的是处理的回调也是在异步中调用的。
  3. cancel()取消请求。

查看OkHttpCall的实现,可以发现所有的Call接口方法的具体实现都是代理给了rawCall

cancel()直接代理给rawCall.cancel()

execute()代理给rawCall.execute(),将返回值交给parseResponse()转换了一次。

enqueue()代理给rawCall.enqueue(),多加了一层Callback回调,在成功回调中也是交给parseResponse()转换之后再回调给原始的Callback

也就是说OkHttpCall把所有的逻辑静态代理给了rawCall,这样做的好处是可以在对应的地方做一下额外的处理,也就是获得返回值通过parseResponse()转换数据。

fAndroid平台默认的CallBackExecutorPlatform的实现类Android中,将Runnable抛到MainHandler中,实现回调到主线程。
static final class Android extends Platform {

@Override public Executor defaultCallbackExecutor() {
return MainThreadExecutor();
}

static class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());

@Override public void execute(Runnable r) {
handler.post(r);
}
}
}
3.1.2.3.2. RxJava2CallAdapterFactory

RxJava2CallAdapterFactory只在返回类型是Observable之类的时候创建CallAdapter

public final class RxJava2CallAdapterFactory extends CallAdapter.Factory {

@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
Class<?> rawType = getRawType(returnType);

if (rawType != Observable.class) { //省略了其他类型
return null;
}

boolean isResult = false;
boolean isBody = false;
Type responseType;


Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
Class<?> rawObservableType = getRawType(observableType);
if (rawObservableType == Response.class) {
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
}
...
return new RxJava2CallAdapter(...);
}
}

RxJava2CallAdapter中的adapt()Call封装成observable返回。

final class RxJava2CallAdapter<R> implements CallAdapter<R, Object> {

@Override public Object adapt(Call<R> call) {
Observable<Response<R>> observable = new CallExecuteObservable<>(call);
...
return RxJavaPlugins.onAssembly(observable);
}
}

最后进到CallExecuteObservable,在启动的时候调用call.execute()并将结果抛给观察者。

final class CallExecuteObservable<T> extends Observable<Response<T>> {
private final Call<T> originalCall;

CallExecuteObservable(Call<T> originalCall) {
this.originalCall = originalCall;
}

@Override protected void subscribeActual(Observer<? super Response<T>> observer) {
Call<T> call = originalCall.clone();
try {
...
Response<T> response = call.execute();
observer.onNext(response);
}
}
}

3.2. 回顾

  1. Api接口实例动态代理HttpServiceMethod
  2. 网络请求的真实执行者是OkHttpClient创建的RealCall
  3. Api接口的调用到RealCall直接有两层静态代理 OkHttpCallCallAdapter
  4. OkHttpCall代理了RealCall,额外调用Converter做序列化处理。
  5. CallAdapter代理了OkHttpCall,可以在这里做扩展,将Call转换为实际的返回类型。

4. 协程实现

在看Retrofit如何实现协程之前,先梳理一下协程的基本概念。

当有一个延迟任务,后续的逻辑又需要等待这个任务执行完成返回数据,能继续执行,为了不阻塞线程,一般就需要就需要通过线程调度和传递回调来通信。在使用了协程之后,却只需要像同步代码那样书写,就可以完成这些操作。

但这并不是什么黑魔法,并不是用了协程之后不需要线程调度和传递回调,而是将这些繁琐的事进行复杂的封装并且为我们自动生成。将回调封装成Continuation,将线程的调度封装成调度器。

Continuation ,调用它的 resume 或者 resumeWithException 来返回结果或者抛出异常,跟我们所说的回调一模一样。

调度器的本质是一个协程拦截器,它拦截的对象就是Continuation,进而在其中实现回调的调度。调度器一般使用现成的,类如Dispatchers.Main,如果去挖它的源码,你会发现到了最后,还是使用的handler.post(),也跟我们所说的线程调度一模一样。

而前面有讲到Retrofit的实现很多时候需要依据返回类型做不同的处理,所以就需要了解协程是如何自动生成的回调代码和如何传递回调。写一个简单的协程接口,看一下转换后的Java代码,以及尝试在Java代码中调用协程接口。

可以看到返回值String被封装成了Continuation<String>作为入参传递,思考一下回调不也是这样实现的。

真实的返回值成了Object(用于状态机状态切换)。

//Kotlin代码
interface SuspendService {
suspend fun C(c1: Long): String
}

//字节码转化的Java代码
public interface SuspendService {
@Nullable
Object C(long var1, @NotNull Continuation var3);
}

//尝试在Java中调用suspend方法
class MMM {
SuspendService service;

public static void main(String[] args) {
MMM mmm = new MMM();
mmm.service.C(1L, new Continuation<String>() {
@NonNull
@Override
public CoroutineContext getContext() {
return null;
}

@Override
public void resumeWith(@NonNull Object o) {

}
});
}
}

接着回到动态代理那部分,因为suspend生成的代码会多加一个回调参数Continuation,那么动态代理的时候这个参数就会传入到代理的handler中。

Continuation的创建和使用十分的繁琐,最好的处理方法应该是把它再丢进一个kotlinsuspend方法中,让编译器去处理这些东西,而这个也就是Retrofit实现协程的原理。

interface SuspendService {
@GET("/user")
suspend fun getUser(): Response<User>

@GET("/name")
suspend fun getName(userId: String): Response<String>
}

//动态代理生成字节码示例
class SuspendServiceProxy implements SuspendService {
InvocationHandler handler;

@Nullable
@Override
public Object getUser(@NonNull Continuation<? super Response<User>> $completion) {
return handler.invoke(
this,
SuspendService.class.getMethod("getUser", Continuation.class),
new Object[]{$completion}
);
}

@Nullable
@Override
public Object getName(@NonNull String userId, @NonNull Continuation<? super Response<String>> $completion) {
return handler.invoke(
this,
SuspendService.class.getMethod("getName", String.class, Continuation.class),
new Object[]{userId, $completion}
);
}
}

接着再回到HttpServiceMethod,看看刚才被省略的代码。

在这里面会判断是不是suspend方法,判断的逻辑在RequestFactory中,判断的方法就是判断参数有没有Continuation对象,感兴趣可以去RequestFactory源码瞅瞅。

现在如果是suspend方法,会直接自定义一个类型adapterType,它的实际类型是Call,泛型是实际的返回类型(Response<T>里面的T)。之后将他作为返回类型去创建CallAdapter,而这里实际创建的就是DefaultCallAdapterFactoryExecutorCallbackCall。最后创建SuspendForResponse对象返回。

abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT> {

static <ResponseT, ReturnT> retrofit2.HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {

/**
* 通过判断参数是不是Continuation,标志函数是不是suspend
* 在requestFactory内部处理
*/

boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
...
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType = Utils.getParameterLowerBound(0,
(ParameterizedType) parameterTypes[parameterTypes.length - 1]);

if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
}
/**
* 自己新建一个返回类型,将实际的返回例行包装给Call
*/

adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
} else {
adapterType = method.getGenericReturnType();
}

CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
Type responseType = callAdapter.responseType();

Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);

okhttp3.Call.Factory callFactory = retrofit.callFactory;
if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else {
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
}
}
}

SuspendForResponse中,adapt()的返回类型对应到了suspend的返回类型Object。并且其中的逻辑就是解析出CallContinuation对象,然后有调用KotlinExtensions.awaitResponse(),就如之前说的,它是一个suspend方法,在代理中不处理Continuation,而是交给编译器去处理。

static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;

SuspendForResponse(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
Converter<ResponseBody, ResponseT> responseConverter,
CallAdapter<ResponseT, Call<ResponseT>> callAdapter) {
super(requestFactory, callFactory, responseConverter);
this.callAdapter = callAdapter;
}

@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);

Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
...
return KotlinExtensions.awaitResponse(call, continuation);
}
}

KotlinExtensions.awaitResponse()Call的扩展函数,扩展函数的实现是通过静态方法传入this的方法实现的,所以前面传入awaitResponse()的参数有两个,分别是Call对象和Continuation对象。

KotlinExtensions.awaitResponse()的主体是suspendCancellableCoroutine方法,suspendCancellableCoroutine运行在协程当中并且帮我们获取到当前协程的 CancellableContinuation 实例,CancellableContinuation是一个可取消的Continuation。通过调用它的 invokeOnCancellation 方法可以设置一个取消事件的回调,一旦这个回调被调用,那么意味着调用所在的协程被取消了,这时候我们也要相应的做出取消的响应,也就是把OkHttp发出去的请求给取消掉。这段建议多读几遍。

之后调用Call.enqueue()发送网络请求,在Callback中调用CancellableContinuation的 resume 或者 resumeWithException 来返回结果或者抛出异常。

这里的Callback也是经过了callAdapterOkHttpCall处理,乏了。

suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

//扩展函数示例
fun <T> Call<T>.awaitResponse(){
toString()
}

//扩展函数示例转换为Java代码
public static final void awaitResponse(@NotNull Call $this$awaitResponse) {
$this$awaitResponse.toString();
}

收起阅读 »

大厂Android高频问题:谈谈Activity的启动模式?

前言Activity可以说是Android开发面试高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Activity启动模式时,他最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!启动模式是什...
继续阅读 »

前言

Activity可以说是Android开发面试高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Activity启动模式时,他最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!

  1. 启动模式是什么?
  2. 启动模式如何设置?
  3. Activity的启动模式区别?
  4. 应用场景以及哪些注意的点?

1.activity堆栈流程以及四种启动模式

一个应用由多个Activity构成,多个Activity构成了任务,系统以栈方式进行管理任务(也就是管理多个Activity),管理方式为“先进后出”。

默认情况下,当用户点击App图标后,启动应用,这时会创建一个任务栈,并且将MAIN Activity压入栈中,作为栈底Activity。之后每启动一个Activity,就会将这个Activity压入栈中,显示处于栈顶的Activity。当用户点击“返回”键后,处于栈顶的Activity进行出栈销毁。

Android提供四种Activity的启动模式来进行入栈操作。

standard:

默认值,启动Activity都会重新创建一个Activity的实例进行入栈。此时Activity可能存在多个实例。

image.png

singleTop:

当Activity处于栈顶时,再启动此Activity,不会重新创建实例入栈,而是会使用已存在的实例。

image.png

singleTask:

与singleTop模式相似,只不过singleTop模式是只是针对栈顶的元素,而singleTask模式下,如果task栈内存在目标Activity实例,则:

  1. 将task内的对应Activity实例之上的所有Activity弹出栈。
  2. 将对应Activity置于栈顶,获得焦点。

image.png

singleInstance:

这是我们最后的一种启动模式,也是我们最恶心的一种模式:在该模式下,我们会为目标Activity分配一个新的affinity,并创建一个新的Task栈,将目标Activity放入新的Task,并让目标Activity获得焦点。新的Task有且只有这一个Activity实例。

如果已经创建过目标Activity实例,则不会创建新的Task,而是将以前创建过的Activity唤醒(对应Task设为Foreground状态)

image.png

2.启动模式如何设置?

AndroidMainfest.xml文件设置

设置的lanuchMode属性。可设置四个值: standard、singleTop、singleTask、singleInstance。若不设置默认为standard。

<activity 
android:name=".activity.MainActivity"
android:launchMode="standard"/>

Intent跳转标记Flag

FLAG_ACTIVITY_SINGLE_TOP 等价于 singleTop。位于栈顶的Activity会重用实例,调用onNewIntent函数接收intent。

Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);

FLAG_ACTIVITY_SINGLE_NEW_TASK,启动新的TASK,这个新的TASK取决于xml中设置的TaskAffinity(亲和性)属性。

首先去寻找是否存在相同亲和性的任务,如果存在,那么直接将这个Activity加入到这个任务中。若不存在,则新建一个任务来加入Activity。

FLAG_ACTIVITY_CLEAR_TOP,会将位于此Activity上方的Activity进行出栈销毁。

// singleTask的行为可使用代码表示为
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

两者区别:

  • xml设置为静态的
  • intent标记是动态的。intent标记Flag的优先级更高一些。所以当标记Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP后,尽管Activity为默认的standard模式,也同样会使用存在的实例,调用onNewIntent。

3.亲和性和多个任务并存

亲和性是指Activity设置在AndroidMainfest.xml中的taskAffinity属性。相同亲和性的Activity在同一个任务中,默认使用application的taskAffinity,也就是package name。

并不是设置taskAffinity就一定起作用,起作用是有条件的:

  • 同时设置了launchMode属性为singleTask。
  • Intent跳转时使用FLAG_ACTIVITY_NEW_TASK。
  • 同时设置allowTaskReparenting属性为true。
  • allowTaskReparenting可以使此Activity从启动任务中转移到该taskAffinity的任务中。此时需要发生Task reset(回到Home之后再进入app)才能看出效果。

不同亲和性意味着不同的任务,也就是同一个app中可以存在不同的任务,前台显示的任务的栈顶Activity为用户可见的Activity。当启动一个新的任务时,新的任务会覆盖当前任务。并且回退时,一个任务中Activity全部出栈,会将后台的任务调出,直到最后一任务的最后一个Activity出栈,app结束,回到Home。

例如: 一个应用有Main、A、B、C四个Activity,C的lanuchMode为singleTask,并且taskAffinity设置为.c,其他都为默认,那么按照启动顺序: Main->A->B->C

此时存在两个task:

  • 默认: Main->A->B (后台)
  • .c: C (前台)

由C启动A,那么此时task为:

  • 默认: Main->A->B->A (前台)

  • .c: C (后台)

按回退键:

  • 出栈顺序为: A、B、A、Main、C

4.应用场景以及需要避免的坑

  1. 新闻客户端的推送,点击打开新闻详情页,此时新闻详情页应该设置singleTop,避免用户在新闻详情页打开推送通知,使得回退出现两次详情页。

  2. 利用singleTask的特性,可以使得应用完全退出。

    • 注意:闪屏页+主页+其他的应用,可以设置主页为singleTask,因为闪屏页展示完就finish掉,栈底存在主页,用户点击回退键可以直接关闭应用。

    • 坑:避免启动MAIN的MainActivity设置为singleTask,这样当用户点击HOME,再重新启动应用时,将始终展示MainActivity,并且此时MainActivity走onNewIntent方法。

  3. singleInstance使用比较少,系统应用比如打电话可使用singleInstance。

  4. singleTop、singleTask、singleInstance在使用已存在的Activity实例时,都将走onNewIntent方法。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 小新聊Android 』,不定期分享原创知识
  3. 同时可以期待后续文章ing🚀

收起阅读 »

当我们讨论Android重建机制时在讨论什么?

前言Android应用有一个常常被忽略的问题,但问题出现时你又不得不面对。比如Activity横竖屏转换时Fragment重影应用长时间处于后台,并用户重新切到前台时,Activity显示异样或者需要等待一段时间才能显示内容这类问题都与Activity的恢复重...
继续阅读 »

前言

Android应用有一个常常被忽略的问题,但问题出现时你又不得不面对。比如

  • Activity横竖屏转换时Fragment重影

  • 应用长时间处于后台,并用户重新切到前台时,Activity显示异样或者需要等待一段时间才能显示内容

这类问题都与Activity的恢复重建机制相关,如果你想知道怎么解决这类问题,以及Activity恢复重建机制内部原理。这篇文或许能够帮到你。

1) 什么时候会重建

并不是任何Activity的销毁行为都会触发Activity数据的保存**。只有销毁行为是被系统发起的并且今后有可能恢复的时候才会触发**。

1.1)不会触发重建机制

  • 按返回按键。比如,A Actvivity启动B Activity,BActivity中返回不会调用BActivity的OnSaveInstanceState()方法
  • 最近使用屏幕中滑动关闭 Activity。
  • 从设置中【停止】应用
  • 完成某种“完成”操作,开发者通过调用Activity #finish()方法完成

1.2)有可能会触发重建机制的

触发恢复重建机制就是两大类

  • 系统回收内存时kill掉。
  • 横竖屏切换和语言切换等配置发生变化时kill重建。(可以通过 Activity #isChangingConfigurations()方法判断是否为配置更改发生的)。

当由系统发起而非人为手动关闭Activity的时候,Activity有可能在未来的某个时机恢复重建。Android系统提供了两套机制,用以保存和恢复界面状态。 这两套机制我个人分别给其取名为 Save-Restore InstanceState机制RetainNonConfiguration机制

2)Save-Restore InstanceState机制

1635401794(1).jpg

  • Save-Restore InstanceState机制的初衷是保存界面的一些瞬时状态,比如ListView滑动的位置、ViewPager的position一些TextView的文本内容。保证用户进入新建Activity的时候能尽快的看到界面状态的恢复。
  • Save-Restore InstanceState是一些比较轻量级的数据,因为保存过程需要经历数据的序列化和反序列化。

对于开发者时机操作层面来说,Save-Restore InstanceState机制的核心就是Activity中 onSaveInstanceState() 、onCreate()和onRestoreInstanceState()这三个回调方法。

2.1) onSaveInstanceState()

#Activity
protected void onSaveInstanceState(@NonNull Bundle outState) {
//A、整个view树中的view相关信息有机会保存到整个bundle中
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
if (mAutoFillResetNeeded) {
outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
getAutofillManager().onSaveInstanceState(outState);
}
dispatchActivitySaveInstanceState(outState);
}
  • onSaveInstanceState(outState)的被调用的条件:非手动杀死Activity而是系统kill同时可能会在未来重建时。
  • 在Activity被系统kill时会调用onSaveInstanceState(outState)方法,允许开发者把一些今后重建时需要的一些状态数据存储到outState里面;这个方法的的触发时机是在onStop之前 (Android P开始会在onDestory之前执行)。
  • 默认地,代码A处,onSaveInstance方法会通过window依次调用整个view树的各个view的onSaveInstanceState()方法,view树的每个符合条件的view都有机会存储一些状态。需要注意的是:需要保存view状态的view需要有id作为标识存储在Bundle整个数据结构中。也就是说当view没有id的时候是保存不成功的。

2.2)onCreate(Bundle savedInstanceState)方法

被系统销毁又重建的Activity onCreate(Bundle savedInstanceState)回调方法中savedInstanceState的方法参数不为null。可以在这个位置取出被系统杀死之前保存的一些状态信息用来构建Activity。

2.3)onRestoreInstanceState(Bundle savedInstanceState)

  • 如果Activity是被系统重建的,会触发onRestoreInstanceState(savedInstanceState)方法,开发者可以在savedInstanceState中取出之前被系统销毁时存储的数据,用以在新Activity中恢复状态。

  • onRestoreInstanceState调用时机是在onStart()之后被调用

  • 默认地,onRestoreInstanceState方法会通过 mWindow.restoreHierarchyState()方法把之前保存的view状态信息分发出去,用以恢复view的状态。

protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//window view树有一次机会恢复销毁之前的状态
mWindow.restoreHierarchyState(windowState);
}
}
}

3) 配置改变发生的重建

image.png

当如横竖屏的切换、语言切换等配置发生改变时也会触发Activity的重建。这种由配置发生改变而导致的Activity重建除了会触发Save-Restore InstanceState机制之外也会触发RetainNonConfigurationInstance机制

RetainNonConfigurationInstance机制的核心是Activity中onRetainNonConfigurationInstance()和 getLastNonConfigurationInstance()这两个回调方法也会回调onRetainNonConfigurationInstance()方法,

3.1) onRetainNonConfigurationInstance()

  • 用于保存配置发生前的数据,这个数据理论上是没有结构和大小限制的甚至可以把旧Activity本身保存其中。

  • 触发时机会在onStop之前

public Object onRetainNonConfigurationInstance() {
return null;
}

3.2)getLastNonConfigurationInstance()

  • 当Activity 中getLastNonConfigurationInstance()方法返回值不为空的时候,说明当前这个Activity是因为配置发生变化重建而来的,可以使用这个返回值做一些Activity状态恢复的操作。

public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}

4)两种机制有什么不同

  • 数据上:
    • 通过Save-Restore InstanceState方式保存和恢复界面时只是一些简单的瞬时数据。究其原因这个机制是讲保存的数据传递到了系统进程,在恢复的时候又从系统进程传递到应用进程,数据经历序列化和反序列化,而这些操作又是在主线程。
    • RetainInstance方式数据可以传递一些大数据甚至可以传递Activity本身。究其原因这个机制保存数据还是在当前应用进程,不会经历数据的序列化和反序列化。
  • 触发条件上
    • Save-Restore InstanceState只要Activity有可能被系统kill就会调用onSaveInstance()方法,只要Activity被重建就会在onCreate()方法中传入instanceState数据和调用onRestoreInstaceState()方法。
    • RetaineInstance方式只在系统配置发生变化的时候才生效。

5)AndroidX 做了什么

坊间流传Jetpack中Viewmodel会比Activity的生命周期长,是怎么回事?

阅读这个章节之前如果你对ViewModel的创建比较了解读起来可能会省力些

5.1) 横竖屏切换的时候

在androidx.activity:activity包下的ComponentActivity中关键点,

code 5.5.1
static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}
  • 引入NonConfigurationInstances类,这个类主要有两个属性,分别用来保存自定义数据和viewModelStore.

    code 5.5.2
    public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
    NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) { viewModelStore = nc.viewModelStore; }
    }
    if (viewModelStore == null && custom == null) {
    return null;
    }
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
    }
  • 重写了onRetainNonConfigurationInstance()方法并把方法设置为了final。onRetainNonConfigurationInstance()方法内部创建NonConfigurationInstances对象nci,把viewModelStore存放到nci,同时收集onRetainCustomNonConfigurationInstance()方法的返回值存在nci里

  • 开发者可重写onRetainCustomNonConfigurationInstance()这个方法返回需要保存的数据。

    code 5.5.3
    public ComponentActivity() {
    ...
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    mContextAwareHelper.clearAvailableContext()
    if (!isChangingConfigurations()) { //不在更改配置状态
    getViewModelStore().clear(); //1
    }
    }
    }
    });
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    ensureViewModelStore();
    getLifecycle().removeObserver(this);
    }
    });
    void ensureViewModelStore() { //在NonConfiguration中取出viewModleStore
    if (mViewModelStore == null) {
    NonConfigurationInstances nc =
    (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) {
    mViewModelStore = nc.viewModelStore;
    }
    if (mViewModelStore == null) {
    mViewModelStore = new ViewModelStore();
    }
    }
    }
  • viewmodel中横竖屏转换,横竖屏切换等配置发生变化导致的重建时,新Activity中可听过ensureViewModelStore()方法获取从旧Activity传递过来viewmodelstore,这样就实现了横竖屏切换的时候viewmodel不丢失。

  • 另外值得注意的点是,Activity onDestory的时候,会通过**isChangingConfigurations()**方法判断activity是否处于配置变化状态,如果不是就会将viewmodelstores清空掉。

这套方式解决了一个问题当Activity由横竖屏转换等配置原因发生变化导致Activity重建的时候,会将旧Activity的viewModelStore传给新Activity。如果不是由于配置发生变化导致的Activity重建会清除掉viewModelStore

那有什么方式能解决非配置变化导致Activity重建时保存ViewModel的数据呢?

5.2)非配置变化引起的Activity重建对ViewModel的保存

结论先行,因配置变化引起的Activity重建可以将ViewModleStore保存,在新Activity中可以直接获取旧Activity中的ViewModel。而对于非配置变化引起的Activity重建不能直接将ViewModelStore对象传递给新Activity。AndroidX中是将ViewModel的数据保存到Bundle中,给Bundle分配一个Key,这样ViewModel的保存和恢复就可以通过Save-Restore Stated Instance机制实现。

稍微展开下实现细节

5.2.1) ViewModel销毁时数据保存
  • 数据保存还是通过Activity Save-Restore StateInstace机制,Activity发起saveStatedInstance()时通过调用注册到SavedStateRegistry上SavedStateProvider的saveState()方法获取到对应的Bundle当然最终存储到saveStatedInstance(outBundle)的outBundle中。
  • 通常情况下,ViewModel中会有LiveData,SavedStateHandle中也支持LiveData中数据的保存,SavingStateLiveData继承MutableLiveData复写setValue()方法,设置到LiveData的数据都会保存到mRegular中一份这样实现LiveData数据的保存。
public final class SavedStateHandle {
..
final Map<String, Object> mRegular;
final Map<String, SavedStateProvider> mSavedStateProviders = new HashMap<>();
private final Map<String, SavingStateLiveData<?>> mLiveDatas = new HashMap<>();

private final SavedStateProvider mSavedStateProvider = new SavedStateProvider() {
@SuppressWarnings("unchecked")
@NonNull
@Override
public Bundle saveState() {
...
Set<String> keySet = mRegular.keySet();
ArrayList keys = new ArrayList(keySet.size());
ArrayList value = new ArrayList(keys.size());
for (String key : keySet) {
keys.add(key);
value.add(mRegular.get(key));
}
Bundle res = new Bundle();
//把mRegular保存的数据存放到Bundle中返回
res.putParcelableArrayList("keys", keys);
res.putParcelableArrayList("values", value);
return res;
}
}
5.2.2) ViewModel重建时恢复
  • 带恢复功能的Viewmodel是通过SavedStateViewModelFactory创建,当Activity重建时,会在Activity的onCreate(Bundle data)带后旧Activity存的数据,这bundle中可以取出旧ViewModel的SavedStateHandle对象并以此为构造参数构建ViewModel。这样新建ViewModel就有了旧ViewModel的数据,数据是通过SavedStateHandle对象为介质进行传递的,ViewModel中可以使用对应的key恢复ViewModel的基本数据类型和可序列化的数据类型。

  • 所以,在具备保存-恢复数据特性的ViewModle中获取数据时使用SavedStateHandle对象上的 get(@NonNull String key)方法。获取LiveData()时使用 MutableLiveData getLiveData(String key)方法。内部方法实现是通过key在mRegular中获取到对应的值,再用值作为LiveData初始值创建LiveData。

5.2.3)其他
  • ViewModle中也会存在非序列化的数据(继承了Parcelable或Serializable)或者不能被Bundle存储的对象,如果要保存恢复这些数据怎么实现呢? Lifecycle 2.3.0-alpha03 开始允许设置自定义的SavedStateProvider这样我们可以把非序列化的数据转化成可序列化的数据保存到Bundle中,实现非序列化的数据的保存和恢复。

  • ViewModel的数据保存和恢复虽然逻辑相对比较简单,但是里面涉及到的类和细节比较繁杂这个章节只是说明了一下实现的核心思想,如果大家想了解内部更多的实现细节,今后可以另开一篇展开聊。

6) 最后

  • Android销毁重建机制常常会被开发者忽略,进而造成App线上出现非预期问题甚至crash。开发阶段我们可以通过 开发者模式 ->不保留活动选项,尽早的暴露相关的问题。
  • 关于此类问题的解决时至今日,我们已经有比较完备的工具箱,如果比较熟悉这些工具内部的实现原理机制,在使用这些工具的时候会更得心应手。

不足处批评指正,望不吝点赞


收起阅读 »

Room & Kotlin 符号的处理

Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。 注解处理器非常强大,但它们会增加构建时间。这对于用 Java...
继续阅读 »

Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。


注解处理器非常强大,但它们会增加构建时间。这对于用 Java 写的代码来说通常是可以接受的,但对于 Kotlin 而言,编译时间消耗会非常明显,这是因为 Kotlin 没有一个内置的注解处理管道。相反,它通过 Kotlin 代码生成了存根 Java 代码来支持注解处理器,然后将其输送到 Java 编译器中进行处理。


由于并不是所有 Kotlin 源代码中的内容都能用 Java 表示,因此有些信息会在这种转换中丢失。同样,Kotlin 是一种多平台语言,但 KAPT 只在面向 Java 字节码的情况下生效。


认识 Kotlin 符号处理


随着注解处理器在 Android 上的广泛使用,KAPT 成为了编译时的性能瓶颈。为了解决这个问题,Google Kotlin 编译器团队开始研究一个替代方案,来为 Kotlin 提供一流的注解处理支持。当这个项目诞生之初,我们非常激动,因为它将帮助 Room 更好地支持 Kotlin。从 Room 2.4 开始,它对 KSP 有了实验性的支持,我们发现编译速度提高了 2 倍,特别是在全量编译的情况下。


本文内容重点不在注解的处理、Room 或者 KSP。而在于重点介绍我们在为 Room 添加 KSP 支持时所面临的挑战和所做的权衡。为了理解本文您并不需要了解 Room 或者 KSP,但必须熟悉注解处理。



注意: 我们在 KSP 发布稳定版之前就开始使用它了。因此,尚不确定之前做的一些决策是否适用于现在。



本篇文章旨在让注解处理器的作者们在为项目添加 KSP 支持前,充分了解需要注意的问题。


Room 工作原理简介


Room 的注解处理分为两个步骤。有一些 "Processor" 类,它们遍历用户的代码,验证并提取必要的信息到 "值对象" 中。这些值对象被送到 "Writer" 类中,这些类将它们转换为代码。和其他诸多的注解处理器一样,Room 非常依赖 Auto-Commonjavax.lang.model 包 (Java 注解处理 API 包) 中频繁引用的类。


为了支持 KSP,我们有三种选择:



  1. 复制 JavaAP 和 KSP 的每个 "Processor" 类,它们会有相同的值对象作为输出,我们可以将其输入到 Writer 中;

  2. 在 KSP/Java AP 之上创建一个抽象层,以便处理器拥有一个基于该抽象层的实现;

  3. 用 KSP 代替 JavaAP,并要求开发者也使用 KSP 来处理 Java 代码。


选项 C 实际上是不可行的,因为它会对 Java 用户造成严重的干扰。随着 Room 使用数量的增加,这种破坏性的改变是不可能的。在 "A" 和 "B" 两者之间,我们决定选择 "B",因为处理器具有相当数量的业务逻辑,将其分解并非易事。


认识 X-Processing


在 JavaAP 和 KSP 上创建一个通用的抽象并非易事。Kotlin 和 Java 可以互操作,但模式却不相同,例如,Kotlin 中特殊类的类型如 Kotlin 的值类或者 Java 中的静态方法。此外,Java 类中有字段和方法,而 Kotlin 中有属性和函数。


我们决定实现 "Room 需要什么",而不是尝试去追求完美的抽象。从字面意思来看,在 Room 中找到导入了 javax.lang.model 的每一个文件,并将其移动到 X-Processing 的抽象中。这样一来,TypeElement 变成了 XTypeElementExecutableElemen 变成了 XExecutableElemen 等等。


遗憾的是,javax.lang.model API 在 Room 中的应用非常广泛。一次性创建所有这些 X 类,会给审阅者带来非常严重的心理负担。因此,我们需要找到一种方法来迭代这一实现。


另一方面,我们需要证明这是可行的。所以我们首先对其做了 原型 设计,一旦验证这是一个合理的选择,我们就用他们自己的测试 逐一重新实现了所有 X 类


关于我说的实现 "Room 需要什么",有一个很好的例子,我们可以在关于类的字段 更改 中看到。当 Room 处理一个类的字段时,它总是对其所有的字段感兴趣,包括父类中的字段。所以我们在创建相应的 X-Processing API 时,添加了获取所有字段的能力。


interface XTypeElement {
fun getAllFieldsIncludingPrivateSupers(): List<XVariableElement>
}

如果我们正在设计一个通用库,这样可能永远不会通过 API 审查。但因为我们的目标只是 Room,并且它已经有一个与 TypeElement 具有相同功能的辅助方法,所以复制它可以减少项目的风险。


一旦我们有了基本的 X-Processing API 和它们的测试方法,下一步就是让 Room 来调用这个抽象。这也是 "实现 Room 所需要的东西" 获得良好回报的地方。Room 在 javax.lang.model API 上已经拥有了用于基本功能的扩展函数/属性 (例如获取 TypeElement 的方法)。我们首先更新了这些扩展,使其看起来与 X-Processing API 类似,然后在 1 CL 中将 Room 迁移到 X-Processing。


改进 API 可用性


保留类似 JavaAP 的 API 并不意味着我们不能改进任何东西。在将 Room 迁移到 X-Processing 之后,我们又实现了一系列的 API 改进。


例如,Room 多次调用 MoreElement/MoreTypes,以便在不同的 javax.lang.model 类型 (例如 MoreElements.asType) 之间进行转换。相关调用通常如下所示:


val element: Element ...
if (MoreElements.isType(element)) {
val typeElement:TypeElement = MoreElements.asType(element)
}

我们把所有的调用放到了 Kotlin contracts 中,这样一来就可以写成:


val element: XElement ...
if (element.isTypeElement()) {
// 编译器识别到元素是一个 XTypeElement
}

另一个很好的例子是在一个 TypeElement 中找寻方法。通常在 JavaAP 中,您需要调用 ElementFilter 类来获取 TypeElement 中的方法。与此相反,我们直接将其设为 XTypeElement 中的一个属性。


// 前
val methods = ElementFilter.methodsIn(typeElement.enclosedElements)
// 后
val methods = typeElement.declaredMethods

最后一个例子,这也可能是我最喜欢的例子之一,就是可分配性。在 JavaAP 中,如果您要检查给定的 TypeMirror 是否可以由另一个 TypeMirror 赋值,则需要调用 Types.isAssignable


val type1: TypeMirror ...
val type2: TypeMirror ...
if (typeUtils.isAssignable(type1, type2)) {
...
}

这段代码真的很难读懂,因为您甚至无法猜到它是否验证了类型 1 可以由类型 2 指定,亦或是完全相反的结果。我们已经有一个扩展函数如下:


fun TypeMirror.isAssignableFrom(
types: Types,
otherType: TypeMirror
): Boolean

在 X-Processing 中,我们能够将其转换为 XType 上的常规函数,如下方所示:


interface XType {
fun isAssignableFrom(other: XType): Boolean
}

为 X-Processing 实现 KSP 后端


这些 X-Processing 接口每个都有自己的测试套件。我们编写它们并非是用来测试 AutoCommon 或者 JavaAP 的,相反,编写它们是为了在有了它们的 KSP 实现时,我们就可以运行测试用例来验证它是否符合 Room 的预期。


由于最初的 X-Processing API 是按照 avax.lang.model 建模,它们并非每次都适用于 KSP,所以我们也改进了这些 API,以便在需要时为 Kotlin 提供更好的支持。


这样产生了一个新问题。现有的 Room 代码库是为了处理 Java 源代码而写的。当应用是由 Kotlin 编写时,Room 只能识别该 Kotlin 在 Java 存根中的样子。我们决定在 X-Processing 的 KSP 实现中保持类似行为。


例如,Kotlin 中的 suspend 函数在编译时生成如下签名:


// kotlin
suspend fun foo(bar:Bar):Baz
// java
Object foo(bar:Bar, Continuation<? extends Baz>)

为保持相同的行为,KSP 中的 XMethodElement 实现为 suspend 方法合成了一个新参数,以及新的返回类型。(KspMethodElement.kt)



注意: 这样做效果很好,因为 Room 生成的是 Java 代码,即使在 KSP 中也是如此。当我们添加对 Kotlin 代码生成的支持时,可能会引起一些变化。



另一个例子与属性有关。Kotlin 属性也可能具有基于其签名的合成 getter/setter (访问器)。由于 Room 期望找到这些访问器作为方法 (参见: KspTypeElement.kt),因此 XTypeElement 实现了这些合成方法。



注意 : 我们已有计划更改 XTypeElement API 以提供属性而非字段,因为这才是 Room 真正想要获取的内容。正如您现在猜到的那样,我们决定 "暂时" 不这样做来减少 Room 的修改。希望有一天我们能够做到这一点,当我们这样做时,XTypeElement 的 JavaAP 实现将会把方法和字段作为属性捆绑在一起。



在为 X-Processing 添加 KSP 实现时,最后一个有趣的问题是 API 耦合。这些处理器的 API 经常相互访问,因此如果不实现 XField / XMethod,就不能在 KSP 中实现 XTypeElement,而 XField / XMethod 本身又引用了 XType 等等。在添加这些 KSP 实现的同时,我们为它们的实现部分写了单独的测试用例。当 KSP 的实现变得更加完整时,我们逐渐通过 KSP 后端启动全部的 X-Processing 测试。


需要注意的是,在此阶段我们只在 X-Processing 项目中运行测试,所以即使我们知道测试的内容没问题,我们也无法保证所有的 Room 测试都能通过 (也称之为单元测试 vs 集成测试)。我们需要通过一种方法来使用 KSP 后端运行所有的 Room 测试,"X-Processing-Testing" 就应运而生。


认识 X-Processing-Testing


注解处理器的编写包含 20% 的处理器代码和 80% 的测试代码。您需要考虑到各种可能的开发者错误,并确保如实报告错误消息。为了编写这些测试,Room 已经提供一个辅助方法如下:


 

runTest 在底层使用了 Google Compile Testing 库,并允许我们简单地对处理器进行单元测试。它合成了一个 Java 注解处理器并在其中调用了处理器提供的 process 方法。


val entitySource : JavaFileObject //示例 @Entity 注释类
val result = runTest(entitySource) { invocation ->
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
}
// 断言结果是否有误,警告等

糟糕的是,Google Compile Testing 仅支持 Java 源代码。为了测试 Kotlin 我们需要另一个库,幸运的是有 Kotlin Compile Testing,它允许我们编写针对 Kotlin 的测试,而且我们为该库贡献了对 KSP 支持。



注意 : 我们后来用 内部实现 替换了 Kotlin Compile Testing,以简化 AndroidX Repo 中的 Kotlin/KSP 更新。我们还添加了更好的断言 API,这需要我们对 KCT 执行 API 不兼容的修改操作。



作为能让 KSP 运行所有测试的最后一步,我们创建了以下测试 API:


fun runProcessorTest(
sources: List<Source>,
handler: (XTestInvocation) -> Unit
): Unit

这个和原始版本之间的主要区别在于,它同时通过 KSP 和 JavaAP (或 KAPT,取决于来源) 运行测试。因为它多次运行测试且 KSP 和 JavaAP 两者的判断结果不同,因此无法返回单个结果。


因此,我们想到了一个办法:


fun XTestInvocation.assertCompilationResult(
assertion: (XCompilationResultSubject) -> Unit
}

每次编译后,它都会调用结果断言 (如果没有失败提示,则检查编译是否成功)。我们把每个 Room 测试重构为如下所示:


val entitySource : Source //示例 @Entity 注释类
runProcessorTest(listOf(entitySource)) { invocation ->
// 该代码块运行两次,一次使用 JavaAP/KAPT,一次使用 KSP
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
invocation.assertCompilationResult {
// 结果被断言为是否有 error,warning 等
hasWarningContaining("...")
}
}

接下来的事情就很简单了。将每个 Room 的编译测试迁移到新的 API,一旦发现新的 KSP / X-Processing 错误,就会上报,然后实施临时解决方案;这一动作反复进行。由于 KSP 正在大力开发中,我们确实遇到了很多 bug。每一次我们都会上报 bug,从 Room 源链接到它,然后继续前进 (或者进行修复)。每当 KSP 发布之后,我们都会搜索代码库来找到已修复的问题,删除临时解决方案并启动测试。


一旦编译测试覆盖情况较好,我们在下一步就会使用 KSP 运行 Room 的 集成测试。这些是实际的 Android 测试应用,也会在运行时测试其行为。幸运的是,Android 支持 Gradle 变体,因此使用 KSP 和 KAPT 来运行我们 Kotlin 集成测试 便相当容易。


下一步


将 KSP 支持添加到 Room 只是第一步。现在,我们需要更新 Room 来使用它。例如,Room 中的所有类型检查都忽略了 nullability,因为 javax.lang.modelTypeMirror 并不理解 nullability。因此,当调用您的 Kotlin 代码时,Room 有时会在运行时触发 NullPointerException。有了 KSP,这些检查现在可在 Room 中创建新的 KSP bug (例如 b/193437407)。我们已经添加了一些临时解决方案,但理想情况下,我们仍希望 改进 Room 以正确处理这些情况。


同样,即使我们支持 KSP,Room 仍然只生成 Java 代码。这种限制使我们无法添加对某些 Kotlin 特性的支持,比如 Value Classes。希望在将来,我们还能对生成 Kotlin 代码提供一些支持,以便在 Room 中为 Kotlin 提供一流的支持。接下来,也许更多 :)。


我能在我的项目上使用 X-Processing 吗?


答案是还不能;至少与您使用任何其他 Jetpack 库的方式不同。如前文所述,我们只实现了 Room 需要的部分。编写一个真正的 Jetpack 库有很大的投入,比如文档、API 稳定性、Codelabs 等,我们无法承担这些工作。话虽如此,Dagger 和 Airbnb (ParisDeeplinkDispatch) 都开始用 X-Processing 来支持 KSP (并贡献了他们需要的东西🙏)。也许有一天我们会把它从 Room 中分解出来。从技术层面上讲,您仍然可以像使用 Google Maven 库 一样使用它,但是没有 API 保证可以这样做,因此您绝对应该使用 shade 技术。


总结


我们为 Room 添加了 KSP 支持,这并非易事但绝对值得。如果您在维护注解处理器,请添加对 KSP 的支持,以提供更好的 Kotlin 开发者体验。


特别感谢 Zac SweersEli Hart 审校这篇文章的早期版本,他们同时也是优秀的 KSP 贡献者。


更多资源



欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!


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

坏了!面试官问我垃圾回收机制

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛 面试官:当时感觉再讲下去那我可能就得加班了 面试官:今天有点空了,继续聊聊「堆」那块吧 候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Ed...
继续阅读 »

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛


面试官:当时感觉再讲下去那我可能就得加班了


面试官今天有点空了,继续聊聊「堆」那块吧


候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「survivor」区又分为「From Survivor」和「To Survivor」区



候选者:说到这里,我就想聊聊Java的垃圾回收机制了


面试官:那你开始你的表演吧


候选者:我们使用Java的时候,会创建很多对象,但我们未曾「手动」将这些对象进行清除


候选者:而如果用C/C++语言的时候,用完是需要自己free(释放)掉的


候选者:那为什么在写Java的时候不用我们自己手动释放”垃圾”呢?原因很简单,JVM帮我们做了(自动回收垃圾)


面试官:嗯…


候选者:我个人对垃圾的定义:只要对象不再被使用了,那我们就认为该对象就是垃圾,对象所占用的空间就可以被回收



面试官那是怎么判断对象不再被使用的呢?


候选者:常用的算法有两个「引用计数法」和「可达性分析法」


候选者:引用计数法思路很简单:当对象被引用则+1,但对象引用失败则-1。当计数器为0时,说明对象不再被引用,可以被可回收


候选者:引用计数法最明显的缺点就是:如果对象存在循环依赖,那就无法定位该对象是否应该被回收(A依赖B,B依赖A)


面试官:嗯…


候选者:另一种就是可达性分析法:它从「GC Roots」开始向下搜索,当对象到「GC Roots」都没有任何引用相连时,说明对象是不可用的,可以被回收



候选者:「GC Roots」是一组必须「活跃」的引用。从「GC Root」出发,程序通过直接引用或者间接引用,能够找到可能正在被使用的对象


面试官还是不太懂,那「GC Roots」一般是什么?你说它是一组活跃的引用,能不能举个例子,太抽象了。


候选者:比如我们上次不是聊到JVM内存结构中的虚拟机栈吗,虚拟机栈里不是有栈帧吗,栈帧不是有局部变量吗?局部变量不就存储着引用嘛。


候选者:那如果栈帧位于虚拟机栈的栈顶,是不是就可以说明这个栈帧是活跃的(换言之,是线程正在被调用的)


候选者:既然是线程正在调用的,那栈帧里的指向「堆」的对象引用,是不是一定是「活跃」的引用?


候选者:所以,当前活跃的栈帧指向堆里的对象引用就可以是「GC Roots」


面试官:嗯…


候选者:当然了,能作为「GC Roots」也不单单只有上面那一小块


候选者:比如类的静态变量引用是「GC Roots」,被「Java本地方法」所引用的对象也是「GC Roots」等等…



候选者:回到理解的重点:「GC Roots」是一组必须「活跃」的「引用」,只要跟「GC Roots」没有直接或者间接引用相连,那就是垃圾


候选者:JVM用的就是「可达性分析算法」来判断对象是否垃圾


面试官:懂了


候选者:垃圾回收的第一步就是「标记」,标记哪些没有被「GC Roots」引用的对象



候选者:标记完之后,我们就可以选择直接「清除」,只要不被「GC Roots」关联的,都可以干掉


候选者:过程非常简单粗暴,但也存在很明显的问题


候选者:直接清除会有「内存碎片」的问题:可能我有10M的空余内存,但程序申请9M内存空间却申请不下来(10M的内存空间是垃圾清除后的,不连续的)



候选者:那解决「内存碎片」的问题也比较简单粗暴,「标记」完,不直接「清除」。


候选者:我把「标记」存活的对象「复制」到另一块空间,复制完了之后,直接把原有的整块空间给干掉!这样就没有内存碎片的问题了


候选者:这种做法缺点又很明显:内存利用率低,得有一块新的区域给我复制(移动)过去


面试官:嗯…


候选者:还有一种「折中」的办法,我未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动


候选者:把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛


候选者:这种专业的术语就叫做「整理」



候选者:扯了这么久,我们把思维再次回到「堆」中吧


候选者:经过研究表明:大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间


候选者:又由于「垃圾回收」是会导致「stop the world」(应用停止访问)


候选者:理解「stop the world」应该很简单吧:回收垃圾的时候,程序是有短暂的时间不能正常继续运作啊。不然JVM在回收的时候,用户线程还继续分配修改引用,JVM怎么搞(:


候选者:为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率


候选者:在很多的垃圾收集器上都会在「物理」或者「逻辑」上,把这两类对象进行区分,死得快的对象所占的区域叫做「年轻代」,活得久的对象所占的区域叫做「老年代」



候选者:但也不是所有的「垃圾收集器」都会有,只不过我们现在线上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。


候选者:所以,你可以看到我的「堆」是画了「年轻代」和「老年代」


候选者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是没有分代的概念的(:


候选者:只不过我为了好说明现状,ZGC的话有空我们再聊


面试官:嗯…好吧


候选者:在前面更前面提到了垃圾回收的过程,其实就对应着几种「垃圾回收算法」,分别是:


候选者:标记清除算法、标记复制算法和标记整理算法【「标记」「清除」「复制」「整理」】


候选者:经过上面的铺垫之后,这几种算法应该还是比较好理解的



候选者:「分代」和「垃圾回收算法」都搞明白了之后,我们就可以看下在JDK8生产环境及以下常见的垃圾回收器了


候选者:「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew


候选者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS


候选者:看着垃圾收集器有很多,其实还是非常好理解的。Serial是单线程的,Parallel是多线程


候选者:这些垃圾收集器实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)


候选者:CMS是「JDK8之前」是比较新的垃圾收集器,它的特点是能够尽可能减少「stop the world」时间。在垃圾回收时让用户线程和 GC 线程能够并发执行!



候选者:又可以发现的是,「年轻代」的垃圾收集器使用的都是「标记复制算法」


候选者:所以在「堆内存」划分中,将年轻代划分出Survivor区(Survivor From 和Survivor To),目的就是为了有一块完整的内存空间供垃圾回收器进行拷贝(移动)


候选者:而新的对象则放入Eden区


候选者:我下面重新画下「堆内存」的图,因为它们的大小是有默认的比例的



候选者:图我已经画好了,应该就不用我再说明了


面试官我还想问问,就是,新创建的对象一般是在「新生代」嘛,那在什么时候会到「老年代」中呢?


候选者:嗯,我认为简单可以分为两种情况:


候选者:1. 如果对象太大了,就会直接进入老年代(对象创建时就很大 || Survivor区没办法存下该对象)


候选者:2. 如果对象太老了,那就会晋升至老年代(每发生一次Minor GC ,存活的对象年龄+1,达到默认值15则晋升老年代 || 动态对象年龄判定 可以进入老年代)



面试官既然你又提到了Minor GC,那Minor GC 什么时候会触发呢?


候选者:当Eden区空间不足时,就会触发Minor GC


面试官:Minor GC 在我的理解就是「年轻代」的GC,你前面又提到了「GC Roots」嘛


面试官那在「年轻代」GC的时候,从GC Roots出发,那不也会扫描到「老年代」的对象吗?那那那..不就相当于全堆扫描吗?


候选者:这JVM里也有解决办法的。


候选者:HotSpot 虚拟机「老的GC」(G1以下)是要求整个GC堆在连续的地址空间上。


候选者:所以会有一条分界线(一侧是老年代,另一侧是年轻代),所以可以通过「地址」就可以判断对象在哪个分代上


候选者:当做Minor GC的时候,从GC Roots出发,如果发现「老年代」的对象,那就不往下走了(Minor GC对老年代的区域毫无兴趣)



面试官但又有个问题,那如果「年轻代」的对象被「老年代」引用了呢?(老年代对象持有年轻代对象的引用),那时候肯定是不能回收掉「年轻代」的对象的


候选者:HotSpot虚拟机下 有「card table」(卡表)来避免全局扫描「老年代」对象


候选者:「堆内存」的每一小块区域形成「卡页」,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为「脏页」


候选者:那知道了「卡表」之后,就很好办了。每次Minor GC 的时候只需要去「卡表」找到「脏页」,找到后加入至GC Root,而不用去遍历整个「老年代」的对象了。



面试官:嗯嗯嗯,还可以的啊,要不继续聊聊CMS?


候选者:这面试快一个小时了吧,我图也画了这么多了。下次?下次吧?有点儿累了


本文总结



  • 什么是垃圾:只要对象不再被使用,那即是垃圾

  • 如何判断为垃圾:可达性分析算法和引用计算算法,JVM使用的是可达性分析算法

  • 什么是GC Roots:GC Roots是一组必须活跃的引用,跟GC Roots无关联的引用即是垃圾,可被回收

  • 常见的垃圾回收算法:标记清除、标记复制、标记整理

  • 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率。

  • Minor GC:当Eden区满了则触发,从GC Roots往下遍历,年轻代GC不关心老年代对象

  • 什么是card table【卡表】:空间换时间(类似bitmap),能够避免扫描老年代的所有对应进而顺利进行Minor GC (案例:老年代对象持有年轻代对象引用)

  • 堆内存占比:年轻代占堆内存1/3,老年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代2/10(其中From 和To 各站1/10)

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

flutter 数字键盘、自定义键盘

有些特殊的场景 会遇到使用数字键盘的特殊场景,例如输入金额、数量 number_keypan.dart =》文件内容如下: import 'package:flutter/material.dart'; /// <summary> /// ...
继续阅读 »

有些特殊的场景 会遇到使用数字键盘的特殊场景,例如输入金额、数量


image.png


number_keypan.dart =》文件内容如下:


import 'package:flutter/material.dart';

/// <summary>
/// todo: 数字键盘
/// author:zwb
/// dateTime:2021/7/19 10:25
/// filePath:lib/widgets/number_keypan.dart
/// desc: 示例
/// <summary>
// OverlayEntry overlayEntry;
// TextEditingController controller = TextEditingController();
//
// numberKeypan(
// initialization: (v){
// /// 初始化
// overlayEntry = v;
// /// 唤起键盘
// openKeypan(context: context);
// },
// onDel: (){
// delCursor(textEditingController: controller);
// },
// onTap: (v){
// /// 更新输入框的值
// controller.text += v;
// /// 保持光标
// lastCursor(textEditingController: controller);
// },
// );
OverlayEntry overlayEntry;
NumberKeypan({@required Function(OverlayEntry) initialization,@required Function(String) onTap,Function onCommit,Function onDel,}){
overlayEntry = OverlayEntry(builder: (context) {
List<String> list = ['1','2','3','4','5','6','7','8','9','','0','删除'];
return new Positioned(
bottom: 0,
child: new Material(
child: new Container(
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
color: Colors.grey[200],
child: Row(
children: [
Expanded(
child: Wrap(
alignment: WrapAlignment.spaceBetween,
children: List.generate(list.length, (index) {
return Material(
color: Colors.white,
child: Ink(
child: InkWell(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200],width: 0.25),
),
alignment: Alignment.center,
height: 50,
width: (MediaQuery.of(context).size.width) / 3,
child: Text("${list[index]}",style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold),),
),
onTap: index == 11 ? onDel : (){
if(list[index] != "" && list[index] !="删除"){
onTap(list[index]);
}
},
),
color: Colors.white,
),
);
}),
),
),
// Column(
// children: [
// SizedBox(
// width: 60,
// height: 50 * 1.5,
// child: MaterialButton(
// onPressed: onDel ?? (){},
// child: Text("删除",style: TextStyle(color: Colors.black,fontWeight: FontWeight.bold)),
// color: Colors.grey[100],
// elevation: 0,
// padding: EdgeInsets.all(0),),
// ),
// SizedBox(
// width: 60,
// height: 50 * 2.5,
// child: MaterialButton(
// onPressed: (){
// disKeypan();
// if(onCommit != null ) onCommit();
// },
// child: Text("确认",style: TextStyle(color: Colors.white,fontWeight: FontWeight.bold),),
// color: Colors.blue,
// elevation: 0,
// padding: EdgeInsets.all(0),
// ),
// ),
// ],
// ),
],
),
),
));
});
initialization(overlayEntry);
}

/// <summary>
/// todo: 保持光标在最后
/// author: zwb
/// date: 2021/7/19 11:43
/// param: 参数
/// return: void
/// <summary>
///
lastCursor({@required TextEditingController textEditingController}){
/// 保持光标在最后
final length = textEditingController.text.length;
textEditingController.selection = TextSelection(baseOffset:length , extentOffset:length);
}

/// <summary>
/// todo: 自定义键盘的删除事件
/// author: zwb
/// date: 2021/7/19 11:45
/// param: 参数
/// return: void
/// <summary>
///
delCursor({@required TextEditingController textEditingController}){
if(textEditingController != null && textEditingController.value.text != "") textEditingController.text = textEditingController.text.substring(0,textEditingController.text.length - 1);
}

/// <summary>
/// todo: 打开键盘
/// author: zwb
/// date: 2021/7/19 12:04
/// param: 参数
/// return: void
/// <summary>
///
openKeypan({BuildContext context}){
Overlay.of(context).insert(overlayEntry);
}

/// <summary>
/// todo: 销毁键盘
/// author: zwb
/// date: 2021/7/19 12:03
/// param: 参数
/// return: void
/// <summary>
///
disKeypan(){
if(overlayEntry!=null) overlayEntry.remove();
}

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

这一次,解决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 被析构
收起阅读 »