这篇文章接着上一篇SystemUI之启动未分析完的SystemUI插件化机制相关的代码

SystemUI插件

SystemUI插件提供了一种快速创建SystemUI功能原型的简便方法,可以在运行时更改SystemUI的行为。 通过创建插件实现SysUI中使用的一组基本接口来完成,然后可以比当前更快的速度迭代由该接口控制的部分代码。

简单来说就是可以快速替换SystemUI原有的组件,也叫hook,我们就来分析下其中的原理

SystemUIApplication.startServicesIfNeeded

private void startServicesIfNeeded(String[] services) {//省略上一篇文章分析过的代码.....Dependency.get(InitController.class).executePostInitTasks();log.traceEnd();final Handler mainHandler = new Handler(Looper.getMainLooper());//调用addPluginListener传入了三个参数,PluginListener,//OverlayPlugin.class,和一个boolean值trueDependency.get(PluginManager.class).addPluginListener(new PluginListener<OverlayPlugin>() {private ArraySet<OverlayPlugin> mOverlays = new ArraySet<>();@Overridepublic void onPluginConnected(OverlayPlugin plugin, Context pluginContext) {mainHandler.post(new Runnable() {@Overridepublic void run() {StatusBar statusBar = getComponent(StatusBar.class);if (statusBar != null) {plugin.setup(statusBar.getStatusBarWindow(),statusBar.getNavigationBarView(), new Callback(plugin));}}});}@Overridepublic void onPluginDisconnected(OverlayPlugin plugin) {mainHandler.post(new Runnable() {@Overridepublic void run() {mOverlays.remove(plugin);Dependency.get(StatusBarWindowController.class).setForcePluginOpen(mOverlays.size() != 0);}});}class Callback implements OverlayPlugin.Callback {private final OverlayPlugin mPlugin;Callback(OverlayPlugin plugin) {mPlugin = plugin;}@Overridepublic void onHoldStatusBarOpenChange() {if (mPlugin.holdStatusBarOpen()) {mOverlays.add(mPlugin);} else {mOverlays.remove(mPlugin);}mainHandler.post(new Runnable() {@Overridepublic void run() {Dependency.get(StatusBarWindowController.class).setStateListener(b -> mOverlays.forEach(o -> o.setCollapseDesired(b)));Dependency.get(StatusBarWindowController.class).setForcePluginOpen(mOverlays.size() != 0);}});}}}, OverlayPlugin.class, true /* Allow multiple plugins */);mServicesStarted = true;}

Dependency是定义在config_systemUIServiceComponents这个config文件中的,在startServicesIfNeeded中会遍历此config定义的所有SystemUI重要类,调用它们的Start方法:

Dependency.start

@Overridepublic void start() {// TODO: Think about ways to push these creation rules out of Dependency to cut down// on imports.mProviders.put(TIME_TICK_HANDLER, mTimeTickHandler::get);mProviders.put(BG_LOOPER, mBgLooper::get);mProviders.put(BG_HANDLER, mBgHandler::get);mProviders.put(MAIN_HANDLER, mMainHandler::get);mProviders.put(ActivityStarter.class, mActivityStarter::get);mProviders.put(ActivityStarterDelegate.class, mActivityStarterDelegate::get);mProviders.put(AsyncSensorManager.class, mAsyncSensorManager::get);mProviders.put(BluetoothController.class, mBluetoothController::get);mProviders.put(SensorPrivacyManager.class, mSensorPrivacyManager::get);mProviders.put(LocationController.class, mLocationController::get);mProviders.put(RotationLockController.class, mRotationLockController::get);mProviders.put(NetworkController.class, mNetworkController::get);mProviders.put(ZenModeController.class, mZenModeController::get);mProviders.put(HotspotController.class, mHotspotController::get);mProviders.put(CastController.class, mCastController::get);mProviders.put(FlashlightController.class, mFlashlightController::get);mProviders.put(KeyguardMonitor.class, mKeyguardMonitor::get);mProviders.put(UserSwitcherController.class, mUserSwitcherController::get);mProviders.put(UserInfoController.class, mUserInfoController::get);mProviders.put(BatteryController.class, mBatteryController::get);......sDependency = this;}

我们可以看到,start方法中将很多的类添加到了mProviders中,接着看:

Dependency.get

private static Dependency sDependency;
@Deprecatedpublic static <T> T get(Class<T> cls) {return sDependency.getDependency(cls);}protected final <T> T getDependency(Class<T> cls) {return getDependencyInner(cls);}private synchronized <T> T getDependencyInner(Object key) {@SuppressWarnings("unchecked")T obj = (T) mDependencies.get(key);if (obj == null) {obj = createDependency(key);mDependencies.put(key, obj);}return obj;}

get方法最终通过mDependencies.get来获取对象,mDependencies是个ArrayMap,如果没有获取到就调用createDependency创建对象,创建之后放入mDependencies中

createDependency

@VisibleForTestingprotected <T> T createDependency(Object cls) {@SuppressWarnings("unchecked")LazyDependencyCreator<T> provider = mProviders.get(cls);if (provider == null) {throw new IllegalArgumentException("Unsupported dependency " + cls+ ". " + mProviders.size() + " providers known.");}return provider.createDependency();}

此方法通过mProviders.get来获取对象,mProviders中的类就是在Start方法中添加的,所以能够直接获取,Dependency中用到了dagger框架的Lazy,Provider等机制。

addPluginListener

所以我们在看到Dependency.get(T.class)时就可以简单认为是获取T的实现类
接着SystemUIApplication的startServicesIfNeeded方法添加插件监听,这里实际调用PluginManagerImpl的addPluginListener方法,此方法接收三个参数:一个PluginListener接口,一个Class对象,一个boolean值

PluginManagerImpl中的addPluginListener有几个重载,为了好说明,我给它们编了个号

 (1)public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {addPluginListener(listener, cls, false);}(2)public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,boolean allowMultiple) {addPluginListener(PluginManager.Helper.getAction(cls), listener, cls, allowMultiple);}(3)public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,Class<?> cls) {addPluginListener(action, listener, cls, false);}(4)public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,Class cls, boolean allowMultiple) {android.util.Log.d("djtang","addPluginListener...action = :"+action+",name = :"+cls.getName());mPluginPrefs.addAction(action);PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,allowMultiple, mLooper, cls, this);p.loadAll();mPluginMap.put(listener, p);startListening();}

