BottomFragment

底部弹出控件 - Fragment 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.dench.baselib.R
import com.dench.baselib.databinding.FragmentBottomBinding

class BottomFragment : Fragment() {
companion object {
fun start(fm: FragmentManager, fragment: Fragment): BottomFragment {
val bottomFragment = BottomFragment().apply {
setFragment(fragment)
}
fm.beginTransaction()
.setCustomAnimations(
R.anim.fragment_bottom_enter,
0,
0,
R.anim.fragment_bottom_exit
)
.add(android.R.id.content, bottomFragment)
.addToBackStack(null)
.commitAllowingStateLoss()
return bottomFragment
}
}

private lateinit var fragment: Fragment

private fun setFragment(fragment: Fragment) {
this.fragment = fragment
}

private lateinit var binding: FragmentBottomBinding
private val TAG = "BottomFragment"

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentBottomBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.run {
binding.bottomFragmentRl.setOnClickListener(View.OnClickListener {
Log.d(TAG, "bottom root view click.")
dismissSelf()
})

/** add fragment */
childFragmentManager.beginTransaction()
.add(R.id.container, fragment)
.commitAllowingStateLoss()
}
}

private fun dismissSelf() {
parentFragmentManager.popBackStack()
}
}

为什么选择Netty

为什么选择Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他如Strom还有业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

通过对Netty的分析,我们将它的优点总结如下。

◎ API使用简单,开发门槛低;

◎ 功能强大,预置了多种编解码功能,支持多种主流协议;

◎ 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;

◎ 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;

◎ 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

◎ 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;

◎ 经历了大规模的商业应用考验,质量得到验证。Netty在互联网、大数据、网络游戏、企业应用、电信软件等众多行业已经得到了成功商用,证明它已经完全能够满足不同行业的商业应用了。

正是因为这些优点,Netty逐渐成为了Java NIO编程的首选框架。

Netty 是什么?

那Netty到底是什么?官方解释:Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。Netty就是一个对Jdk的Nio进行封装的一个框架。

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.

https://netty.io/

在开始了解 Netty 是什么之前,我们先来回顾一下,如果我们需要实现一个客户端与服务端通信的程序,使用传统的 IO 编程,应该如何来实现?

IO编程

我们简化下场景:客户端每隔两秒发送一个带有时间戳的 “hello world” 给服务端,服务端收到之后打印。

为了方便演示,下面例子中,服务端和客户端各一个类,把这两个类拷贝到你的 IDE 中,先后运行 IOServer.javaIOClient.java可看到效果。

下面是传统的 IO 编程中服务端实现

IOServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @author 闪电侠
*/
public class IOServer {
public static void main(String[] args) throws Exception {

ServerSocket serverSocket = new ServerSocket(8000);

// (1) 接收新连接线程
new Thread(() -> {
while (true) {
try {
// (1) 阻塞方法获取新的连接
Socket socket = serverSocket.accept();

// (2) 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// (3) 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();

} catch (IOException e) {
}

}
}).start();
}
}

Server 端首先创建了一个serverSocket来监听 8000 端口,然后创建一个线程,线程里面不断调用阻塞方法 serversocket.accept();获取新的连接,见(1),当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据,见(2),然后读取数据是以字节流的方式,见(3)。

下面是传统的IO编程中客户端实现

IOClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author 闪电侠
*/
public class IOClient {

public static void main(String[] args) {
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 8000);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}

客户端的代码相对简单,连接上服务端 8000 端口之后,每隔 2 秒,我们向服务端写一个带有时间戳的 “hello world”。

IO 编程模型在客户端较少的情况下运行良好,但是对于客户端比较多的业务来说,单机服务端可能需要支撑成千上万的连接,IO 模型可能就不太合适了,我们来分析一下原因。

上面的 demo,从服务端代码中我们可以看到,在传统的 IO 模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个 while 死循环,那么 1w 个连接对应 1w 个线程,继而 1w 个 while 死循环,这就带来如下几个问题:

  1. 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起
  2. 线程切换效率低下:单机 CPU 核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
  3. 除了以上两个问题,IO 编程中,我们看到数据读写是以字节流为单位。

