最新要闻
- 热推荐:锂金属电池爱长枝晶?韩国科学家找到破解之法
- 【当前独家】确认了!《阿凡达2:水之道》没有片尾彩蛋
- 天天观速讯丨1152分区+4K 144Hz 联合创新32寸miniLED显示器首发5399元
- 终于修好了!Win11新补丁解决22H2大文件复制缓慢Bug
- 首款第二代骁龙8游戏旗舰!红魔8 Pro来了
- 今日最新!王思聪投资百万成立新公司 经营范围包括动漫游戏开发
- 全球最资讯丨全自动门锁比半自动更人性化!但半自动更受青睐 真相终于揭开
- 【热闻】-真正的国产亲民MPV 新款传祺M6 PRO上市:11.98万起
- 看点-旗下新作首月收入超4.8亿!腾讯成为《妮姬》开发商Shift Up第二大股东
- 女子高烧39.8度喊妈 妈妈以为鸭子叫没搭理 网友:怎么又变异了
- 观察-《三体》动画明天开播第3集 官方公布史强、古筝行动档案
- 视点!小米万兆路由器用上企业级处理器!卢伟冰:降维打击
- 【天天报资讯】LCD面板价格连连下跌!LG P7 LCD工厂停产
- 环球关注:中国高速看山东!山东高速施工用上北斗卫星:精度达到毫米级
- 世界要闻:三大板卡品牌之一的微星缺席RX 7900首发 原因揭晓:直接非公版
- 世界快消息!女生手捏温度计度数直线飙到38度!腋下一测39.5度
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
世界速读:注解在Android中的使用场景
Android课程学习记录
注解是 在Java SE5 这个版本中引入的
1、什么是注解
在代码中最常见的一个就是 @Override
,看一下它的语法定义:
(资料图片仅供参考)
@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}
可能会遇到的一个问题就是:请讲解一下什么注解?答:注解,写法上来说就是上述的形式,一般常用来标记到方法、类、参数、变量上面一句话就能解释清楚,好像说清楚了,但是又好像什么都没说清。
首先来看一下关注注解的一些基本定义
元注解- 专门负责注解其他注解的注解,常用的元注解有@Target
、@Retention
、@Documented
、 @Inherited
@Target
是标记这个注解在哪里使用的,参数类型是定义在ElementType
中的
/** Class, interface (including annotation type), or enum declaration */ TYPE, /** Field declaration (includes enum constants) */域声明 FIELD, /** Method declaration */ METHOD, /** Formal parameter declaration */ 普通参数 PARAMETER, /** Constructor declaration */ 构造方法 CONSTRUCTOR, /** Local variable declaration */ 局部变量 LOCAL_VARIABLE, /** Annotation type declaration */ 注解 ANNOTATION_TYPE, /** Package declaration */ 包 PACKAGE, /** * Type parameter declaration * Java 1.8 加入的,用于类型参数的声明 * @since 1.8 */ TYPE_PARAMETER, /** * Use of a type * Java 1.8 加入的,用于一个类型的使用 * @since 1.8 */ TYPE_USE
用 @Target
标注指定类型后,就能高速编译器做类型限制
@Retention
表示需要在什么级别保留该注解信息。参数类型是在RetentionPolicy
中定义
public enum RetentionPolicy { /** 表示注解仅在源码中可用,将会被编译器丢掉. */ SOURCE, /**表示注解会被编译器记录在 class 文件中,但在运行时虚拟机(VM)不会保留*/ CLASS, /** * 表示注解会被编译器记录在 class 文件中,而且在运行时虚拟机(VM)会保留注解。所以这里可以通过反射读取注解的信息 * @see java.lang.reflect.AnnotatedElement */ RUNTIME}
2、 使用场景
单独的讲一个注解是没啥意义的,它必须和其他的一些技术或者场景集合起来,才能发挥其作用
2.1 语法检查
这个用法在Android 系统的源码也用的比较多,看一下下边这个例子:
/**定义一个方法,参数用 {@link @DrawableRes} 标记*/ private static void setImg(@DrawableRes int imgRes){ Log.d("TAG", "setImg: "+imgRes); }
可以看到,传入不是 @DrawableRes
类型的 int 参数会提示传参有误,这个用法可用来规范传参,防止误传
2.2 代替枚举
枚举的每一个元素占用会比较大,是一个对象,有对象头等等,用注解定义的元素一般是基础数据类型,例如int等,能节省一部分开销,算是一个小小的优化项
2.3 注解+APT
一些使用比较广泛的开源框架就是利用这个技术点来实现的,例如 ButterKnife、Dagger2、Hilt等
2.3.1 APT是啥
APT 全称 Annotation Processing Tool,翻译过来就是注解处理程序,在Java 的.java 文件编译成.class 文件过程中做一些动作,简单画个图
2.3.2 操作流程
下边用一个demo简单说明下 APT 的实现流程
- 建一个新的Android项目
- 创建一个java-library 用来存放定义的注解,命名 annotation
// build.gradleapply plugin: "java-library"dependencies { implementation fileTree(dir: "libs", include: ["*.jar"])}sourceCompatibility = "7"targetCompatibility = "7"
这里定义一个 找view id的注解 BindView
@Target(ElementType.FIELD)@Retention(RetentionPolicy.SOURCE)public @interface BindView { int value();}
- 在创建一个 java-library,用来处理定义的注解,命名 annotation_compiler,定义如下
// build.gradleapply plugin: "java-library"dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) annotationProcessor "com.google.auto.service:auto-service:1.0-rc4" compileOnly "com.google.auto.service:auto-service:1.0-rc4" implementation project(path: ":annotations")}sourceCompatibility = "7"targetCompatibility = "7"
TIPS:
- 需要引入
com.google.auto.service:auto-service:1.0-rc4
依赖库 - 把前一个module annotation添加到依赖里面接下来就是 apt 的处理逻辑代码,创建一个处理类:
// 注意需要添加 @AutoService(Processor.class) 这个标记@AutoService(Processor.class)public class AnnotationsCompiler extends AbstractProcessor { @Override public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) { return false; }}
apt 的模板代码 就写好了,具体的处理逻辑 在 process()
里面去实现就好,这里先简单介绍几个 可能会用到的一些其他 父类方法
- 第一个
/** 支持的版本,一般使用 SourceVersion.latestSupported() 就好 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }
- 第二个
/** 把需要处理的注解装到一个集合里面,比如这里要处理的 BindView 注解 */ @Override public Set getSupportedAnnotationTypes() { Set types = new HashSet<>(); types.add(BindView.class.getCanonicalName()); return types; }
- 第三个
/** 初始化入口 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnvironment.getFiler(); }
- 第四个
/** 处理具体逻辑的地方 */ @Override public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) { return false; }
接下来就根据 添加注解自动找ID这个功能做一个简单的实现,整个注解处理的逻辑如下:
@AutoService(Processor.class)public class AnnotationsCompiler extends AbstractProcessor { @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public Set getSupportedAnnotationTypes() { Set types = new HashSet<>(); types.add(BindView.class.getCanonicalName()); return types; } /** 定义一个对象,用来生成APT目录下面的文件 */ Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnvironment.getFiler(); } /** * 所有的坏事都在这个方法中实现 */ @Override public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "test---------------" + set); //获取APP中所有用到了BindView注解的对象 Set extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);// TypeElement//类// ExecutableElement//方法// VariableElement//属性 //开始对elementsAnnotatedWith进行分类 Map> map = new HashMap<>(); for (Element element : elementsAnnotatedWith) { VariableElement variableElement = (VariableElement) element; String activityName = variableElement.getEnclosingElement().getSimpleName().toString(); Class> aClass = variableElement.getEnclosingElement().getClass(); List variableElements = map.get(activityName); if (variableElements == null) { variableElements = new ArrayList<>(); map.put(activityName, variableElements); } variableElements.add(variableElement); } //开始生成文件// package com.example.aptdemo;// import com.example.aptdemo.IBinder;// public class MainActivity_ViewBinding implements IBinder {// @Override// public void bind(com.example.aptdemo.MainActivity target) {// target.textView = (android.widget.TextView) target.findViewById(2131165359);//// }// } if (map.size() > 0) { Writer writer = null; Iterator iterator = map.keySet().iterator(); while (iterator.hasNext()) { String activityName = iterator.next(); List variableElements = map.get(activityName); //得到包名 TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement(); String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString(); try { JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + activityName + "_ViewBinding"); writer = sourceFile.openWriter(); // package com.example.aptdemo; writer.write("// source code is generated by CusButterKnife\n\n"); writer.write("package " + packageName + ";\n"); // import com.example.aptdemo.IBinder; writer.write("import " + packageName + ".IBinder;\n"); // public class MainActivity_ViewBinding implements IBinder< // com.example.aptdemo.MainActivity>{ writer.write("public class " + activityName + "_ViewBinding implements IBinder<" + packageName + "." + activityName + ">{\n"); // public void bind(com.example.aptdemo.MainActivity target) { writer.write(" @Override\n" + " public void bind(" + packageName + "." + activityName + " target){\n"); //target.tvText=(android.widget.TextView)target.findViewById(2131165325); for (VariableElement variableElement : variableElements) { //得到名字 String variableName = variableElement.getSimpleName().toString(); //得到ID int id = variableElement.getAnnotation(BindView.class).value(); //得到类型 TypeMirror typeMirror = variableElement.asType(); writer.write("\ttarget." + variableName + "=(" + typeMirror + ")target.findViewById(" + id + ");\n"); } writer.write("\t}\n}"); } catch (Exception e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (Exception e) { e.printStackTrace(); } } } } } return false; }}
到此,注解处理的逻辑就完成了,接下来,在app module 中写上一些工具类
- 首先定义一个接口,用于绑定有添加对应注解
@BindView
的界面,把当前界面 与 apt生成的代码 关联起来,apt自动生成的代码类是实现该接口的
public interface IBinder { void bind(T target);}
2.顶一个工具类,用来传入当前界面
public class CusButterKnife { public static void bind(@NonNull Activity activity) { String name = activity.getClass().getName() + "_ViewBinding"; try { Class> aClass = Class.forName(name); IBinder iBinder = (IBinder) aClass.newInstance(); iBinder.bind(activity); } catch (Exception e) { e.printStackTrace(); } }}
3.看下Activity的使用
@BindView(R.id.tvText) TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CusButterKnife.bind(this); textView.setText("123456"); }
4.在 annotation_compiler代码写完之后,build 一下工程,就能找到我们自己实现的需要生成的代码:
// source code is generated by CusButterKnifepackage com.example.aptdemo;import com.example.aptdemo.IBinder;public class MainActivity_ViewBinding implements IBinder { @Override public void bind(com.example.aptdemo.MainActivity target) { target.textView = (android.widget.TextView) target.findViewById(2131165359); }}
至此,这个注解+APT 实现自动找id 的功能就完成了
2.4 注解+反射+动态代理
比如于xutils 这个框架的核心处理逻辑就是通过这个来实现的
也通过一个demo实现一个功能:给方法添加注解,自动实现view 的点击事件众所周知,常规实现一个按钮的点击实现,是需要这样的代码:
Button btn1 = findViewById(R.id.btn1);btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });
这里需要实现的功能是 写一个方法,添加一个注解(点击事件定义注解为@OnClick
),直接实现点击功能,无需再使用上述的常规写法注册点击事件,代码如下:
@OnClick(R.id.btn1) public void abc(@NonNull View view) { Toast.makeText(this, "触发点击", Toast.LENGTH_SHORT).show(); }
具体实现如下
- 第一步,把注册点击事件这段代码划分成三个部分,如下图所示:
- 通过注解 @OnClick(R.id.btn1),就要能拿到对应的这三部分用于实现点击事件,那就创建一个注解类,用来存放这三个部分
@Target(ElementType.ANNOTATION_TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface EventBase { //1.订阅关系 setOnClickListener String listenerSetter(); //2.事件本身 new View.OnClickListener() Class> listenerType(); //3.事件处理程序 onClick方法 String callbackMethod();}
- 把这个注解 标记到我们的功能注解
@OnClick
上
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@EventBase(listenerSetter = "setOnClickListener" ,listenerType = View.OnClickListener.class ,callbackMethod = "onClick")public @interface OnClick { int[] value() default -1;}
- 接下来,写一个工具类,通过反射的语法,把
@OnCLick
注解的具体逻辑给它实现
public class InjectUtils { public static void inject(Activity context) { inJeckEvent(context); } private static void inJeckEvent(Activity context) { Class> clazz = context.getClass(); // 反射获取传入类所有的方法 Method[] methods = clazz.getDeclaredMethods();//遍历,筛选出有所需注解的方法 for (Method method : methods) {//拿到有注解的方法的注解 Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { Class> annotationClass = annotation.annotationType();//遍历方法注解,判断是否有 EventBase 注解 EventBase eventBase = annotationClass.getAnnotation(EventBase.class); //判断是不是事件处理程序 onClick onLongClink if (eventBase == null) { continue; } //1.setOnClickListener 订阅关系// String listenerSetter(); String listenerSetter = eventBase.listenerSetter(); //2.new View.OnClickListener() 事件本身// Class> listenerType(); Class> listenerType = eventBase.listenerType(); //3.事件处理程序// String callbackMethod(); String callBackMethod = eventBase.callbackMethod(); //得到3要素之后,就可以执行代码了 Method valueMethod = null; try { //拿到注解 value (传入的id) valueMethod = annotationClass.getDeclaredMethod("value"); int[] viewId = (int[]) valueMethod.invoke(annotation); for (int id : viewId) { //为了得到Button对象,使用findViewById Method findViewById = clazz.getMethod("findViewById", int.class); View view = (View) findViewById.invoke(context, id); if (view == null) { continue; } //context===activity click=method ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context, method); //new View.OnClickListener() Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler); // view.setOnClickListener(new View.OnClickListener()) Method onClickMethod = view.getClass().getMethod(listenerSetter, listenerType); onClickMethod.invoke(view, proxy); } } catch (Exception e) { e.printStackTrace(); } } } }}
这一段代码,是使用了动态代理,把组件原本onclick代理到 有对应注解的方法上,看一下相关实现代码
/** * 这个类用来代理 事件本身 (new View.OnClickListener()) * 并执行这个对象身上的onClick方法 */public class ListenerInvocationHandler implements InvocationHandler { //需要在onClick中执行activity.click(); private Activity activity; private Method activityMethod; public ListenerInvocationHandler(Activity activity, Method activityMethod) { this.activity = activity; this.activityMethod = activityMethod; } /** * 就表示onClick的执行 * 程序执行onClick方法,就会转到这里来 * 因为框架中不直接执行onClick * 所以在框架中必然有个地方让invoke和onClick关联上 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在这里去调用被注解了的click(); return activityMethod.invoke(activity,args); }}
- 最后在 activity 中调用
InjectUtils.inject
,就完成了,看下运行
到此 ,APT+注解+动态代理 的常见使用方式就讲完了
补充
这里只是实现了 点击事件,那比如要实现注解实现长按事件呢?先前已经定了一个注解用来存放 一个事件注册的及要素嘛,这个时候,要在进行扩展就比较容易了,长按事件的注解实现,就只需要定义一个这样的注解类就行了:
@EventBase(listenerSetter = "setOnLongClickListener" ,listenerType = View.OnLongClickListener.class ,callbackMethod = "onLongClick")@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface OnLongClick { int[] value() default -1;}
是不是很眼熟,跟上面 OnCLick 注解的定义止呕三要素的不同,传不同的参数到 @EventBase
注解里面就行了,这个就是一个 注解的多态这个概念的实际应用了。在想要扩展Android 中的其他各种出入事件监听,就一次类推,定义对应的注解就行了;
好了,结束
世界速读:注解在Android中的使用场景
热推荐:锂金属电池爱长枝晶?韩国科学家找到破解之法
【当前独家】确认了!《阿凡达2:水之道》没有片尾彩蛋
天天观速讯丨1152分区+4K 144Hz 联合创新32寸miniLED显示器首发5399元
终于修好了!Win11新补丁解决22H2大文件复制缓慢Bug
首款第二代骁龙8游戏旗舰!红魔8 Pro来了
今日最新!王思聪投资百万成立新公司 经营范围包括动漫游戏开发
全球最资讯丨全自动门锁比半自动更人性化!但半自动更受青睐 真相终于揭开
世界简讯:Hessian2序列化支持这一点,让重构dubbo接口更容易了
【热闻】-真正的国产亲民MPV 新款传祺M6 PRO上市:11.98万起
看点-旗下新作首月收入超4.8亿!腾讯成为《妮姬》开发商Shift Up第二大股东
女子高烧39.8度喊妈 妈妈以为鸭子叫没搭理 网友:怎么又变异了
观察-《三体》动画明天开播第3集 官方公布史强、古筝行动档案
【天天聚看点】微信小程序报错“getLocation:fail the api need to be declared in the requiredPriva
【全球快播报】记录--三分钟打造自己专属的uni-app工具箱
视点!小米万兆路由器用上企业级处理器!卢伟冰:降维打击
【天天报资讯】LCD面板价格连连下跌!LG P7 LCD工厂停产
环球关注:中国高速看山东!山东高速施工用上北斗卫星:精度达到毫米级
世界要闻:三大板卡品牌之一的微星缺席RX 7900首发 原因揭晓:直接非公版
世界快消息!女生手捏温度计度数直线飙到38度!腋下一测39.5度
天天日报丨项目经理的核心价值:以目标为导向做正确的事
环球热点评!Vue3项目-生成Cron表达式组件
世界今亮点!MIUI 14终于再次成为最好用的操作系统
ChatGPT已经牛到取代谷歌了?测试结果来了
男子每天点赞上万次被处罚 当庭演示一分钟才点赞91个
【当前独家】“智轨列车”亮相咸阳:可识别虚拟轨道 载客达300人
全球新动态:研究揭示马桶不盖盖后果多严重:致病菌满屋乱飞
全球滚动:Java 反射概念的引入
天天热议:小米13太火爆了 博主准点抢购结果秒没:最后等了20分钟捡漏 成功上车
【天天速看料】“火流星”掉落 专家判断陨石来自46亿年前:比地球上所有石头都古老
当前速看:《阿凡达2》成2022进口片首日票房冠军!时隔69天单日再破亿 豆瓣8.4分
价格能顶半套正版Win11 老牌压缩软件WinZip 27发布 你会买吗?
快播:44岁的泰国长公主因心脏问题失去知觉:紧急送医
渗透实录-01
要闻:Nacos 2.2 正式发布,这次更新太炸了!
世界关注:Kerberos身份验证在ChunJun中的落地实践
IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!
【聚看点】多数据源事务处理-涉及分布式事务
怎么硬盘安装ubuntu?硬盘ubuntu安装教程
佳能5d系列哪个最好?佳能5DX相机参数
诺基亚6200上市价格是多少?诺基亚6200手机参数
烤箱如何预热?烤箱预热的方法有哪些?
短讯!IDEA没有新建jsp文件按钮
VS2022生成控制台引用程序,.net应用导出成exe文件,发部成独立文件的详细图解
MySQL学习笔记2
网页字体变大是怎么回事?网页字体变大了怎么还原?
洞庭连天是什么意思?洞庭连天九疑高是什么生肖?
国庆一词最早出现在什么时候?国庆一词最早出现在什么地方?
蝴蝶发现花蜜靠的是什么?蝴蝶发现花蜜的句子有哪些?
造梦西游3金角大王怎么打?造梦西游3金角大王掉什么?
传闻中的陈芊芊韩烁失忆是第几集?传闻中的陈芊芊韩烁失忆是真的吗?
爱情公寓决战紫禁之巅是第几集?爱情公寓决战紫禁之巅花了多少经费?
曾小莲是什么梗?爱情公寓曾小莲是哪一集?
睢宁县属于哪个市?睢宁县旅游景点有哪些?
类似于惊天魔盗团的电影有哪些?惊天魔盗团剧情解析
独家首发是什么意思?独家首发和普通授权有什么区别?
环球短讯!认证管理(锐捷网关篇)
世界即时看!C语言字符串拆分的两种方式strtok和正则表达式
全球通讯!低代码靠不靠谱?看看低代码在智能物联系统搭建中的应用
美国一战机垂直降落失控 飞行员弹射7秒落地:现场机头先撞地
世界时讯:可拆卸手柄神似Switch!OnexPlayer 2掌机海外发布:起售价超6200元
全球要闻:混动车鼻祖上新 全新丰田普锐斯售价公布:约19.10万元起
一加10T漫威限定版上架:用4年仍然很流畅 4700元
天天观焦点:三连降稳了!新一轮油价三天后开调:预计下跌0.41元/升
世界资讯:因MacBook Pro蝶式键盘翻车:苹果赔了3个多亿
速递!贾跃亭造车梦要成了?FF91交付计划公布 还差10几亿资金
全球热议:爱奇艺VIP会员今起涨价:连续包月黄金25元/月
今日热讯:浙江“火流星”现场被砸大坑!专家:捡到陨石碎片别用水清洗
焦点精选!机械硬盘真没人买!西数股价大跌 公司业绩要暴雷:或将大清库存
每日精选:V2Board机场项目泄露400余万条数据
环球快报:《阿凡达2:水之道》今日内地正式上映:票房瞬间破亿
日系车为什么在中国卖不动了?你为什么不买了?
每日速讯:Blazor和Vue对比学习(进阶.路由导航五):路由守卫
观点:【从零开始学爬虫】采集收视率排行数据
mvn 打包报错:no compiler is provided in this environment
天天微动态丨JavaScript DOM的性能优化详解
每日资讯:VUE的实例的生命周期
每日热门:河洛肉鸽卡牌《天外武林》上架Steam 明年1月发售
别急换机!本月还有6场发布会:劝你先等等再买
世界微动态丨《阿凡达:水之道》预测票房仅25.11亿!远不到《长津湖》一半
【环球新要闻】Epic大促开启 连续15天免费送游戏!75折套娃优惠券来了
【天天播资讯】RTX 4070 Ti跑分首曝:猛升46%、超越RX 7900 XTX
焦点热讯:替代if esle 的高级方法
make学习
天天热门:高手必备10大难题:Mysql如何实现RR级隔离时,不会幻读?
FreeSWITCH学习笔记:应用程序(APP)
速度飞起!全球首款232层闪存SSD细节公布:TLC颗粒、拒绝PCIe 5.0
快资讯:一图看懂moto新机:二代骁龙8 3699元!价格屠夫简直了
全球观察:7-2 案例 字符串关键字的散列映射
天天热议:微软2025年目标:确保非洲再增加1亿人访问互联网
焦点信息:火球坠入浙江 警方带走疑似陨石碎片:现场砸出六七厘米深坑
环球微头条丨初始Docker
热点评!分智慧果 - 2021算法与数据结构实验题
环球观热点:不调好不上市 moto X40驯服二代骁龙8:1小时王者温度仅42度
Nature年度十大人物出炉:猪心移植手术团队负责人等入选
饱满如水滴!联想moto X40开箱图赏
5000mAh大电池+120Hz屏 899元联想moto G53图赏
109度大电池能跑730KM!岚图追光预售:32.29万元起
天天微动态丨自研分布式高性能RPC框架及服务注册中心ApiRegistry实践笔记【原创】【开源】
【独家】图形用户界面(GUI)编程可以学习C++ Builder,多图、实例、书籍