PluginListener

我们先来看下PluginListener这个接口,这是个泛型接口,其中T必须是Plugin的子类,此接口提供两个生命周期方法,插件连接时调用onPluginConnected,断开连接调用
onPluginDisconnected

public interface PluginListener<T extends Plugin> {void onPluginConnected(T plugin, Context pluginContext);default void onPluginDisconnected(T plugin) {}
}

从上面代码我们知道,Dependency.get(PluginManager.class).addPluginListener调用的是(2)号重载方法,获取了一个Action后调用了(4)号方法,看下Action是啥:

PluginManager.Helper.getAction(cls)

public interface PluginManager {class Helper {public static <P> String getAction(Class<P> cls) {//获取ProvidesInterface的注解信息ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);if (info == null) {throw new RuntimeException(cls + " doesn't provide an interface");}if (TextUtils.isEmpty(info.action())) {throw new RuntimeException(cls + " doesn't provide an action");}//获取ProvidesInterface注解的actionreturn info.action();}
}

通过getDeclaredAnnotation方法获取注解,ProvidesInterface这是个注解类,每个继承Plugin的类包含一个ProvidesInterface,这个注解类包含两项,version和action

public @interface ProvidesInterface {int version();String action() default "";}

用法如下:

@ProvidesInterface(action = MyPlugin.ACTION, version = MyPlugin.VERSION)
public interface MyPlugin extends Plugin {String ACTION = "com.android.systemui.action.PLUGIN_MY_PLUGIN";int VERSION = 1;...
}

这样就通过ProvidesInterface注解将action和version暴露了出来,获取的方式很简单,调用ProvidesInterface的action方法获取的就是其action

public interface PluginManager {class Helper {public static <P> String getAction(Class<P> cls) {ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);if (info == null) {throw new RuntimeException(cls + " doesn't provide an interface");}if (TextUtils.isEmpty(info.action())) {throw new RuntimeException(cls + " doesn't provide an action");}return info.action();}}

我们再回到Dependency.get(PluginManager.class).addPluginListener的方法,它的第二个参数是OverlayPlugin.class

OverlayPlugin


@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION)
public interface OverlayPlugin extends Plugin {String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY";int VERSION = 3;.....}

根据我们上面的分析,就知道了在(2)号方法中获取的action就是OverlayPlugin中定义的ACTION = “com.android.systemui.action.PLUGIN_OVERLAY”,通过ProvidesInterface注解获取,好了接着(2)号方法调到了(4)号方法:

public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,Class cls, boolean allowMultiple) {mPluginPrefs.addAction(action);PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,allowMultiple, mLooper, cls, this);p.loadAll();mPluginMap.put(listener, p);startListening();}