为了解决这三个问题,JDK 在 1.4 之后提出了 NIO。

NIO 编程

关于 NIO 相关的文章网上也有很多,这里不打算详细深入分析,下面简单描述一下 NIO 是如何解决以上三个问题的。

线程资源受限

NIO 编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责,那么他是怎么做到的?我们用一幅图来对比一下 IO 与 NIO

image.png

如上图所示,IO 模型中,一个连接来了,会创建一个线程,对应一个 while 死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w 个连接里面同一时刻只有少量的连接有数据可读,因此,很多个 while 死循环都白白浪费掉了,因为读不出啥数据。

而在 NIO 模型中,他把这么多 while 死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个 while 死循环就能监测1w个连接是否有数据可读的呢? 这就是 NIO 模型中 selector 的作用,一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明 IO 与 NIO 的区别。

在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有 100 个小朋友,有两种方案可以解决小朋友上厕所的问题:

  1. 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100 个小朋友就需要 100 个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
  2. 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。

这就是 NIO 模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于 IO 模型中一个线程管理一条连接,消耗的线程资源大幅减少

线程切换效率低下

由于 NIO 模型中线程数量大大降低,线程切换效率因此也大幅度提高

IO读写面向流

IO 读写是面向流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,你需要自己缓存数据。 而 NIO 的读写是面向 Buffer 的,你可以随意读取里面任何一个字节数据,不需要你自己缓存数据,这一切只需要移动读写指针即可。

简单讲完了 JDK NIO 的解决方案之后,我们接下来使用 NIO 的方案替换掉 IO 的方案,我们先来看看,如果用 JDK 原生的 NIO 来实现服务端,该怎么做

前方高能预警:以下代码可能会让你感觉极度不适,如有不适,请跳过

NIOServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* @author 闪电侠
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();

new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}

}
}
}
} catch (IOException ignored) {
}

}).start();


new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}

}
}
}
} catch (IOException ignored) {
}
}).start();


}
}

相信大部分没有接触过 NIO 的同学应该会直接跳过代码来到这一行:原来使用 JDK 原生 NIO 的 API 实现一个简单的服务端通信程序是如此复杂!

复杂得我都没耐心解释这一坨代码的执行逻辑(开个玩笑),我们还是先对照 NIO 来解释一下几个核心思路

  1. NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在我们这个例子中serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读
  2. 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等,参见(1)
  3. clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过 clientSelector.select(1)方法可以轮询出来,进而批量处理,参见(2)
  4. 数据的读写面向 Buffer,参见(3)

其他的细节部分,我不愿意多讲,因为实在是太复杂,你也不用对代码的细节深究到底。总之,强烈不建议直接基于JDK原生NIO来进行网络开发,下面是我总结的原因

  1. JDK 的 NIO 编程需要了解很多的概念,编程复杂,对 NIO 入门非常不友好,编程模型不友好,ByteBuffer 的 Api 简直反人类
  2. 对 NIO 编程来说,一个比较合适的线程模型能充分发挥它的优势,而 JDK 没有给你实现,你需要自己实现,就连简单的自定义协议拆包都要你自己实现
  3. JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
  4. 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug

正因为如此,我客户端代码都懒得写给你看了==!,你可以直接使用IOClient.javaNIOServer.java通信

JDK 的 NIO 犹如带刺的玫瑰,虽然美好,让人向往,但是使用不当会让你抓耳挠腮,痛不欲生,正因为如此,Netty 横空出世!

Netty编程

那么 Netty 到底是何方神圣? 用一句简单的话来说就是:Netty 封装了 JDK 的 NIO,让你用得更爽,你不用再写一大堆复杂的代码了。 用官方正式的话来说就是:Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

