当前位置: 首页 > news >正文

Android Handler/Looper视角看UI线程的原理

概述

Handler/Looper机制是android系统非重要且基础的机制,即使在rtos或者linux操作系统上开发应用框架时,也经常借鉴这个机制。通过该机制机制可以让一个线程循环处理事件,事件处理逻辑即在Handler的handleMessge种。本文建议android8.1源码分析这套机制的实现原理。

Handler/Looper

Handler:顾名思义,处理消息message的类,Handler也可以发送消息。

Looper:顾名思义,一个循环体,这个循环体本质上就是不断取出Queue种的Message,分发给特定的目标,具体分发给哪个目标要具体情况具体分析。并且Looper内部含有一个Thread对象,很好理解,就是Looper这个循环体是在该线程中执行的。

MessageQueue:消息队列,通过Handler发送的Message加入到该队列中,并且被Looper运行在Thread线程中不断取出,又被Handler执行。

执行流程图:

应用程序使用Looper

应用程序使用Looper分为两种情况,主线程和普通线程:

普通线程
  * <p>This is a typical example of the implementation of a Looper thread,* using the separation of {@link #prepare} and {@link #loop} to create an* initial Handler to communicate with the Looper.**  class LooperThread extends Thread {*      public Handler mHandler;**      public void run() {*          Looper.prepare();**          mHandler = new Handler() {*              public void handleMessage(Message msg) {*                  // process incoming messages here*              }*          };**          Looper.loop();*      }*  }

这段代码在Android源码中非常典型,使用步骤有三:

  1. Looper.prepare创建线程私有的Looper对象。
  2. 创建处理消息的Handler。
  3. Looper.loop开始运作:线程一直循环取消息,处理消息,如果没有消息处理将休眠。

上面代码看似简单,越是简单的代码有时候越是难以理解,我们要理解上面三个步骤是怎么把Thread,MessageQueue,Handler和Looper联系起来的。

Looper.prepare源码
    private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();}

可以看到prepare就给调用prepare的当前线程创建一个Looper对象,Looper对象创建的时候,内部新建了MessageQueue和Thread对象,特别注意,mThread是调用prepare的线程