首先将action添加到mPluginPrefs,mPluginPrefs是一个PluginPrefs,它的内部有一个ArraySet和一个SharedPreferences,它的存储机制就是将action先存储到ArraySet,再将此ArraySet存到SharedPreferences中,具体代码就不贴出了

接着mFactory.createPluginInstanceManager方法,mFactory是PluginInstanceManagerFactory类型,是PluginManagerImpl的静态内部类

@VisibleForTestingpublic static class PluginInstanceManagerFactory {public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,Class<?> cls, PluginManagerImpl manager) {return new PluginInstanceManager(context, action, listener, allowMultiple, looper,new VersionInfo().addClass(cls), manager);}}

createPluginInstanceManager其实就是new了一个PluginInstanceManager对象,
接着调用了PluginInstanceManager的loadAll方法

PluginInstanceManager.loadAll()

public void loadAll() {if (DEBUG) Log.d(TAG, "startListening");mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);}

通过handler处理消息,注意mPluginHandler这个handler,它发送的消息并不是到主线程的,因为它使用的不是主线程的Looper,这个looper其实是:
mProviders.put(BG_LOOPER, mBgLooper::get),是一个后台Looper

PluginInstanceManager(Context context, PackageManager pm, String action,PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,PluginManagerImpl manager, boolean debuggable, String[] pluginWhitelist) {mMainHandler = new MainHandler(Looper.getMainLooper());mPluginHandler = new PluginHandler(looper);...}

QUERY_ALL

接着看mPluginHandler里的具体处理:

private class PluginHandler extends Handler {private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();public void handleMessage(Message msg) {switch (msg.what) {case QUERY_ALL:if (DEBUG) Log.d(TAG, "queryAll " + mAction);//如果mPlugins不为空,则遍历mPlugins,调用onPluginDisconnected//清理Pluginfor (int i = mPlugins.size() - 1; i >= 0; i--) {PluginInfo<T> plugin = mPlugins.get(i);mListener.onPluginDisconnected(plugin.mPlugin);if (!(plugin.mPlugin instanceof PluginFragment)) {plugin.mPlugin.onDestroy();}}//清空mPluginsmPlugins.clear();handleQueryPlugins(null);break;.....}
}

handleQueryPlugins

private void handleQueryPlugins(String pkgName) {Intent intent = new Intent(mAction);//pkgName == null,此处的intent只有action,没有pkgNameif (pkgName != null) {intent.setPackage(pkgName);}//給定intent,返回满足条件的ResolveInfo(本质是service)集合List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");//mAllowMultiple == true,不会走进去if (result.size() > 1 && !mAllowMultiple) {Log.w(TAG, "Multiple plugins found for " + mAction);if (DEBUG) {for (ResolveInfo info : result) {ComponentName name = new ComponentName(info.serviceInfo.packageName,info.serviceInfo.name);Log.w(TAG, "  " + name);}}return;}//遍历ResolveInfofor (ResolveInfo info : result) {ComponentName name = new ComponentName(info.serviceInfo.packageName,info.serviceInfo.name);//根据info信息创建插件PluginInfo<T> t = handleLoadPlugin(name);if (t == null) continue;//如果成功创建了PluginInfo就添加到mPlugins保存mPlugins.add(t);//将创建好的PluginInfo发送到主线程处理mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget();}}

PLUGIN_CONNECTED

private class MainHandler extends Handler {public void handleMessage(Message msg) {switch (msg.what) {case PLUGIN_CONNECTED:if (DEBUG) Log.d(TAG, "onPluginConnected");PluginPrefs.setHasPlugins(mContext);//将handler发送过来的obj强转为PluginInfoPluginInfo<T> info = (PluginInfo<T>) msg.obj;mManager.handleWtfs();//如果是Fragment类型的Pluginif (!(msg.obj instanceof PluginFragment)) {//调用它的onCreate方法info.mPlugin.onCreate(mContext, info.mPluginContext);}//调用onPluginConnected方法mListener.onPluginConnected(info.mPlugin, info.mPluginContext);break;......}
}

我们可以看到mPluginHandler和mMainHandler的逻辑还是比较对称的:

  1. mPluginHandler在后台线程处理消息,在收到QUERY_ALL消息后如果有Plugin则先判断是否是Fragment类型Plugin,如果是则调用onDestroy方法,还会调用Plugin添加的PluginListener的回调onPluginDisconnected
  2. mMainHandler在主线程处理mPluginHandler发送来的Plugin,同样判断如果是Fragment类型Plugin则调用onCreate,还会调用Plugin添加的PluginListener的回调onPluginConnected

我们再会到addPluginListener,经过上面的分析,我们知道了PluginInstanceManager是一个专门用来管理Plugin的类,每个Plugin对应一个

public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,Class cls, boolean allowMultiple) {mPluginPrefs.addAction(action);PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,allowMultiple, mLooper, cls, this);p.loadAll();mPluginMap.put(listener, p);startListening();}

mPluginMap是一个ArrayMap,将同一个Plugin的listener和PluginInstanceManager一一对应的保存进去

private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap= new ArrayMap<>();

startListening()

private void startListening() {//保证只启动一次if (mListening) return;mListening = true;IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);filter.addAction(Intent.ACTION_PACKAGE_CHANGED);filter.addAction(Intent.ACTION_PACKAGE_REPLACED);filter.addAction(Intent.ACTION_PACKAGE_REMOVED);filter.addAction(PLUGIN_CHANGED);filter.addAction(DISABLE_PLUGIN);filter.addDataScheme("package");mContext.registerReceiver(this, filter);filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);mContext.registerReceiver(this, filter);}

这个方法就是注册了一些广播,PluginManagerImpl继承BroadcastReceiver,所有收到广播的处理都在它自己的onReceive方法中:

public void onReceive(Context context, Intent intent) {if(Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())){......}else if(DISABLE_PLUGIN.equals(intent.getAction())){......}else {.....if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {for (PluginInstanceManager manager : mPluginMap.values()) {manager.onPackageChange(pkg);}} else {for (PluginInstanceManager manager : mPluginMap.values()) {manager.onPackageRemoved(pkg);}}}
}

我们可以看到,else分支中如果是ACTION_PACKAGE_REMOVED广播则调用
manager.onPackageRemoved,否则调用manager.onPackageChange,这两个方法在PluginInstanceManager类中,其实它们是一对相对应的方法,一个onPackageRemoved最终会调到PluginListener的onPluginDisconnected中,
onPackageChange最终会调到PluginListener的onPluginConnected中,我们看下具体代码:

PluginInstanceManager

onPackageChange中相当于先调了一次onPackageRemoved

public void onPackageRemoved(String pkg) {mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();}public void onPackageChange(String pkg) {mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget();}

QUERY_PKG和REMOVE_PKG

private class PluginHandler extends Handler {public void handleMessage(Message msg) {switch (msg.what) {case REMOVE_PKG:mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,plugin.mPlugin).sendToTarget();break;case QUERY_PKG:String p = (String) msg.obj;handleQueryPlugins(p);.....break;} }
}

REMOVE_PKG中向MainHandler主线程发送了PLUGIN_DISCONNECTED的msg,QUERY_PKG中调用了handleQueryPlugins方法,此方法前面已经分析过了,最终是向MainHandler主线程发送了PLUGIN_CONNECTED的msg

private class MainHandler extends Handler {switch (msg.what) {case PLUGIN_CONNECTED:......mListener.onPluginConnected(info.mPlugin, info.mPluginContext);break;case PLUGIN_DISCONNECTED:mListener.onPluginDisconnected((T) msg.obj);......break;}
}

我们看到startListening注册的广播里处理了PluginListener的回调

本篇以SystemUI启动过程中添加的OverlayPlugin为例分析了SystemUI的插件化机制Plugin

关于Plugin作一个总结:
Systemui插件化机制的添加步骤:

  1. 创建一个继承自Plugin或者FragmentBase的接口,定义ACTION和VERSION,使用ProvidesInterface注解将ACTION和VERSION暴露出去
  2. 创建PluginListener接口作为Plugin的回调
  3. 调用PluginManagerImpl的addPluginListener方法添加PluginListener和Plugin

SystemUI插件化的目的就是使开发者能够轻松随意替换SystemUI的一些组件,而不需要修改大量代码,在AOSP提供的官方doc中提到要使用插件化Plugin的条件有:

  1. 必须使用平台证书进行签名
  2. 必须在LOCAL_JAVA_LIBRARIES中包含SystemUIPluginLib(不能是LOCAL_STATIC_JAVA_LIBRARIES)
  3. 声明插件还需要在清单文件中注册类似如下代码:
       <service android:name=".SampleOverlayPlugin"android:label="@string/plugin_label"><intent-filter><action android:name="com.android.systemui.action.PLUGIN_OVERLAY" /></intent-filter></service>
  1. 需要添加相应权限:
<uses-permission android:name="com.android.systemui.permission.PLUGIN" />
  1. 然后实现你需要替换的SystemUI组件的提供的插件接口
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {...
}

AndroidQ SystemUI之插件化机制Plugin相关推荐

  1. ASP.NET:插件化机制

    概述 nopCommerce的插件机制的核心是使用BuildManager.AddReferencedAssembly将使用Assembly.Load加载的插件程序集添加到应用程序域的引用中.具体实现 ...

  2. [插件化] Droid Plugin 学习总结

    原文地址: http://www.jianshu.com/p/d16cd0e3333f http://blog.csdn.net/qq_24889075/article/details/6848983 ...

  3. c++插件化 NDD源码的插件机制实现解析

    插件机制是一种框架,允许开发人员简单地在应用程序中添加或扩展功能.它使广泛使用,因为它可以作为模块被重复使用,并使它们更易于维护和扩展,因此它们在应用程序中非常有用.插件机制允许管理员在需要时轻松安装 ...

  4. iOS 上的插件化设计

    ????????关注后回复 "进群" ,拉你进程序员交流群???????? 转自:掘金 ZenonHuang https://juejin.cn/post/697962703724 ...

  5. Android APP热更新中的插件化(Hook技术:反射或动态代理),Demo (2)