下面是我总结的使用 Netty 不使用 JDK 原生 NIO 的原因

  1. 使用 JDK 自带的NIO需要了解太多的概念,编程复杂,一不小心 bug 横飞
  2. Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从 NIO 模型变身为 IO 模型
  3. Netty 自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
  4. Netty 解决了 JDK 的很多包括空轮询在内的 Bug
  5. Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理
  6. 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
  7. Netty 社区活跃,遇到问题随时邮件列表或者 issue
  8. Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大

看不懂没有关系,这些我们在后续的课程中我们都可以学到,接下来我们用 Netty 的版本来重新实现一下本文开篇的功能吧

首先,引入 Maven 依赖,本文后续 Netty 都是基于 4.1.6.Final 版本

1
2
3
4
5
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>

然后,下面是服务端实现部分

NettyServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author 闪电侠
*/
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();

NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
.bind(8000);
}
}

这么一小段代码就实现了我们前面 NIO 编程中的所有的功能,包括服务端启动,接受新连接,打印客户端传来的数据,怎么样,是不是比 JDK 原生的 NIO 编程优雅许多?

初学 Netty 的时候,由于大部分人对 NIO 编程缺乏经验,因此,将 Netty 里面的概念与 IO 模型结合起来可能更好理解

  1. boss 对应 IOServer.java 中的接受新连接线程,主要负责创建新连接
  2. worker 对应 IOServer.java 中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理

然后剩下的逻辑我在后面的系列文章中会详细分析,你可以先把这段代码拷贝到你的 IDE 里面,然后运行 main 函数

然后下面是客户端 NIO 的实现部分

NettyClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author 闪电侠
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();

bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
});

Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

while (true) {
channel.writeAndFlush(new Date() + ": hello world!");
Thread.sleep(2000);
}
}
}

在客户端程序中,group对应了我们IOClient.java中 main 函数起的线程,剩下的逻辑我在后面的文章中会详细分析,现在你要做的事情就是把这段代码拷贝到你的 IDE 里面,然后运行 main 函数,最后回到 NettyServer.java 的控制台,你会看到效果。

使用 Netty 之后是不是觉得整个世界都美好了,一方面 Netty 对 NIO 封装得如此完美,写出来的代码非常优雅,另外一方面,使用 Netty 之后,网络通信这块的性能问题几乎不用操心,尽情地让 Netty 榨干你的 CPU 吧。

资料:
1、选择Netty作为基础通信框架 :
https://www.cnblogs.com/mxyhws/p/5500425.html

2、Nginx/Netty/ZeroMQ网络模型:

https://blog.csdn.net/kobejayandy/article/details/20294909

Android专栏-WebView

Android专栏-WebView

0x00 常规WebViewActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package com.dench.webviewlib

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Gravity
import android.webkit.*
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.alibaba.android.arouter.facade.annotation.Autowired
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.dench.baselib.provider.WebViewService
import com.dench.baselib.utlis.StatusBarHelper
import com.dench.webviewlib.bridge.JsInterface
import com.dench.webviewlib.databinding.ActivityWebViewBinding
import kotlinx.android.synthetic.main.activity_web_view.*

@Route(path = WebViewService.activityPath)
class WebViewActivity : AppCompatActivity() {
@Autowired
lateinit var title: String

@Autowired
lateinit var url: String

private lateinit var dataViewBinding: ActivityWebViewBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this)
StatusBarHelper.fitSystemBar(this, false)

dataViewBinding = DataBindingUtil.setContentView(this, R.layout.activity_web_view)

initToolbar()

initWebView()
}

override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
return
}
super.onBackPressed()
}

private fun initToolbar() {
dataViewBinding.toolbar.titleTv.text = title
val backIv = dataViewBinding.toolbar.leftIv
backIv.setImageResource(R.drawable.ic_close)
backIv.setOnClickListener {// 关闭
finish()
}
}

@SuppressLint("JavascriptInterface")
private fun initWebView() {
// webView Settings
initWebViewSetting()

// init Client
initClient()

// add Javascript Interface
webView.addJavascriptInterface(JsInterface(this), JsInterface.NAME)

// register Scroll Listener
registerScrollListener()

loadUrl()
}

