热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入理解AsyncChannel、Messenger

这篇文章主要介绍AndroidAsyncChannel、Messenger原理及其应用实例注:文章参考的是Andrdoid8.0源码AsyncChannel简介

这篇文章主要介绍Android AsyncChannel、Messenger原理及其应用实例
注:文章参考的是Andrdoid 8.0源码


AsyncChannel 简介
  
  AsyncChannel的源码位于 frameworks/base/core/java/com/android/internal/util/AsyncChannel.java,是对Handler和Messenger的一个扩展,用于单个进程内(包含不同线程间)或两个进程间的通信。在不同的进程间,AsyncChanne实质上使用的是IMessenger通过Binder的形式对消息进行发送和接收,当然,这种方式对单个进程内非remove service同样适用。
  在Android系统中,有很多地方都使用了这个工具类,如ConnectivityService与NetworkAgent的通信,WifiServiceImpl与WifiStateMachine之间的通信等。由于AsyncChannel属于系统内部源码,三方应用无法直接进行使用,但我们可以学习思想,甚至可以自己简单实现并作用于我们的APP代码中。

AsyncChannel的主要特点:


  • 可以在单进程或不同进程间实现消息传递
  • 支持建立单向通信或双向通信
  • 是对Handler,Messenger的一种包装,并没有实现额外的通信方式


Messenger

  由于AsyncChannel实际上使用的是Handler和Messenger的机制,考虑到很多开发者对Messenger还不够了解,所以我们这里有必要先对这个工具做一个详细的解析。
  Messenger的源码位于 frameworks/base/core/java/android/os/Messenger.java,是对Handler的一个再包装,并结合了Binder机制,使得跨进程间Message的传递和处理成为了可能。

成员变量及构造函数:

  主要成员变量是IMessenger类型的mTarget,该变量可以通过Handler或者IBinder进行初始化,分别应用于同进程消息或者跨Service(remote或者非remote)消息发送:

// IMessenger类型,mTarget代表message的目的端
private final IMessenger mTarget;// 初始化mTarget并指向Handler中的IMessenger
public Messenger(Handler target) {mTarget = target.getIMessenger();
}// 初始化mTarget并指向IBinder的实际类型
public Messenger(IBinder target) {mTarget = IMessenger.Stub.asInterface(target);
}

Messenger的发送函数:

  无论是否跨进程,Messenger实际上都是利用IMessenger进行消息发送的:

public void send(Message message) throws RemoteException {mTarget.send(message);
}

IMessenger:

  源码位于 frameworks/base/core/java/android/os/IMessenger.aidl,其目的是通过AIDL使用Binder机制对Message进行发送:

/** @hide */
oneway interface IMessenger {void send(in Message msg);
}

通过Handler对mTarget进行初始化:

  Handler中MessengerImpl对IMessenger进行了实现,在 send 函数中最终还是使用Handler自身对Message进行了处理,因此,这里的IMessenger仅仅是一个中介:

final IMessenger getIMessenger() {synchronized (mQueue) {if (mMessenger != null) {return mMessenger;}mMessenger = new MessengerImpl();return mMessenger;}
}private final class MessengerImpl extends IMessenger.Stub {public void send(Message msg) {// 由于存在Binder调用,初始化sendingUid为发送端uidmsg.sendingUid = Binder.getCallingUid();// 通过Handler自身发送消息并进行处理Handler.this.sendMessage(msg);}
}

通过Service IBinder对mTarget进行初始化:
  
  这里以Android源码中针对Messenger的测试case作为例子对其过程进行解析,源码路径:
- frameworks/base/core/tests/coretests/src/android/os/MessengerService.java
- frameworks/base/core/tests/coretests/src/android/os/MessengerTest.java

Service端实现(MessengerService.java):

public class MessengerService extends Service {private final Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// 在这里处理client端发送的消息}};// 定义一个Messenger,Messenger中的mTarget即为mHandler中的mMessengerprivate final Messenger mMessenger = new Messenger(mHandler);@Overridepublic IBinder onBind(Intent intent) {// 返回mHandler中的mMessengerreturn mMessenger.getBinder();}
}

Client端实现(MessengerTest.java):

  通过 bindService 与MessengerService建立连接,连接完成后,对Messenger进行初始化:

private ServiceConnection mConnection = new ServiceConnection() {public void onServiceConnected(ComponentName name, IBinder service) {synchronized (MessengerTest.this) {// 通过IBinder类型的service对Messenger中的mTarget进行初始化mServiceMessenger = new Messenger(service);MessengerTest.this.notifyAll();}}public void onServiceDisconnected(ComponentName name) {// service断开后,对应的messenger也不会再有作用mServiceMessenger = null;}
};

  因此,这样初始化后,下面的几个变量就可以看做是同一个对象的引用,即:
  MessengerTest.mServiceMessenger.mTarget
    = MessengerService.mMessenger.mTarget
    = MessengerService.mHandler.mMessenger

Messenger小结:

  实质上是通过IMessenger利用Binder机制进行消息的发送。Handler中实现了IMessenger中的 send 函数,并在其中调用Handler自身的 sendMessage 函数进行消息处理;Service中可以定义自己的Handler和对应的Messenger,并在onBind时返回Handler中MessengerImpl的实例,Client端在 onServiceConnected 函数中通过IBinder对象对Client中Messenger进行初始化,之后,Client端就可以通过该Messenger将消息发送到Service中的Handler进行处理了。Client和Service间通过Messenger进行消息传递的大致流程如下图:
通过Messenger与Service进行消息传递



AsyncChannel成员常量和变量

注明:从分析AsyncChannel开始,我们称连接的首次发起端为"source端",被连接端为"destination端"

连接指令相关静态常量:

/** AsyncChannel消息码在系统中的唯一开始标志 */
private static final int BASE = Protocol.BASE_SYSTEM_ASYNC_CHANNEL;/** 单向连接建立后,AsyncChannel通知source端半连接已建立,可以进行单向通信,但此时destination端是完全没有感知的 */
public static final int CMD_CHANNEL_HALF_CONNECTED = BASE + 0;/** source端在半连接建立后,发送该消息码给destination端,请求建立双向连接 */
public static final int CMD_CHANNEL_FULL_CONNECTION = BASE + 1;/** destination端在建立双向连接后,发送该消息码给source端,告知双向连接建立完成 */
public static final int CMD_CHANNEL_FULLY_CONNECTED = BASE + 2;/** source或者destination主动请求断开 */
public static final int CMD_CHANNEL_DISCONNECT = BASE + 3;/** 在一端主动断开后,对端会收到该消息码 */
public static final int CMD_CHANNEL_DISCONNECTED = BASE + 4;

连接和消息状态相关静态常量:

/** 连接状态相关:成功半连接,成功连接,成功断开 */
public static final int STATUS_SUCCESSFUL = 0;/** 跨service连接,bindService成功或者失败 */
public static final int STATUS_BINDING_UNSUCCESSFUL = 1;/** 消息发送失败 */
public static final int STATUS_SEND_UNSUCCESSFUL = 2;/** 已建立双向连接,拒绝再次连接 */
public static final int STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED = 3;/** 跨service连接,对端service死亡,binderDied返回后发送该状态码 */
public static final int STATUS_REMOTE_DISCONNECTION = 4;

主要成员变量:

/** ServiceConnection,用于和service或remote service建立连接 */
private AsyncChannelConnection mConnection;/** 连接建立端Context,仅用于bind/unbind service时使用 */
private Context mSrcContext;/** binderDied回调监听,仅用于与service连接后使用 */
private DeathMonitor mDeathMonitor;/** 连接建立端Handler */
private Handler mSrcHandler;/** 连接建立端Messenger */
private Messenger mSrcMessenger;/** 连接对端Messenger */
private Messenger mDstMessenger;


AsyncChannel连接过程 - 半连接(Half Connect)

Source端发起连接请求:

/** 发起连接 */
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {// source端AsyncChannel变量初始化connected(srcContext, srcHandler, dstMessenger);// 发送"CMD_CHANNEL_HALF_CONNECTED"告知source端半连接已建立replyHalfConnected(STATUS_SUCCESSFUL);
}

/** 初始化source端的成员变量,创建source端Messenger */
public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {// Initialize source fieldsmSrcContext = srcContext;mSrcHandler = srcHandler;mSrcMessenger = new Messenger(mSrcHandler);// Initialize destination fieldsmDstMessenger = dstMessenger; linkToDeathMonitor();
}

/** 发送"CMD_CHANNEL_HALF_CONNECTED"告知source端半连接已建立 */
private void replyHalfConnected(int status) {Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);msg.arg1 = status;msg.obj = this;msg.replyTo = mDstMessenger;if (!linkToDeathMonitor()) {// Override status to indicate failuremsg.arg1 = STATUS_BINDING_UNSUCCESSFUL;}mSrcHandler.sendMessage(msg);
}

半连接小结:

  整个半连接过程由连接发起端(source端)完成,destination端没有任何消息告知。source端调用AsyncChannel的 connect 函数后,开始对AsyncChannel中变量的初始化,主要是对mDstMessenger的初始化。完成后source端Handler会收到 “CMD_CHANNEL_HALF_CONNECTED” 的消息告知半连接已经完成,之后,source端就可以通过该AsyncChannel与destination进行通信。这种通信方式是单向的,只能由source端主动向destination推送消息并获取其回复,destination无法主动向source推送消息。这个连接过程可以由下面的时序图来表示:
AsyncChannel half connect

AsyncChannel连接过程 - 全连接(Full Connect)

AsyncChannel的全连接建立主要有以下两种方式:


  • 在半连接的基础上由source端发送 “CMD_CHANNEL_FULL_CONNECTION” 请求全连接
  • source端直接调用AsyncChannel的 fullyConnectSync 进行全连接

下面对这两种全连接建立方式分别作解析。

方式一(在半连接的基础上由source端发送 “CMD_CHANNEL_FULL_CONNECTION” 请求全连接):

  这个过程主要是由AsyncChannel使用者自己完成,我们以ConnectivityService与NetworkAgent之间的通信进行举例,例子的关键源码路径如下:
- frameworks/base/services/core/java/com/android/server/ConnectivityService.java
- frameworks/base/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
- frameworks/base/core/java/android/net/NetworkAgent.java

  首先,ConnectivityService作为连接主动端主动请求半连接:

/** ConnectivityService#handleRegisterNetworkAgent */
private void handleRegisterNetworkAgent(NetworkAgentInfo na) {// ...// 通过NetworkAgentInfo中新创建的AsyncChannel主动请求连接// mTrackerHandler是ConnectivityService中的Handler,作为source handler// na.messenger是连接的对端,这里就是指 NetworkAgentna.asyncChannel.connect(mContext, mTrackerHandler, na.messenger);// ..
}

  通过对半连接的了解,在 connect 之后,source handler就会收到 “CMD_CHANNEL_HALF_CONNECTED”。由于需要建立全连接,ConnectivityService在收到该消息后,立刻向半连接对端请求全连接:

/** ConnectivityService#NetworkStateTrackerHandler */
private class NetworkStateTrackerHandler extends Handler {// ...private boolean maybeHandleAsyncChannelMessage(Message msg) {switch (msg.what) {case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: {// 收到 "CMD_CHANNEL_HALF_CONNECTED",在下面的函数中处理handleAsyncChannelHalfConnect(msg);break;}// ...
}/** ConnectivityService#handleAsyncChannelHalfConnect */
private void handleAsyncChannelHalfConnect(Message msg) {// ...else if (mNetworkAgentInfos.containsKey(msg.replyTo)) {if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {if (VDBG) log("NetworkAgent connected");// 发送全连接请求 "CMD_CHANNEL_FULL_CONNECTION"mNetworkAgentInfos.get(msg.replyTo).asyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);}
}

  NetworkAgent在收到 “CMD_CHANNEL_FULL_CONNECTION” 请求后,便开始创建自己的AsyncChannel,并完成其初始化,连接的对端是ConnectivityService:

/** NetworkAgent#handleMessage */
public void handleMessage(Message msg) {switch (msg.what) {// 收到由ConnectivityService发来的全连接请求case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {if (mAsyncChannel != null) {log("Received new connection while already connected!");} else {if (VDBG) log("NetworkAgent fully connected");// 创建自己的AsyncChannelAsyncChannel ac = new AsyncChannel();// msg.replayTo 就是NetworkAgentInfo.asyncChannel中的mSrcMessenger,// 消息的处理者就是ConnectivityService.mTrackerHandlerac.connected(null, this, msg.replyTo);// 全连接已经建立,发送 "CMD_CHANNEL_FULLY_CONNECTED" 给ConnectivityServiceac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,AsyncChannel.STATUS_SUCCESSFUL);// ...}break;// ...
}

  从上述的过程可以看出,建立全连接的方式就是连接的两端都需要创建自己的AsyncChannel,并且每个AsyncChannel中都保存着对端的Messenger。这种全连接的方式可以通过下面流程图进行表示:
AsyncChannel full connect
方式二:source端直接调用AsyncChannel的 fullyConnectSync 进行全连接

  从 fullyConnectSync 这个函数名,可以知道这是一个同步的操作,AsyncChannel中使用了SyncMessenger这个静态内部类来实现这个同步连接的操作。直接从 fullyConnectSync 这个函数开始分析。

  fullyConnectSync 中对source端AsyncChannel进行了初始化,并发送 “CMD_CHANNEL_FULL_CONNECTION” 同步消息请求全连接:

/** AsyncChannel#fullyConnectSync*/
public int fullyConnectSync(Context srcContext, Handler srcHandler, Handler dstHandler) {// 只是调用了connected函数,对source端AsyncChannel成员变量进行初始化int status = connectSync(srcContext, srcHandler, dstHandler);if (status == STATUS_SUCCESSFUL) {// 发送"CMD_CHANNEL_FULL_CONNECTION"同步消息给destination// 同步消息,需要等待回应后才继续Message response = sendMessageSynchronously(CMD_CHANNEL_FULL_CONNECTION);status = response.arg1;}return status;
}

  sendMessageSynchronously 实际使用的是 “SyncMessenger.sendMessageSynchronously”:

/** AsyncChannel.SyncMessenger#sendMessageSynchronously*/
private static Message sendMessageSynchronously(Messenger dstMessenger, Message msg) {// 获取一个SyncMessenger对象,启动消息接收者SyncHandlerSyncMessenger sm = SyncMessenger.obtain();try {if (dstMessenger != null && msg != null) {msg.replyTo = sm.mMessenger;// 获取对象锁:"sm.mHandler.mLockObject"synchronized (sm.mHandler.mLockObject) {// 发送 "CMD_CHANNEL_FULL_CONNECTION"dstMessenger.send(msg);// 释放该锁并中断等待notifysm.mHandler.mLockObject.wait();}} else {sm.mHandler.mResultMsg = null;}} catch (InterruptedException e) {sm.mHandler.mResultMsg = null;} catch (RemoteException e) {sm.mHandler.mResultMsg = null;}// 被CPU唤醒,消息回复已经收到,返回该回复Message resultMsg = sm.mHandler.mResultMsg;sm.recycle();return resultMsg;
}

  通过SyncMessenger向对端发送完消息后,对端如果已经完成了 connect 的操作或者拒绝连接,都应该回复消息。由于发送消息前设定了msg.replyTo = sm.mMessenger,因此回复的消息会被SyncManager中的SyncHandler处理:

/** AsyncChannel.SyncManager#SyncHandler#handleMessage*/
public void handleMessage(Message msg) {mResultMsg = Message.obtain();mResultMsg.copyFrom(msg);// 获取mLockObject对象锁synchronized(mLockObject) {// 唤醒在mLockObject这个锁上等待的线程mLockObject.notify();}
}

  从上面的流程可以看出,通过 fullyConnectSync 建立全连接省略了中间 “CMD_CHANNEL_HALF_CONNECTED” 消息回复过程,并且通过SyncMessenger,实现了在对端完成connect操作后才返回。因此,这个过程不需要AsyncChannel的主动连接者进行其他操作就可以完成全连接。然而,AsyncChannel的对端依然需要处理 “CMD_CHANNEL_FULL_CONNECTION”。整个过程的大致流程图如下:
fullyConnectSync
AsyncChannel的跨进程消息传递

  由于AsyncChannel内部使用的是Messenger,且Handler和Messenger都已经支持了Binder通信方式,因此,AsyncChannel同样可以利用在跨进程通信上。
  前文在介绍Messenger时,通过例子”MessengerService”对Messenger做了解析,AsyncChannel在跨进程通信上与前文例子相似:在连接前,需要先bindService,在 onServiceConnected 时,拿到Service的Messenger并完成了对mDstMessenger的初始化,从而实现了半连接。之后,全连接的方式就与前文介绍的相同。

/** AsyncChannel#connect*/
public void connect(Context srcContext, Handler srcHandler, String dstPackageName,String dstClassName) {// 由于有bindService操作,并且需要等待onServiceConnected回调,// 因此实现了一个Runnable并且新开一个线程去完成这个操作final class ConnectAsync implements Runnable {// ...@Overridepublic void run() {// 完成对AsyncChannel中除mDstMessenger的成员变量的初始化// 并执行bindService操作int result = connectSrcHandlerToPackageSync(mSrcCtx, mSrcHdlr, mDstPackageName,mDstClassName);replyHalfConnected(result);}}ConnectAsync ca = new ConnectAsync(srcContext, srcHandler, dstPackageName, dstClassName);// 新开一个线程执行异步连接操作new Thread(ca).start();
}

  connectSrcHandlerToPackageSync 完成了对AsyncChannel中基本成员变量的初始化,与destination相关的mDstMessenger变量需要等待 onServiceConnected 回调后再进行初始化:

/** AsyncChannel#connectSrcHandlerToPackageSync */
public int connectSrcHandlerToPackageSync(Context srcContext, Handler srcHandler, String dstPackageName, String dstClassName) {mConnection = new AsyncChannelConnection();/* 初始化AsyncChannel source相关的变量 */mSrcContext = srcContext;mSrcHandler = srcHandler;mSrcMessenger = new Messenger(srcHandler);// 对端Messenger等待onServiceConnected后再进行初始化mDstMessenger = null;// bindServiceIntent intent = new Intent(Intent.ACTION_MAIN);intent.setClassName(dstPackageName, dstClassName);boolean result = srcContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);return result ? STATUS_SUCCESSFUL : STATUS_BINDING_UNSUCCESSFUL;
}

  AsyncChannelConnection#onServiceConnected 会在Service bind成功后被回调,在这个函数中,完成了对 mDstMessenger 的初始化,到此,半连接完成:

/** AsyncChannel#AsyncChannelConnection */
class AsyncChannelConnection implements ServiceConnection {AsyncChannelConnection() {}@Overridepublic void onServiceConnected(ComponentName className, IBinder service) {// service bind成功,初始化mDstMessengermDstMessenger = new Messenger(service);replyHalfConnected(STATUS_SUCCESSFUL);}// ...
}

  跨进程或同进程Service的AsyncChannel连接与普通的相同,只不过多了一步bindService的操作,并且mDstMessenger 需要等待 onServiceConnected 回调后才能完成初始化。

总结

  AsyncChannel 结合Messenger,实现了两个进程或线程间Handler消息的传递。这篇文章主要介绍了Messenger的原理、AsyncChannel的两种连接方式:半连接和全连接、AsyncChannel跨进程连接等,虽然这个工具类无法直接使用,但如果应用中有需要,仍然可以借鉴其原理作适合的简单实现。


推荐阅读
  • 本文详细解析了Java类加载系统的父子委托机制。在Java程序中,.java源代码文件编译后会生成对应的.class字节码文件,这些字节码文件需要通过类加载器(ClassLoader)进行加载。ClassLoader采用双亲委派模型,确保类的加载过程既高效又安全,避免了类的重复加载和潜在的安全风险。该机制在Java虚拟机中扮演着至关重要的角色,确保了类加载的一致性和可靠性。 ... [详细]
  • 在 Android 开发中,`android:exported` 属性用于控制组件(如 Activity、Service、BroadcastReceiver 和 ContentProvider)是否可以被其他应用组件访问或与其交互。若将此属性设为 `true`,则允许外部应用调用或与之交互;反之,若设为 `false`,则仅限于同一应用内的组件进行访问。这一属性对于确保应用的安全性和隐私保护至关重要。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • 在本文中,我们将探讨如何在Docker环境中高效地管理和利用数据库。首先,需要安装Docker Desktop以确保本地环境准备就绪。接下来,可以从Docker Hub中选择合适的数据库镜像,并通过简单的命令将其拉取到本地。此外,我们还将介绍如何配置和优化这些数据库容器,以实现最佳性能和安全性。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • 本文详细介绍了在 Android 7.1 系统中调整屏幕分辨率和默认音量设置的方法。针对系统默认音量过大的问题,提供了具体的步骤来降低系统、铃声、媒体和闹钟的默认音量,以提升用户体验。此外,还涵盖了如何通过系统设置或使用第三方工具来优化屏幕分辨率,确保设备显示效果更加清晰和流畅。 ... [详细]
  • 在Java Web服务开发中,Apache CXF 和 Axis2 是两个广泛使用的框架。CXF 由于其与 Spring 框架的无缝集成能力,以及更简便的部署方式,成为了许多开发者的首选。本文将详细介绍如何使用 CXF 框架进行 Web 服务的开发,包括环境搭建、服务发布和客户端调用等关键步骤,为开发者提供一个全面的实践指南。 ... [详细]
  • 深入理解排序算法:集合 1(编程语言中的高效排序工具) ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 本文详细探讨了使用纯JavaScript开发经典贪吃蛇游戏的技术细节和实现方法。通过具体的代码示例,深入解析了游戏逻辑、动画效果及用户交互的实现过程,为开发者提供了宝贵的参考和实践经验。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 本文深入解析了Java面向对象编程的核心概念及其应用,重点探讨了面向对象的三大特性:封装、继承和多态。封装确保了数据的安全性和代码的可维护性;继承支持代码的重用和扩展;多态则增强了程序的灵活性和可扩展性。通过具体示例,文章详细阐述了这些特性在实际开发中的应用和优势。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有