    修改AAPT,资源分区,用于Android插件化- https://github.com/BaoBaoJianqiang/AAPT -- Android下的挂钩(hook)和代码注入(inject) ...

  6. 【Android 插件化】插件化简介 ( 组件化与插件化 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

  7. Android插件化原理—ClassLoader加载机制

    前面<Android 插件化原理学习 -- Hook 机制之动态代理>一文中我们探索了一下动态代理 hook 实现了 启动没有在 AndroidManifest.xml 中显式声明的 Ac ...

  8. Java SPI机制实现插件化扩展功能

    Java SPI机制实现插件化扩展功能 1.背景 我们有一个图数据库的服务,用户希望在不修改现有源代码的情况下扩展自定义的分词器,达到可插件式扩展功能的目标. 通过Java的SPI机制实现插件式的扩展 ...

  9. Android 插件化原理学习 —— Hook 机制之动态代理

    前言 为了实现 App 的快速迭代更新,基于 H5 Hybrid 的解决方案有很多,由于 webview 本身的性能问题,也随之出现了很多基于 JS 引擎实现的原生渲染的方案,例如 React Nat ...

  10. Android插件化原理解析——Hook机制之动态代理

    使用代理机制进行API Hook进而达到方法增强是框架的常用手段,比如J2EE框架Spring通过动态代理优雅地实现了AOP编程,极大地提升了Web开发效率:同样,插件框架也广泛使用了代理机制来增强系 ...

最新文章

  1. 【决战西二旗】|Redis面试热点之底层实现篇
  2. Unable to locate package update
  3. C++ 虚拟析构函数
  4. 使用fastcoll进行md5碰撞,两个不同的文件md5值一样。
  5. Asp.net Core Jenkins Docker 实现一键化部署
  6. k66 pit计时功能配置_PIT,JUnit 5和Gradle –仅需额外的一行配置
  7. P2240 【深基12.例1】部分背包问题
  8. bada项目在真机上调试
  9. matlab模拟退火最小球覆盖,【模板】模拟退火 费马点以及最小球覆盖
  10. Hive记录-Hive on Spark环境部署
  11. .Net WebApi接口之Swagger集成详解
  12. (一)音视频:解码H264文件流程 渲染和拿到解码后源数据YUV 完整Demo
  13. 【webRTC】一个基于 tornado 和 webRTC 的点对点视频语音文字聊天室
  14. 一个老病号对感冒发烧的经验
  15. 系统集成16真题解析
  16. linux设备驱动中的module_init
  17. Android与iOS在DES加密算法上的统一
  18. 球半足球分析,巴西甲:布拉干RB VS 博塔弗戈 7月5日
  19. 东北大学软件项目管理与过程改进复习提纲(2020)——第十一章《项目风险管理》
  20. 时间类型转换为字符串

热门文章

  1. C# 输入一个正整数N,判断N是奇数还是偶数,输出1~N的奇数和或是偶数和,三种不同方法分别实现
  2. python学习之心路历程
  3. Debian7 更换源
  4. 天正双击墙体不能编辑_如何提高天正CAD画图速度?不妨试试以下几个技巧
  5. 翟菜花:从风流到下流,杜蕾斯新文案为何被人口诛笔伐?
  6. 用知识图谱解读抑郁症——树洞
  7. 电信系统服务器地址,电信高速dns服务器地址谁知道?
  8. 小武与YOLOv3----优图代码
  9. 好听的女孩名字2225个 懿婕、馨媛、雨熙、若涵、馨瑜、瑾涵、羽欣、琪悦、逸菲、馨瑜、雨婷、昕妤、婉婷、梦琪、馨月、佳瑜、晓琦、婷瑛、诗琪、瑾瑜、艺琳、雨婷、欣怡、诗雨、佳琪、悦涵、昕瑶、蓓佳、诗萌、
  10. 网络篇 - netty实现高并发安全聊天客户端