// webView Settings
@SuppressLint("SetJavaScriptEnabled")
private fun initWebViewSetting() {
//声明WebSettings子类
val webSettings = webView.settings

//如果访问的页面中要与Javascript交互,则webView必须设置支持Javascript
webSettings.javaScriptEnabled = true
webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口

//设置自适应屏幕,两者合用
webSettings.useWideViewPort = true //将图片调整到适合webView的大小
webSettings.loadWithOverviewMode = true // 缩放至屏幕的大小
//缩放操作
webSettings.setSupportZoom(true)//支持缩放,默认为true。是下面那个的前提。
webSettings.builtInZoomControls = true //设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.displayZoomControls = false //隐藏原生的缩放控件
// 缓存
webSettings.cacheMode = WebSettings.LOAD_DEFAULT //webView缓存策略
webSettings.domStorageEnabled = true
webSettings.databaseEnabled = true
webSettings.setAppCacheEnabled(true)

//其他
webSettings.allowFileAccess = true //设置可以访问文件
webSettings.loadsImagesAutomatically = true //支持自动加载图片
webSettings.defaultTextEncodingName = "utf-8" //设置编码格式

// 在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
}

// init Client
private fun initClient() {
webView.webViewClient = object : WebViewClient() {

// 在网页上的所有加载都经过这个方法
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
Log.i(_tag, "shouldOverrideUrlLoading()")
request?.url?.let {
return when (it.scheme) {
"http", "https" -> { // 加载网络html
super.shouldOverrideUrlLoading(view, request)
}
"file", "content" -> { // 加载本地html
super.shouldOverrideUrlLoading(view, request)
}
else -> { // 特殊 scheme 不处理
showToast("$it")
true
}
}
}
return true
}

// 加载页面的服务器出现错误时(如404)调用
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
Log.i(_tag, "onReceivedError()")
super.onReceivedError(view, request, error)
}

// ssl 证书错误
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
Log.i(_tag, "onReceivedSslError()")
handler?.proceed() //表示等待证书响应
// handler?.cancel() //表示挂起连接,为默认方式
// handler?.handleMessage(null) //可做其他处理
}

// 开始载入页面调用的。我们可以设定一个loading的页面,告诉用户程序在等待网络响应
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
Log.i(_tag, "onPageStarted()")
Log.d(
_tag,
"onPageStarted() called with: view = $view, url = $url, favicon = $favicon"
)
super.onPageStarted(view, url, favicon)
webViewProgress.show()
}

// 在页面加载结束时调用。我们可以关闭loading 条,切换程序动作
override fun onPageFinished(view: WebView?, url: String?) {
Log.i(_tag, "onPageFinished()")
super.onPageFinished(view, url)
webViewProgress.hide()
}

// 在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次
override fun onLoadResource(view: WebView?, url: String?) {
Log.d(_tag, "onLoadResource()")
Log.d(_tag, "onLoadResource() called with: view = $view, url = $url")
super.onLoadResource(view, url)
}
}

webView.webChromeClient = object : WebChromeClient() {
// 加载进度
override fun onProgressChanged(view: WebView?, newProgress: Int) {
webViewProgress.progress = newProgress
}

// Title
override fun onReceivedTitle(view: WebView?, title: String?) {
if (!TextUtils.isEmpty(title)) {
findViewById<TextView>(R.id.titleTv).text = title
}
}
}
}

// 加载URL
private fun loadUrl() {
//方式1. 加载一个网页
webView.loadUrl(url)
// //方式2:加载apk包中的html页面
// webView.loadUrl("file:///android_asset/test.html")
// //方式3:加载手机本地的html页面
// webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html")
}

// 注册监听
private fun registerScrollListener() {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// webView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
// Log.d(_tag, "scrollY: $scrollY, oldScrollY: $oldScrollY")
// }
// }
}

private val _tag = "WebViewActivity"

private fun showToast(info: String?) {
val toast = Toast.makeText(this, info, Toast.LENGTH_SHORT)
toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0)
toast.show()
}
}

0x01 白屏问题