Handler对象创建
  **          mHandler = new Handler() {*              public void handleMessage(Message msg) {*                  // process incoming messages here*              }*          };

可以想象,这里Handler必须要跟前面prepare创建的Looper产生联系才对,这个就要看Handler的构造函数:

public Handler();
public Handler(Callback callback);
public Handler(Looper looper);
public Handler(Looper looper, Callback callback);

我们要看下上面Handler()构造函数:

    public Handler() {this(null, false);}public Handler(Callback callback, boolean async) {mLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");}mQueue = mLooper.mQueue;mCallback = callback;mAsynchronous = async;}

 可以看到默认构造函数中非常重要的一句,Looper.myLooper,会取得当前线程的Looper对象,也就是Looper.prepare创建的线程私有的对象,同时Handler's MessageQueue来自瑜当前线程Looper's MessageQueue。

注意:Thread和Handler是1:N关系。比如UI线程就可以有多个Handler对象。

Handler发送和处理消息

按照上面分析,调用mHandler可以发送消息,然后处理消息在该Handler的handleMessage函数,我们看看上述流程是如何实现的。

Looper从MessageQueue中取得一个Message后,首先会调用Handler.dispatchMessage进行消息分发,后者再根据具体的策略将Message分发给相应的责任人源码如下:

 Handler.java:

    /*** Subclasses must implement this to receive messages.*/public void handleMessage(Message msg) {}/*** Handle system messages here.*/public void dispatchMessage(Message msg) {                                                                                                             if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}private static void handleCallback(Message message) {                                                                                                  message.callback.run();}

分发策略:

1. Meesage.callback对象不为空优先分发给message.callback。这种情况对应mHandler.post接口发送的runable对象:这种情况下Message.callback就是post的runnable对象

public final boolean post(Runnable r);
public final boolean postAtTime(Runnable r, long uptimeMillis);public final boolean post(Runnable r)                                                                                                                  
{return  sendMessageDelayed(getPostMessage(r), 0);
}private static Message getPostMessage(Runnable r) {                                                                                                    Message m = Message.obtain();m.callback = r;return m;
}

2.  Handler.mCallback不为空,分发给mCallback.handleMessage

3.  前两个都不存在,则调用handleMessage,比如我们当前普通线程场景,override了这个方法,就会调用我们自己Handler.handleMessage。这种情况主要对应是Handler send系列接口使用:

public boolean sendMessageAtTime(Message msg, long uptimeMillis);
public final boolean sendMessageDelayed(Message msg, long delayMillis);比如如下使用:
Message msg = mHandler.obtainMessage();
mHandler.sendMessageAtTime(msg,xxxx);public final Message obtainMessage()                                                                                                                   
{return Message.obtain(this);
}public static Message obtain(Handler h) {Message m = obtain();m.target = h;return m;
}

 可以看到这种情况下Message.target = mHandler,而Looper在loop中分发代码:

     */public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}final MessageQueue queue = me.mQueue;// Make sure the identity of this thread is that of the local process,// and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();for (;;) {Message msg = queue.next(); // might block...final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();final long end;try {msg.target.dispatchMessage(msg);end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();} finally {if (traceTag != 0) {Trace.traceEnd(traceTag);}}...msg.recycleUnchecked();}}

 loop循环从Looper's MessageQueue取得消息,然后调用Message.target.dispatchMessage,正如上面的分析Message.target = mHandler,所以调用到了Handler.dispatchMessage方法。

总结:

1. Handler.post(runnable),在Handler.dispatchMessage方法由于优先分发给runnable对象,所以这个runnable对象被执行,而不是Handler.handleMessage。

2. 如果使用sendMessage接口,那么Handler.dispatchMessage分发给Handler override的handleMessage接口,也就是普通线程使用代码中Handler's handleMessage函数。

UI线程

 我们知道ActivityThread是主线程的入口点,我们看看android UI线程中Looper的使用源码:

    public static void main(String[] args) {                                                                                                               Looper.prepareMainLooper();ActivityThread thread = new ActivityThread();thread.attach(false);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}

与普通线程使用Looper类似,也分3个步骤:

1.准备Looper: Looper.prepareMainLooper

2.创建主线程私有的Handler:sMainThreadHandler = thread.getHandler

3.开始工作:Looper.loop

与普通线程的不同点:

1. 普通线程使用Looper.prepare,而主线程需要使用Looper.prepareMainLooper

2.Handler不同:普通线程生成一个与Looper绑定的Handler即可,而主线程是从当前线程总获取

我们分别分析下上面两个不同点

prepareMainLooper
    public static void prepareMainLooper() {                                                                                                               prepare(false);synchronized (Looper.class) {sMainLooper = myLooper();}}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}

可以看到,prepareMainLooper也使用了prepare,不过参数false代表ui线程不能退出,并且创建的Looper要赋值给sMainLooper,这样其他线程可以通过调用getMainLooper获取该对象。

MainThreadHandler

ActivityThread对象创建时候,会在内部同时生成一个Handler对象:

final H mH = new H();

thread.getHandler获取的就是该mH,也就是说ActivityThread中主线程处理消息的Handler之一就是该Handler mH。

loop

分析loop函数之前,我们提一下一个图形窗口典型的main函数如下:

main() {initialize()//初始化CreateWindow();//创建窗口ShowWindow();while(GetMessage()) {//不断分发处理DispatchMessage();}
}

那么根据前面loop函数的分析,其实UI线程的loop函数也是在循环中不断取消息,处理消息,符合上述模型。

ViewRootImpl

我们知道ViewRootImpl也属于UI线程的范围,因为ViewRootImpl创建就是在UI线程中创建,那么根据Handler创建的默认构造函数我们知道,这个Handler()这个默认的构造函数,适合当前线程的Looper绑定的,那么ViewRootImpl中Handler()形式构建的Handler也是处理UI线程的相关消息。

final ViewRootHandler mHandler = new ViewRootHandler();final class ViewRootHandler extends Handler {public void handleMessage(Message msg) {switch (msg.what) {case MSG_INVALIDATE:((View) msg.obj).invalidate();break;case MSG_INVALIDATE_RECT:final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;info.target.invalidate(info.left, info.top, info.right, info.bottom);info.recycle();break;case MSG_PROCESS_INPUT_EVENTS:mProcessInputEventsScheduled = false;doProcessInputEvents();break;case MSG_DISPATCH_APP_VISIBILITY:handleAppVisibility(msg.arg1 != 0);break;case MSG_DISPATCH_GET_NEW_SURFACE:handleGetNewSurface();break;....
Choreographer

Choreographer也是工作在UI主线程的一个重要的类,跟Vsync有关,我们通过源码看看这个类为什么工作在UI主线程。

看是否是主线程,我们要看下Choreographer中的Handler/Looper是基于哪个Thread来创建,看下Choreographer的构造函数如下:

private Choreographer(Looper looper, int vsyncSource) {mLooper = looper;mHandler = new FrameHandler(looper);                                                                                                               mDisplayEventReceiver = USE_VSYNC? new FrameDisplayEventReceiver(looper, vsyncSource): null;mLastFrameTimeNanos = Long.MIN_VALUE;mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];for (int i = 0; i <= CALLBACK_LAST; i++) {mCallbackQueues[i] = new CallbackQueue();}}

所以重点就是构建函数中Looper是哪里构建的,Choreographer是个单例类,所以看下其初始化的代码:

    // Thread local storage for the choreographer.private static final ThreadLocal<Choreographer> sThreadInstance =                                                                                      new ThreadLocal<Choreographer>() {@Overrideprotected Choreographer initialValue() {Looper looper = Looper.myLooper();if (looper == null) {throw new IllegalStateException("The current thread must have a looper!");}return new Choreographer(looper, VSYNC_SOURCE_APP);}};

也就是说最终Looper.myLooper获取的是当前线程私有的Looper对象,而Choreographer的getInstance是在public ViewRootImpl(Context context, Display display) 构造函数中调用的,根据ViewRootImpl小节我们知道,ViewRootImpl在主线程构建,所以Choreographer是主线程的

 Vsync信号处理

我们都知道vsync信号处理的实际逻辑是在ui主线程当中,我们看下具体实现,首先要看下vsync信号接受和处理的地方,这个在Choreographer当中:

    private final class FrameDisplayEventReceiver extends DisplayEventReceiverimplements Runnable {private boolean mHavePendingVsync;private long mTimestampNanos;private int mFrame;public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {super(looper, vsyncSource);}@Overridepublic void onVsync(long timestampNanos, int builtInDisplayId, int frame) {...mTimestampNanos = timestampNanos;mFrame = frame;Message msg = Message.obtain(mHandler, this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);}@Overridepublic void run() {mHavePendingVsync = false;doFrame(mTimestampNanos, mFrame);}

可以看到接收到vsync信号之后,使用mHandler向该Handler所在的线程发送消息,根据Choreographer小节我们知道,这里mHandler和其Looper是UI线程的,所以上面代码中发送的消息在主线程中处理,而FrameDisplayEventReceiver是个runnable对象,所以最终主线程收到这消息的处理函数是:doFrame函数:

            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);mFrameInfo.markInputHandlingStart();doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);mFrameInfo.markAnimationsStart();doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);mFrameInfo.markPerformTraversalsStart();doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

vsync信号过来在主线程处理, 最终处理函数是doFrame->doCallbacks,而doCallbacks就是要处理应用已经请求,且记录到queue里面的各种事件,比如应用请求渲染的时候调用invalidate调用栈:

invalidate() : ViewRootImpl.java--->scheduleTraversals() : ViewRootImpl.java--->mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

最终postCallback的实现:


private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) {synchronized (mLock) {final long now = SystemClock.uptimeMillis();final long dueTime = now + delayMillis;//将要执行的回调封装成CallbackRecord对象,保存到mCallbackQueues数组中mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);//函数执行时间到if (dueTime <= now) {scheduleFrameLocked(now);} else {//通过异步消息方式实现函数延时执行Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);msg.arg1 = callbackType;msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, dueTime);}}

1.将请求的事件保存在mCallbackQueue,等到vsync信号来到,doFrame中取出执行。

2. 发送MSG_DO_SCHEDULE_CALLBACK消息,最终调用native层的mDisplayEventReceiver.scheduleVsync();请求接收下一次vsync信号

相关文章:

Android Handler/Looper视角看UI线程的原理

概述 Handler/Looper机制是android系统非重要且基础的机制&#xff0c;即使在rtos或者linux操作系统上开发应用框架时&#xff0c;也经常借鉴这个机制。通过该机制机制可以让一个线程循环处理事件&#xff0c;事件处理逻辑即在Handler的handleMessge种。本文建议android8.1源码…...

【网络】网络入门

网络入门 一、网络发展二、网络协议初识1、认识"协议"2、协议分层3、OSI七层模型4、TCP/IP五层(或四层)模型 三、网络传输基本流程1、同局域网的两台主机通信2、跨网络的两台主机通信 四、网络中的地址管理1、IP地址2、认识MAC地址 一、网络发展 独立模式&#xff1a…...

GO-实现简单文本格式 文本字体颜色、大小、突出

毫无疑问GO的生态就是一坨大便。老子英文水平小学啊。 实现简单文本格式 文本字体颜色、大小、突出显示等。 创建要给docx文件容器【我估算的】 doc : document.New() defer doc.Close() doc.SaveToFile("simple.docx") 把容器保存为文件 设置标题 创建自然段…...

铅华洗尽,粉黛不施,人工智能AI基于ProPainter技术去除图片以及视频水印(Python3.10)

视频以及图片修复技术是一项具有挑战性的AI视觉任务&#xff0c;它涉及在视频或者图片序列中填补缺失或损坏的区域&#xff0c;同时保持空间和时间的连贯性。该技术在视频补全、对象移除、视频恢复等领域有广泛应用。近年来&#xff0c;两种突出的方案在视频修复中崭露头角&…...

latex,不带行号的algorithm

\usepackage{algorithm,algorithmic}\begin{algorithm} \caption{The Example Algorithm} \label{alg123} \begin{algorithmic} \STATE{\textbf{Input:} ...} \STATE{\textbf{Output:} ...} \IF{...} \STATE{...} \ENDIF \RETURN{...} \end{algorithmic} \end{algorithm}...

RocketMQ高性能核心原理与源码架构剖析

文章目录 一、源码环境搭建主要功能模块源码启动服务启动nameServer启动Broker发送消息消费消息 二、源码热身阶段NameServer的启动过程关注重点源码重点 Broker服务启动过程关注重点源码重点 Netty服务注册框架关注重点源码重点关于RocketMQ的同步结果推送与异步结果推送 Brok…...

MATLAB中zp2tf函数用法

目录 语法 说明 示例 质点弹簧系统的传递函数 zp2tf函数的功能是将零极点增益滤波器参数转换为传递函数形式。。 语法 [b,a] zp2tf(z,p,k) 说明 [b, a] zp2tf(z, p, k) 将一个分解的传递函数表示方式转换。 将单输入/多输出&#xff08;SIMO&#xff09;系统的多输出…...

解决:uniapp项目中调用小程序的chooseAddress() API失效

目录 问题描述 解决方案 问题描述 使用 Hbuilder X 编辑器和 uni-app 框架开发小程序项目&#xff0c;在调用小程序提供的 uni.chooseAddress() API实现选择收货地址的功能时&#xff0c;点击选择收货地址没有反应&#xff0c;获取不到用户收货地址&#xff0c;API失效了 …...

2023 项目组总结(待完善)

2023 项目组总结 目录概述需求&#xff1a; 设计思路实现思路分析1.JA项目2.XC项目3.XL 项目4.tydic 项目 总结 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better re…...

Chrome浏览器 键盘快捷键整理

名人说&#xff1a;莫听穿林打叶声&#xff0c;何妨吟啸且徐行。—— 苏轼《定风波莫听穿林打叶声》 本篇笔记整理&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 〇、前言一、常用快捷键二、分类型快捷键表&#xff08;…...

【JAVA】集合与背后的逻辑框架,包装类,List,Map,Set,静态内部类

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录 collectionCollection创建collection使用泛型collection方法 Map 接口Map的存储结构HashMap和Tr…...

mac电脑版数字图像处理软件:ACDSee Photo Studio 9最新 for Mac

ACDSee Photo Studio 9是一款由ACD Systems开发的功能强大的照片管理和编辑软件&#xff0c;专为Mac用户提供一站式解决方案&#xff0c;方便用户轻松浏览、管理和编辑照片。该软件提供了许多实用的工具和功能&#xff0c;包括高效的导入和排序工具、强大的编辑工具、智能组织和…...

酷开系统 | 酷开科技让你放肆嗨唱,聆听内心最真实的声音

在这个喧嚣的城市里&#xff0c;每个人都像是一座孤岛&#xff0c;漂浮在茫茫人海之中&#xff0c;我们总是忙于奔波在各种琐事之间&#xff0c;渐渐忘记了内心深处的声音&#xff0c;我们压抑自己的情感&#xff0c;害怕被误解、被批评&#xff0c;然而真正的我们&#xff0c;…...

PC电脑 VMware安装的linux CentOs7如何扩容磁盘?

一、VM中进行扩容设置 必须要关闭当前CentOS&#xff0c;不然扩展按钮是灰色的。 输入值必须大于当前磁盘容量。然后点击扩展&#xff0c;等待扩展完成会提示一个弹框&#xff0c;点击确定&#xff0c;继续确定。 二、操作CentOS扩容——磁盘分区 第一步设置完成。那就启动 …...

redis极速的奥秘

文章目录 1.基于内存存储实现2.高效的数据结构3.合理的数据编码4.合理的线程模型5. 虚拟内存机制实现原理 1.基于内存存储实现 内存读写是比在磁盘快很多的&#xff0c;Redis 基于内存存储实现的数据库&#xff0c;相对于数据存在磁盘的 MySQL 数据库&#xff0c;省去磁盘 I/O…...

three.js之初识three.js

什么是three.js Three.js是一款运行在浏览器中的 3D 引擎&#xff08;基于WebGL的API的封装&#xff09; 什么是WebGL&#xff1f; WebGL&#xff08;英语&#xff1a;Web Graphics Library&#xff09;是一种3D绘图协议&#xff0c;这种绘图技术标准允许把JavaScript和Open…...

二维码智慧门牌管理系统:地址管理的现代革命

文章目录 前言一、标准地址的革新二、广泛的应用前景 前言 在科技不断发展和社会进步的背景下&#xff0c;高效、精准、智能的管理系统已经成为当今社会的迫切需求。传统的门牌管理系统在应对这一需求方面已显得力不从心&#xff0c;因此&#xff0c;二维码智慧门牌管理系统的…...

BricsCAD 23 for Mac:轻松驾驭CAD建模的强大工具

如果你正在寻找一款功能强大、操作简便的CAD建模软件&#xff0c;那么BricsCAD 23 for Mac绝对值得你考虑。这款软件将为你提供一套完整的2D和3D设计解决方案&#xff0c;让你在Mac上轻松创建、编辑和修改图形。 一、BricsCAD 23的功能特点 高效的2D和3D建模&#xff1a;Bric…...

如何利用Web应用防火墙应对未知威胁

网络安全是一个永恒的话题&#xff0c;尤其是在未知威胁不断涌现的情况下。Web应用防火墙&#xff08;WAF&#xff09;是企业网络安全防线的重要组成部分&#xff0c;能够帮助企业在面对未知威胁时采取有效的防护措施。本文将探讨如何利用Web应用防火墙应对未知的网络威胁。 一…...

四、多线程服务器

1.进程的缺陷和线程的优点 1.进程的缺陷 创建进程&#xff08;复制&#xff09;的工作本身会给操作系统带来相当沉重的负担。 而且&#xff0c;每个进程具有独立的内存空间&#xff0c;所以进程间通信的实现难度也会随之提高。 同时&#xff0c;上下文切换&#xff08;Cont…...

基于vue实现滑块动画效果

主要实现&#xff1a;通过鼠标移移动、触摸元素、鼠标释放、离开元素事件来进行触发 创建了一个滑动盒子&#xff0c;其中包含一个滑块图片。通过鼠标按下或触摸开始事件&#xff0c;开始跟踪滑块的位置和鼠标/触摸位置之间的偏移量。然后&#xff0c;通过计算偏移量和起始时的…...

探寻蓝牙的未来:从蓝牙1.0到蓝牙5.4,如何引领无线连接革命?

►►►蓝牙名字的来源 这要源于一个小故事&#xff0c;公元940-985年&#xff0c;哈洛德布美塔特(Harald Blatand)&#xff0c;后人称Harald Bluetooth&#xff0c;统一了整个丹麦。他的名字“Blatand”可能取自两个古老的丹麦词语。“bla”意思是黑皮肤的&#xff0c;而“tan…...

openssl 之 RSA加密数据设置OAEP SHA256填充方式

背景 如题 环境 openssl 1.1.1l c centos7.9 代码 /** 思路&#xff1a;填充方式自己写&#xff0c;不需要使用库提供的&#xff0c;然后加密时选择不填充的方式加密 关键代码 */ int padding_result RSA_padding_add_PKCS1_OAEP_mgf1(buf, padding_len, (unsigned char*…...

js将带标签的内容转为纯文本

背景&#xff1a;现需要将富文本的所有 html 标签全部删除得到纯文本 思路&#xff1a;创建临时DOM元素并获取其中的文本 创建一个临时 DOM 并给他赋值&#xff0c;然后我们使用 DOM 对象方法提取文本。 代码如下&#xff1a; convertToPlain( html){//新创建一个 divvar di…...

如何通过内网穿透实现远程连接NAS群晖drive并挂载电脑硬盘?

文章目录 前言1.群晖Synology Drive套件的安装1.1 安装Synology Drive套件1.2 设置Synology Drive套件1.3 局域网内电脑测试和使用 2.使用cpolar远程访问内网Synology Drive2.1 Cpolar云端设置2.2 Cpolar本地设置2.3 测试和使用 3. 结语 前言 群晖作为专业的数据存储中心&…...

4.2 抽象类

1. 抽象类概念 定义一个类时&#xff0c;常常需要定义一些成员方法用于描述类的行为特征&#xff0c;但有时这些方法的实现方式是无法确定的。例如&#xff0c;Animal类中的shout()方法用于描述动物的叫声&#xff0c;但是不同的动物&#xff0c;叫声也不相同&#xff0c;因此…...

ITextRenderer将PDF转换为HTML详细教程

引入依赖 <dependency><groupId>org.xhtmlrenderer</groupId><artifactId>flying-saucer-pdf-itext5</artifactId><version>9.1.18</version></dependency> 问题一&#xff1a;输出中文字体 下载字体simsun.ttc 下载链接&am…...

c#设计模式-行为型模式 之 备忘录模式

&#x1f680;简介 备忘录模式&#xff08;Memento Pattern&#xff09;是一种行为型设计模式&#xff0c;它保存一个对象的某个状态&#xff0c;以便在适当的时候恢复对象。所谓备忘录模式就是在不破坏封装的前提下&#xff0c;捕获一个对象的内部状态&#xff0c;并在该对象…...

ffmpeg+安卓+yolo+RK3399部署

一次满足多项需求. 首先, 思路是, 使用ffmpeg解码本地mp4文件, 在无需任何其他改动的情况下, 就可以直接播放rtsp流, 这个是使用ffmpeg的好处. ffmpeg本身是c语言的, 所以需要编译成jni的库, https://note.youdao.com/s/6XeYftc 具体过程在这里, 用windows/macOS, Ubuntu应该都…...

发电机教程:小白必学的柴油发电机技巧

柴油发电机监控是关键的能源管理和维护工具&#xff0c;它用于确保持续的电力供应&#xff0c;提高能源效率&#xff0c;并延长发电机的寿命。 随着科技的不断发展&#xff0c;监控系统变得更加智能和高效&#xff0c;使用户能够远程监测和管理柴油发电机的运行状态。 客户案例…...

免费网站建设哪个好/看b站视频软件下载安装

2020/08/12每日二十个英语单词 tuning 调谐&#xff1b;调频tuning and control interface 调谐及控制接口tuning fork 音叉tuning, fine 微调&#xff1b;精调tunnel diode 隧道二极管tunnel effect 隧道效应tunneling 隧道技术tunneling, quantum 量子隧道turbine 涡轮turbo…...

郑州治疗精神病哪家好/河南seo推广

过几天小弟要去面试了&#xff0c;当然免不了要好好复习下功课&#xff0c;其实很多东西也不是特别清楚&#xff0c;今天都当作一个回顾和巩固&#xff0c;希望我的这篇文章能对即将去找工作的同学有所帮助。 1. Q&#xff1a;什么是activity&#xff1f; 虽然这个问题现在不流…...

上海松江做网站建设/如何建立个人网址

MemcacheDB是 一个开源项目&#xff0c;给memcached分布式缓存服务器添加了Berkeley DB的持久化存储机制和异步主辅复制机制&#xff0c;让memcached具备了事务恢复能力、持久化能力和分布式复制能力&#xff0c;非常适合于需要超高性能读写速度&#xff0c;但是 不需要严格事务…...

一个ip做几个网站/网站申请流程

背景 首先我是个菜鸡&#xff0c;工资也低的一笔。 刚毕业时候在一家国企上班干 app 开发&#xff0c;干了快两年的时候&#xff0c;跳槽到了一家伪大厂干安全。投了不少简历都没有回音&#xff0c;只有这加伪大厂要我就来了。当时说好了会接触一些底层的东西&#xff0c;然而…...

免费英文网站建设/网络营销与直播电商专升本

把数组排成最小的数(三十二) 题目描述 输入一个正整数数组&#xff0c;把数组里所有数字拼接起来排成一个数&#xff0c;打印能拼接出的所有数字中最小的一个。例如输入数组{3&#xff0c;32&#xff0c;321}&#xff0c;则打印出这三个数字能排成的最小数字为321323。 代码…...

政府网站建设的问题/全网最好的推广平台

卷积计算和池化计算公式 卷积 卷积计算中&#xff0c;&#xff08;&#xff09;表示向下取整。   输入&#xff1a;n* c0* w0* h0   输出&#xff1a;n* c1* w1* h1   其中&#xff0c;c1就是参数中的num_output&#xff0c;生成的特征图个数。    w1(w02pad-kernel_s…...