情景一:如果访问的页面中要与Javascript交互,则webView必须设置支持Javascript

1
2
3
4
val webSettings = webView.settings
//如果访问的页面中要与Javascript交互,则webView必须设置支持Javascript
webSettings.javaScriptEnabled = true
webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口

情景二:在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置

1
2
3
4
// 在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}

其他情景: 设置domStorageEnabled 和背景色需要验证,暂时没遇到。

0x02 卡顿问题

由于设置了 android:layerType="software"导致的 webview 卡顿。

关于三个layerType属性介绍:https://blog.csdn.net/a345017062/article/details/7478667

解决:

开启 Activity 硬件加速 ``android:hardwareAccelerated=”true”, 并且设置 webview 的android:layerType=”none”`。

Gradle配置构建多Module项目

Gradle配置构建多Module项目

0x01 配置远程代码库

可以按如下方式声明特定的 Maven 或 Ivy 代码库:

1
2
3
4
5
6
7
8
allprojects {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' } // public仓是包含central仓和jcenter仓的聚合仓
maven { url 'https://maven.aliyun.com/repository/google' } // 阿里镜像库
maven { url "file://local/repo/" } // 本地文件代码库
ivy { url "https://repo.example.com/ivy" } // Ivy代码库
}
}

0x02 统一配置Gradle依赖库版本

随着项目采用模块化,组件化开发,moudle 的个数也会随着增加,统一管理配置gradle就显得比较重要了。

1、在 project 根目录创建一个 config.gradle 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ext {  
// app 相关版本控制
versions = [
compileVersion : 26,
buildVersion : "26.0.2",

sdkMinVersion : 15,
sdkTargetVersion : 26,
appVersionCode : 520,
appVersionName : "1.0.0"
]
// support依赖
support = [
appcompat : "com.android.support:appcompat-v7:26.+",
recyclerview: "com.android.support:recyclerview-v7:26.+"
]
// 依赖
deps = [
glide : "com.github.bumptech.glide:glide:4.11.0"
]
}

2、在 Project 根目录下的 build.gradle 添加apply

1
apply from: 'config.gradle' 

3、在相应Moudle中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
android {  
def versions = rootProject.ext.versions
compileSdkVersion versions.compileVersion
buildToolsVersion versions.buildVersion
defaultConfig {
applicationId "com.dench.wanandroid"
minSdkVersion versions.sdkMinVersion
targetSdkVersion versions.sdkTargetVersion
versionCode versions.appVersionCode
versionName versions.appVersionName
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
def dependencies = rootProject.ext.deps
def support = rootProject.ext.support

implementation support.appcompat
implementation support.recyclerview
implementation dependencies.glide
}

0x03 配置Flavor

创建产品变种与创建构建类型类似:将其添加到构建配置中的 productFlavors 代码块并添加所需的设置。产品变种支持与 defaultConfig 相同的属性,这是因为,defaultConfig 实际上属于 ProductFlavor 类。这意味着,您可以在 defaultConfig 代码块中提供所有变种的基本配置,每个变种均可更改其中任何默认值,如 applicationId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
android {
defaultConfig {...}
buildTypes {
debug{...}
release{...}
}
// Specifies one flavor dimension.
flavorDimensions "version"
productFlavors {
demo {
dimension "version"
applicationIdSuffix ".demo"
versionNameSuffix "-demo"
versionCode 30000 + android.defaultConfig.versionCode
}
full {
dimension "version"
applicationIdSuffix ".full"
versionNameSuffix "-full"
versionCode 20000 + android.defaultConfig.versionCode
}
}
}

0x04 创建源代码集

1、Gradle 要求:

在所有构建变体之间共享的所有内容创建 main/ 源代码集和目录。

将“debug”构建类型特有的 Java 类文件放在 src/debug/java/ 目录中。

1
2
3
4
5
6
7
8
9
10
11
12
13
debug
----
Compile configuration: compile
build.gradle name: android.sourceSets.debug
Java sources: [app/src/debug/java]
Manifest file: app/src/debug/AndroidManifest.xml
Android resources: [app/src/debug/res]
Assets: [app/src/debug/assets]
AIDL sources: [app/src/debug/aidl]
RenderScript sources: [app/src/debug/rs]
JNI sources: [app/src/debug/jni]
JNI libraries: [app/src/debug/jniLibs]
Java-style resources: [app/src/debug/resources]

依次转到 MyApplication > Tasks > android,然后双击 sourceSets。Gradle 执行该任务后,系统应该会打开 Run 窗口以显示输出。

2、更改默认源代码集配置

1
2
3
4
5
6
7
8
9
android {
sourceSets {
main {
java.srcDirs = ['other/java']
res.srcDirs = ['other/res1', 'other/res2']
manifest.srcFile 'other/AndroidManifest.xml'
}
}
}

0x05 声明依赖项

1
2
3
4
5
6
dependencies {
// Adds the local "mylibrary" module as a dependency to the "free" flavor.
freeImplementation project(":mylibrary")
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

0x06 配置签名

1、在项目的根目录下创建一个名为 keystore.properties 的文件,并使其包含以下信息:

1
2
3
4
storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=myKeyAlias
storeFile=myStoreFileLocation

2、在 build.gradle 文件中,按如下方式加载 keystore.properties 文件(必须在 android 代码块前面):

1
2
3
4
5
6
7
8

def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
// before android
android {

}

3、输入存储在 keystoreProperties 对象中的签名信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}

如需从环境变量获取这些密码,请添加以下代码:

1
2
storePassword System.getenv("KSTOREPWD")
keyPassword System.getenv("KEYPWD")

0x07 apk重命名

1
2
3
4
5
6
7
8
9
android {
applicationVariants.all { variant ->
if (variant.buildType.name == 'release') {
variant.outputs.all {
outputFileName = "app_v${variant.versionName}.${buildTime}_${variant.productFlavors[0].name}_${variant.buildType.name}.apk"
}
}
}
}

or

1
2
3
outputFileName = "app_v${versionName}.${buildTime}_${flavorName}_${buildType.name}.apk"
outputFileName = "../../${outputFileName}"
println outputFileName

0x08 将构建变量注入清单

1、如果您需要将变量插入在 build.gradle 文件中定义的 AndroidManifest.xml 文件,可以使用 manifestPlaceholders 属性执行此操作。此属性采用键值对的映射,如下所示:

1
2
3
4
5
6
android {
defaultConfig {
manifestPlaceholders = [hostName:"www.example.com"]
applicationId "com.example.myapp"
}
}

2、您可以将某个占位符作为属性值插入清单文件,如下所示:

1
2
3
4
<intent-filter ... >
<data android:scheme="http" android:host="${hostName}" ... />
<action android:name="${applicationId}.TRANSMOGRIFY" />
</intent-filter>

0x09 gradle自定义Java变量和资源值

在构建时,Gradle 将生成 BuildConfig 类,以便应用代码可以检查与当前构建有关的信息。您也可以从 Gradle 构建配置文件中使用 buildConfigField() 方法将自定义字段添加到 BuildConfig 类中,然后在应用的运行时代码中访问这些值。同样,您也可以使用 resValue() 添加应用资源值。

1
2
3
4
5
6
7
8
9
10
11
12
13
def buildTime = new Data().format("yyyyMMddHHmm", TimeZone.getTimeZone("GTM+08:00"))
android {
buildTypes {
release {
buildConfigField("String", "BUILD_TIME", "\"${buildTime}\"")
resValue("string", "build_time", "${buildTime}")
}
debug {
buildConfigField("String", "BUILD_TIME", "\"0\"")
resValue("string", "build_time", "0")
}
}
}

在应用代码中,您可以按如下方式访问属性:

1
2
Log.i(TAG, BuildConfig.BUILD_TIME);
Log.i(TAG, getString(R.string.build_time));

0x10 设定编码

1
2
3
4
5
allprojects {
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}
}

DataBinding踩坑指南

0x01 ViewBinding

1.使用 View Binding 先要在Module 的 build.gradle 文件注册

1
2
3
4
5
6
android {
...
buildFeatures {
viewBinding true
}
}

2.会根据布局文件,编译之后自动生成对应的Binding class,可以在Activity 和 Fragment 直接调用 inflate 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

0x02 DataBinding

1.在 build.gradle 文件中开启lib

1
2
3
4
5
6
android {
...
buildFeatures {
dataBinding true
}
}

2.布局文件start with a root tag of layout followed by a data element

1
2
3
4
5
6
7
8
9
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewmodel"
type="com.myapp.data.ViewModel" />
</data>
<ConstraintLayout... /> <!-- UI layout's root element -->
</layout>

3.在Activity中使用

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
// or
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)

binding.user = User("Test", "User")
}

Fragment, ListView, or RecyclerView adapter, you may prefer to use the inflate()

1
2
3
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

0x03 view binding 和 data binding 比较

View binding and data binding both generate binding classes that you can use to reference views directly. However, view binding is intended to handle simpler use cases and provides the following benefits over data binding:

  • Faster compilation: View binding requires no annotation processing, so compile times are faster.
  • Ease of use: View binding does not require specially-tagged XML layout files, so it is faster to adopt in your apps. Once you enable view binding in a module, it applies to all of that module’s layouts automatically.

Conversely, view binding has the following limitations compared to data binding:

Because of these considerations, it is best in some cases to use both view binding and data binding in a project. You can use data binding in layouts that require advanced features and use view binding in layouts that do not.

0x04 遇到的坑

  • 等标签,如果使用databinding ,子布局xml的 root tag 依旧需要layout 标签嵌套 data 标签。否者编译报错,找不到对应的属性

自定义Notification遇到的坑

自定义Notification 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// RemoteViews for notification
private var rv: RemoteViews? = null
private var rvExpanded: RemoteViews? = null
private fun customNotification(process: Int) {
// custom RemoteViews
if (rv == null) rv = RemoteViews(packageName, R.layout.notification_small)
rv?.setTextViewText(R.id.notification_title, "这是一个小标题")
if (rvExpanded == null) rvExpanded = RemoteViews(packageName, R.layout.notification_large)
rvExpanded?.setTextViewText(R.id.large_notification_title, "这是一个大标题,支持很多的内容: $process%")

// PendingIntent
val intentNotification = Intent(this, PlayMusicActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendIntent = PendingIntent.getActivity(
this,
0,
intentNotification,
PendingIntent.FLAG_UPDATE_CURRENT
)

// build notification
val customNotification =
NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.bravo) // small Icon
.setStyle(NotificationCompat.DecoratedCustomViewStyle()) // 自定义contentView
.setCustomContentView(rv!!)
.setContent(rvExpanded!!)
.setCustomBigContentView(rvExpanded!!)
.setCustomHeadsUpContentView(rvExpanded!!)
.setOngoing(true) // 一直显示
.setAutoCancel(false) // 点击后消失
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏显示,需要配合权限设置
.setPriority(NotificationCompat.PRIORITY_HIGH) // Priority
.setOnlyAlertOnce(true) // 声音,震动,仅弹出一次
.setContentIntent(pendIntent)
.build()
startForeground(NOTIFICATION_ID, customNotification)
}

1.使用 NotificationCompat 兼容各个版本差异性

2.RemoteViews 布局文件不支持 constraintlayout ,切记

3.在SDK 26之后必须要绑定Channel,所以通知要先创建Channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fun checkAndCreateChannel(
context: Context,
channelId: String,
channelName: String,
desc: String = channelName
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
try {
// 查找分组
val nc = notificationManager.getNotificationChannel(channelId)
Log.d("ChannelHelper", "${nc.id} Notification Channel exist.")
} catch (e: Exception) {
Log.d("ChannelHelper", "empty channel need create.")
// 创建分组
val mChannel =
NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = desc
enableLights(true)
enableVibration(true)
}
notificationManager.createNotificationChannel(mChannel)
}
}
}
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×