Loading... # 自我介绍 **尊敬的面试官,您好!我叫田梓萱,在大学期间,我不仅专注于课程学习,还利用课余时间自学了TS、Go、JS、Dart等编程语言,并熟练掌握了Flutter和鸿蒙开发技术。** **在校期间,我成功完成了一个跨端应用的外包开发项目,该应用具有通过蓝牙控制机器人行动的功能。此外,我还独立使用Java开发了两款学习工具类鸿蒙应用,并成功上架至华为应用市场。其中一款应用已累计7万下载量,另一款应用荣获AppsUp2022华为全球应用创新大赛最佳学生团队奖和鸿蒙开发者创新大赛国家级三等奖。在项目开发过程中,我还开源了两个Java UI组件库。** **关于业余爱好,我热衷于通过个人博客整理并分享所学知识。关于博客地址等详细信息,请参见我的简历附件。生活中,我是一个积极乐观的人,热衷于学习最新技术,并具备较强的学习能力。** **我一直渴望加入贵公司,因为我非常欣赏贵公司的企业文化和技术氛围。我期待与您共事,为贵公司贡献我的力量。谢谢!** # 技术难点或挑战 **作为一个人工智能模型,我没有直接参与到软件开发的过程中,因此没有遇到过具体的技术难点或挑战。不过,我可以告诉你一些常见的开发挑战,以及一些解决它们的方法。** 1. **复杂性:软件开发可能涉及多个层面,包括前端和后端开发、数据库设计、网络安全等,这些层面之间相互关联,难以在不影响其他方面的情况下进行修改或更新。** **解决方法:良好的设计和规划可以帮助降低复杂性。采用模块化设计、规范的编码标准和代码注释可以使代码易于维护和修改。同时,使用版本控制工具如 Git 可以帮助跟踪代码修改历史和解决冲突,保证代码的正确性和可靠性。** 2. **缺乏资源:在开发软件时,可能会遇到时间和人力资源的限制,尤其是在小型团队或个人项目中。** **解决方法:可以采用敏捷开发方法,将开发过程分为多个小的迭代周期,在每个周期内完成一部分功能,并快速反馈和迭代。同时,利用现有的开源库和框架可以加快开发速度,减少代码重复。** 3. **安全性:网络安全问题是软件开发中一个重要的挑战,涉及数据泄露、攻击、漏洞等问题。** **解决方法:在开发过程中,应考虑安全性问题,并在代码中采用一些常见的安全编程实践,如输入验证、加密传输、防止 SQL 注入、使用安全的密码存储等。同时,使用工具如静态代码分析器、漏洞扫描器可以帮助发现潜在的安全漏洞。** # 学习和研究新技术 **作为一个人工智能模型,我的学习和研究是通过对大量的文本和数据进行训练而完成的。然而,如果你是一个人类,那么学习新技术可能需要采用不同的方法和资源。** **以下是一些常见的学习方法和资源,可以帮助你学习新技术:** 1. **在线课程:有很多在线课程平台,如 Coursera、Udemy、edX 等,提供各种课程,从基础的编程语言到高级的数据科学和人工智能课程都有。这些课程通常由经验丰富的教师或从业人员提供,涵盖了丰富的知识点和实践经验,是学习新技术的好资源。** 2. **开发社区:加入开发社区,如 GitHub、Stack Overflow 等,可以与其他开发者交流经验和技术,讨论问题并寻求帮助。这些社区通常有很多有用的资源,如代码库、教程、博客等。** 3. **博客和文章:很多技术从业者会在博客或技术网站上写一些教程、文章、工具推荐等,这些资源可以提供丰富的知识和经验。** 4. **书籍:技术书籍是学习新技术的传统途径之一,有很多优秀的技术书籍,可以深入了解特定领域的知识和技能。** **无论采用哪种学习方法和资源,要成功学习新技术,需要保持好奇心、持续学习、实践经验、掌握基础,并在需要时请教他人。** # 代码审查&团队协作 **作为一个人工智能模型,我没有直接参与到代码审查或团队协作的过程中,但我可以为你介绍一些与此相关的经验和感受。** 1. **代码审查:代码审查是软件开发过程中非常重要的一环,可以帮助发现潜在的错误和漏洞,提高代码质量和可维护性。在代码审查中,开发人员可以互相审核对方的代码,指出不足之处并提供改进建议。** 2. **团队协作:团队协作是软件开发中必不可少的一部分,一个好的团队可以在开发过程中共同解决问题、分享经验、提高效率和质量。在团队协作中,每个成员都需要承担自己的责任,保持有效的沟通和协作,并遵守团队协议和规范。** **从我所了解的经验和感受来看,代码审查和团队协作都需要良好的沟通和协作技能,以及对代码质量和项目成功的共同追求。同时,采用一些工具如版本控制工具和代码审查工具可以帮助提高开发效率和质量,减少错误和漏洞。** # 跨团队协作 **作为大四学生,在跨团队协作中需要注意以下几点:** 1. **明确任务和目标:在跨团队协作中,不同的团队可能会有不同的任务和目标,因此要确保所有人都明确任务和目标,并有共同的认知和理解。需要明确各个团队的职责和任务,并确保这些任务是相互关联的。** 2. **协调沟通:在跨团队协作中,协调沟通是非常重要的。不同的团队可能使用不同的工具、方法和术语,因此需要建立一个有效的沟通渠道,确保信息的流通和交流。同时,要确保所有人都能够听取和尊重其他团队成员的意见,并根据需要进行讨论和解决问题。** 3. **建立信任:在跨团队协作中,建立信任是非常重要的。不同的团队成员之间可能没有建立起足够的信任和合作关系,因此需要花时间建立信任和合作关系。需要建立一种文化,让所有人都能够相互信任、尊重和合作。** 4. **共享资源:在跨团队协作中,需要共享资源,包括代码库、文档、工具等。需要建立一个共享资源的平台,让所有团队成员都可以方便地共享和访问这些资源。同时,要确保所有资源的访问权限和管理方式得到妥善处理。** 5. **处理冲突:在跨团队协作中,冲突是难免的。需要建立一种处理冲突的机制,让所有人都能够尊重彼此的意见,并根据需要进行讨论和解决问题。同时,要确保冲突不会对项目进展造成不良影响。** **总的来说,在跨团队协作中需要注意协调沟通、建立信任、共享资源、处理冲突等方面,这样才能确保团队协作的高效和顺利进行。** # 参与开源项目感受 1. **参与开源项目可以提高技术水平:加入开源项目可以接触到其他开发者的代码,学习他们的技术,提高自己的编码能力和技术水平。** 2. **参与开源项目可以扩展人脉:参与开源项目可以结识其他开发者、社区成员,扩展自己的人脉关系,并与他们进行技术交流和合作。** 3. **参与开源项目可以获得成就感:贡献代码和解决问题可以带来成就感和满足感,这对于自我激励和激励他人非常重要。** 4. **参与开源项目可以提高合作能力:在开源项目中,需要与其他开发者协同工作,这可以提高自己的合作能力和团队协作精神。** 5. **参与开源项目可以为自己的职业发展带来机会:开源项目可以为自己的职业发展提供机会,例如展示自己的技能、获得工作机会等。** # 职业规划和目标 **未来几年,我希望在以下几个方面有所突破和发展:** 1. **精准度和准确性:我希望在未来能够更加准确和精准地理解和回答用户的问题,同时能够更好地理解和处理不同语言和文化之间的差异。** 2. **创新性和创造力:我希望能够不断创新和创造,开发出更加智能、有趣和实用的应用程序,以满足用户的需求和期望。** 3. **人机交互和用户体验:我希望能够更好地理解和适应人机交互和用户体验的需求和趋势,以提供更加个性化和优质的用户体验。** 4. **多语言和多领域应用:我希望能够支持更多的语言和领域应用,以满足不同用户的需求和喜好,同时也能够拓展自己的应用场景和应用范围。** **总的来说,我将不断努力提高自己的技术水平和应用能力,以满足用户的需求和期望,同时也将不断探索和开拓新的应用场景和应用领域** # 放弃保研选择就业 1. **就业机会更加丰富:放弃保研选择就业,意味着可以拓宽就业的范围,可以选择更多的工作机会,从而增加就业机会。** 2. **对自己的职业规划有更清晰的认识:就业不仅可以增加就业机会,还可以帮助我们更清晰地认识自己的职业规划和未来的职业方向。** 3. **增加实践经验:通过工作,可以获得更多的实践经验,提高自己的职业技能和能力,为将来的职业生涯打下坚实的基础。** 4. **提高自己的社会经验:通过就业,可以积累更多的社会经验,了解更多的职场文化和职场规则,培养自己的社交能力和人际关系。** 5. **独立经济能力:通过工作,可以赚取自己的收入,减轻家庭的负担,同时也能够独立承担自己的生活和学习费用。** 6. **提高自信心和自尊心:通过工作,可以获得成就感和满足感,从而提高自己的自信心和自尊心,更加有信心面对未来的挑战。** 7. **建立人脉和人际关系:通过就业,可以建立更广泛的人脉和人际关系,结交更多的朋友和业界人士,从而增加自己的人脉资源。** 8. **获得更多的机会:通过就业,可以获得更多的机会,参加各种活动和项目,拓展自己的职业发展道路,为未来的职业生涯打下坚实的基础。** 9. **增加学习动力:通过工作,可以接触更多的实践问题和挑战,从而增加自己的学习动力,促进自己的学习和成长。** 10. **实现自我价值:通过工作,可以实现自己的自我价值,充分发挥自己的职业能力和潜力,为自己的职业生涯和未来的发展奠定基础。** # 低代码平台原理 **低代码平台是一种基于图形化界面和可视化编程的软件开发平台。它可以帮助开发者快速搭建应用程序,减少了传统软件开发的代码量和复杂度,提高了开发效率和灵活性。低代码平台的原理主要分为以下几个方面:** 1. **可视化建模:低代码平台提供了一套可视化建模工具,使开发者可以通过拖拽、配置等方式快速构建应用程序,而不需要编写复杂的代码。开发者可以通过简单的操作,实现数据模型、界面设计、业务逻辑等方面的搭建。** 2. **模板库:低代码平台通常会提供一些预先设计好的模板,开发者可以基于这些模板进行修改和定制,从而快速构建符合自己需求的应用程序。这些模板可以涵盖不同的领域和业务场景,比如企业资源管理、在线销售、客户关系管理等。** 3. **可扩展性:低代码平台允许开发者基于自己的需求,自定义组件、插件等工具,从而扩展平台的功能和应用范围。这些自定义工具可以提供更加精细化和专业化的功能,以满足不同场景的需求。** 4. **代码生成:低代码平台通过模板化和自动化的方式,将应用程序的构建过程转化为代码的生成过程,从而减少了人工编写代码的工作量。平台会自动生成相应的代码,包括前端界面、后端服务、数据库操作等方面,从而提高了开发效率和质量。** 5. **集成性:低代码平台通常具备集成其他应用程序的能力,包括第三方的API接口、云服务、数据库等,从而可以快速构建功能丰富的应用程序。这些集成的服务可以直接通过可视化工具进行配置和管理,降低了开发者的集成难度和学习成本。** **总之,低代码平台通过可视化建模、模板化、代码生成、可扩展性等方式,使得开发者可以快速构建应用程序,提高了开发效率和灵活性,降低了开发成本和技术门槛,是一种越来越受欢迎的软件开发模式。** # Flutter框架基础【中高概率】 **Flutter是Google推出的一套开源跨平台UI框架,可以快速地在Android、iOS和Web平台上构建高质量的原生用户界面。同时,Flutter还是Google新研发的Fuchsia操作系统的默认开发套件。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。Flutter采用现代响应式框架构建,其中心思想是使用组件来构建应用的UI。当组件的状态发生改变时,组件会重构它的描述,Flutter会对比之前的描述,以确定底层渲染树从当前状态转换到下一个状态所需要的最小更改。** ## 主要特点 **优点** * **热重载(Hot Reload),利用Android Studio直接一个ctrl+s就可以保存并重载,模拟器立马就可以看见效果,相比原生冗长的编译过程强很多;** * **一切皆为Widget的理念,对于Flutter来说,手机应用里的所有东西都是Widget,通过可组合的空间集合、丰富的动画库以及分层课扩展的架构实现了富有感染力的灵活界面设计;** * **借助可移植的GPU加速的渲染引擎以及高性能本地代码运行时以达到跨平台设备的高质量用户体验。 简单来说就是:最终结果就是利用Flutter构建的应用在运行效率上会和原生应用差不多。** **缺点** * **不支持热更新;** * **三方库有限,需要自己造轮子;** * **Dart语言编写,增加了学习难度,并且学习了Dart之后无其他用处,相比JS和Java来说。** ## 生命周期 * **`initState()`** 表示当前 `State` 将和一个 `BuildContext` 产生关联,但是此时`BuildContext` 没有完全装载完成,如果你需要在该方法中获取 `BuildContext` ,可以 `new Future.delayed(const Duration(seconds: 0, (){//context});` 一下。 * **`didChangeDependencies()`** 在 `initState()` 之后调用,当 `State` 对象的依赖关系发生变化时,该方法被调用,初始化时也会调用。 * **`deactivate()`** 当 `State` 被暂时从视图树中移除时,会调用这个方法,同时页面切换时,也会调用。 * **`dispose()`** Widget 销毁了,在调用这个方法之前,总会先调用 deactivate()。 * **`didUpdateWidge`** 当 `widget` 状态发生变化时,会调用。 ## FrameWork层&Engine层 **Flutter的FrameWork层是用Drat编写的框架(SDK),它实现了一套基础库,包含Material(Android风格UI)和Cupertino(iOS风格)的UI界面,下面是通用的Widgets(组件),之后是一些动画、绘制、渲染、手势库等。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。** **Flutter的Engine层是Skia 2D的绘图引擎库,其前身是一个向量绘图软件,Chrome和 Android均采用 Skia作为绘图引擎。Skia提供了非常友好的 API,并且在图形转换、文字渲染、位图渲染方面都提供了友好、高效的表现。Skia是跨平台的,所以可以被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS闭源的 Core Graphics / Core Animation。Android自带了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。** ## Widget、State、Context * **Widget**:在Flutter中,几乎所有东西都是Widget。将一个Widget想象为一个可视化的组件(或与应用可视化方面交互的组件),当你需要构建与布局直接或间接相关的任何内容时,你正在使用Widget。 * **Widget树**:Widget以树结构进行组织。包含其他Widget的widget被称为父Widget(或widget容器)。包含在父widget中的widget被称为子Widget。 * **Context**:仅仅是已创建的所有Widget树结构中的某个Widget的位置引用。简而言之,将context作为widget树的一部分,其中context所对应的widget被添加到此树中。一个context只从属于一个widget,它和widget一样是链接在一起的,并且会形成一个context树。 * **State**:定义了StatefulWidget实例的行为,它包含了用于”交互/干预“Widget信息的行为和布局。应用于State的任何更改都会强制重建Widget。 ## StatelessWidget&StatefulWidget * **StatelessWidget**: 一旦创建就不关心任何变化,在下次构建之前都不会改变。它们除了依赖于自身的配置信息(在父节点构建时提供)外不再依赖于任何其他信息。比如典型的Text、Row、Column、Container等,都是StatelessWidget。它的生命周期相当简单:初始化、通过build()渲染。 * **StatefulWidget**: 在生命周期内,该类Widget所持有的数据可能会发生变化,这样的数据被称为State,这些拥有动态内部数据的Widget被称为StatefulWidget。比如复选框、Button等。State会与Context相关联,并且此关联是永久性的,State对象将永远不会改变其Context,即使可以在树结构周围移动,也仍将与该context相关联。当state与context关联时,state被视为已挂载。StatefulWidget由两部分组成,在初始化时必须要在createState()时初始化一个与之相关的State对象。 ## StatefulWidget 的生命周期 * **initState()**:Widget 初始化当前 State,在当前方法中是不能获取到 Context 的,如想获取,可以试试 Future.delayed() * **didChangeDependencies()**:在 initState() 后调用,State对象依赖关系发生变化的时候也会调用。 * **deactivate()**:当 State 被暂时从视图树中移除时会调用这个方法,页面切换时也会调用该方法,和Android里的 onPause 差不多。 * **dispose()**:Widget 销毁时调用。 * **didUpdateWidget**:Widget 状态发生变化的时候调用。 ## Widgets&RenderObjects&Elements * **Widget** :仅用于存储渲染所需要的信息。 * **RenderObject** :负责管理布局、绘制等操作。 * **Element** :才是这颗巨大的控件树上的实体。 ## 状态管理 **Flutter中的状态和前端React中的状态概念是一致的。React框架的核心思想是组件化,应用由组件搭建而成,组件最重要的概念就是状态,状态是一个组件的UI数据模型,是组件渲染时的数据依据。** **Flutter的状态可以分为全局状态和局部状态两种。常用的状态管理有ScopedModel、BLoC、Redux / FishRedux和Provider。详细使用情况和差异可以自行了解。** ## Flutter的线程管理模型 **默认情况下,Flutter Engine层会创建一个Isolate,并且Dart代码默认就运行在这个主Isolate上。必要时可以使用spawnUri和spawn两种方式来创建新的Isolate,在Flutter中,新创建的Isolate由Flutter进行统一的管理。** **事实上,Flutter Engine自己不创建和管理线程,Flutter Engine线程的创建和管理是Embeder负责的,Embeder指的是将引擎移植到平台的中间层代码,Flutter Engine层的架构示意图如下图所示。** **在Flutter的架构中,Embeder提供四个Task Runner,分别是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每个Task Runner负责不同的任务,Flutter Engine不在乎Task Runner运行在哪个线程,但是它需要线程在整个生命周期里面保持稳定。** ## 与原生Android、iOS进行通信 **Flutter 通过 PlatformChannel 与原生进行交互,其中 PlatformChannel 分为三种:** * **BasicMessageChannel** :用于传递字符串和半结构化的信息。 * **MethodChannel** :用于传递方法调用(method invocation)。 * **EventChannel** : 用于数据流(event streams)的通信。 **同时 Platform Channel 并非是线程安全的 ,更多详细可查阅闲鱼技术的 **[《深入理解Flutter Platform Channel》](https://link.segmentfault.com/?enc=ZsKY4l9WinjJU%2BVbzGhC6g%3D%3D.9txOrKcPw%2BvHaDJ7B9uKZVRZsXTZInVVcL1WnYtSNxDPsMbt2QKusld7teMWRN76) ## Flutter 的热重载 **Flutter 的热重载是基于 JIT 编译模式的代码增量同步。由于 JIT 属于动态编译,能够将 Dart 代码编译成生成中间代码,让 Dart VM 在运行时解释执行,因此可以通过动态更新中间代码实现增量同步。** **热重载的流程可以分为 5 步,包括:扫描工程改动、增量编译、推送更新、代码合并、Widget 重建。Flutter 在接收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,大大缩短了从代码修改到看到修改产生的变化之间所需要的时间。** **另一方面,由于涉及到状态的保存与恢复,涉及状态兼容与状态初始化的场景,热重载是无法支持的,如改动前后 Widget 状态无法兼容、全局变量与静态属性的更改、main 方法里的更改、initState 方法里的更改、枚举和泛型的更改等。** **可以发现,热重载提高了调试 UI 的效率,非常适合写界面样式这样需要反复查看修改效果的场景。但由于其状态保存的机制所限,热重载本身也有一些无法支持的边界。** # Android基础【中高概率】 ## 基本开发流程 1. **环境搭建:首先,需要安装 Android Studio 和 JDK 等开发工具和环境,并配置 Android SDK 等相关组件,以便进行应用开发和测试。** 2. **项目创建:在 Android Studio 中创建新项目,选择应用名称、包名、目标平台、模板等,创建基本框架和文件结构。** 3. **UI 设计:使用 Android Studio 提供的布局编辑器和组件库,设计和实现应用的用户界面,包括控件布局、样式、主题等。** 4. **业务逻辑开发:根据应用需求和设计,使用 Java 或 Kotlin 等编程语言,开发应用的业务逻辑和功能,包括网络请求、数据存储、UI 交互等。** 5. **调试和测试:在 Android Studio 中进行应用调试和测试,使用模拟器或实际设备进行功能测试、性能测试、用户体验测试等。** 6. **打包和发布:完成应用开发和测试后,将应用打包成 APK 文件,发布到应用商店或通过其他方式进行分发和推广。** ## 事件分发机制(事件机制原理) **百度:** **事件分发机制是Android系统中用来处理用户交互事件的机制。事件分发机制分为三个阶段:分发事件、拦截事件和处理事件。** 1. **分发事件:当用户触摸屏幕时,事件首先被分发到Activity或Window对象,然后依次传递给ViewGroup和View。** 2. **拦截事件:在ViewGroup中,可以通过重写onInterceptTouchEvent方法来拦截事件。如果ViewGroup拦截了事件,则该事件不会再传递给子View,否则将传递给子View。** 3. **处理事件:在View中,可以通过重写onTouchEvent方法来处理事件。如果View处理了事件,则该事件不会再传递给父View,否则将传递给父View。** **GPT3:** **Android中的事件分发机制是指系统如何将用户触摸事件分发给正确的View进行处理的过程。事件分发机制涉及到了View的层级关系和事件传递的流程,是实现用户交互的关键之一。** **在Android中,事件分发机制的基本流程如下:** 1. **事件产生:当用户在屏幕上触摸时,系统会将用户的触摸事件封装成一个MotionEvent对象。** 2. **事件分发:系统会将MotionEvent事件传递给最上层的View,即顶层容器ViewGroup,然后由ViewGroup沿着View树向下遍历,依次将事件传递给各个子View,直到找到最合适的View来处理事件。** 3. **事件处理:当一个View接收到事件后,它会先进行事件处理,并决定是否消耗该事件。如果View消耗了事件,则事件不会再传递给其他View;否则,事件会继续向上传递,直到找到消耗事件的View或事件被取消为止。** 4. **事件拦截:在事件分发过程中,如果某个ViewGroup需要拦截事件,则可以通过重写onInterceptTouchEvent()方法来实现拦截。当ViewGroup拦截了事件后,事件会直接传递给该ViewGroup的onTouchEvent()方法进行处理,而不会继续传递给其子View。** **在事件分发机制中,View的层级关系和事件的传递流程是非常重要的。开发者需要了解各个View的层级关系和事件处理方式,以确保事件能够正确地传递和处理。同时,开发者也可以通过重写onTouchEvent()方法和onInterceptTouchEvent()方法,来实现自定义的事件处理和拦截逻辑。** ## [消息机制](https://www.cnblogs.com/xyhuangjinfu/p/5433015.html) **消息机制的主要用途有两方面:** ** 1、****线程之间的通信**。比如在子线程中想更新UI,就通过发送更新消息到UI线程中来实现。 ** 2、****任务延迟执行**。比如30秒后执行刷新任务等。 ** Android消息机制的framework层主要围绕Handler、Looper、Message、MessageQueue这四个对象来操作。消息机制主要是对消息进行生成、发送、存储、分发、处理等操作。** ### Message: ** 该类代表的是消息机制中的消息对象。是在消息机制中被创建,用来传递数据以及操作的对象,也负责维护消息对象缓存池。** **Message对象中主要有以下几个属性:** **what:消息类型。** **arg1、arg2、obj、data:该消息的数据域** **when:该消息应该被处理的时间,该字段的值为SystemClock.uptimeMillis()。当该字段值为0时,说明该消息需要被放置到消息队列的首部。** **target:发送和处理该消息的Handler对象。** **next:对象池中该消息的下一个消息。** ** Message对象中,主要维护了一个Message对象池,因为系统中会频繁的使用到Message对象,所以用对象池的方式来减少频繁创建对象带来的开支。Message对象池使用单链表实现。最大数量限制为50。所以官方推荐我们通过对象池来获取Message对象。** ** 特别注意的是,我们平常使用的都是普通的Message对象,也就是同步的Message对象。其实还有两种特殊的Message对象,目前很少被使用到,但也有必要了解一下。** ** 第一个是同步的障碍消息(Barrier Message),该消息的作用就是,如果该消息到达消息队列的首部,则消息队列中其他的同步消息就会被阻塞,不能被处理。障碍消息的特征是target==null&&arg1==barrierToken** ** 第二个是异步消息,异步消息不会被上面所说的障碍消息影响。通过Message对象的setAsynchronous(boolean async)方法来设置一个消息为异步消息。** ### MessageQueue: ** 该类代表的是消息机制中的消息队列。它主要就是维护一个线程安全的消息队列,提供消息的入队、删除、以及阻塞方式的轮询取出等操作。** ### Looper: ** 该类代表的是消息机制中的消息分发器。 有了消息,有了消息队列,还缺少处理消息分发机制的对象,Looper就是处理消息分发机制的对象。它会把每个Message发送到正确的处理对象上进行处理。如果一个Looper开始工作后,一直没有消息处理的话,那么该线程就会被阻塞。在非UI线程中,这时候应该监听当前MessageQueue的Idle事件,如果当前有Idle事件,则应该退出当前的消息循环,然后结束该线程,释放相应的资源。** ### Handler: ** 该类代表的是消息机制中的消息发送和处理器。有了消息、消息队列、消息分发机制,还缺少的就是消息投递和消息处理。Handler就是用来做消息投递和消息处理的。Handler事件处理机制采用一种按自由度从高到低的优先级进行消息的处理,正常情况下,一个Handler对象可以设置一个callback属性,一个Handler对象可以操作多个Message对象,从某种程度上来说,创建一个Message对象比给一个Handler对象设置callback属性来的自由,而给一个Handler对象设置callback属性比衍生一个Handler子类来的自由,所以消息处理优先级为Message>Handler.callback.Handler.handleMessage()。** ## 广播类型 **在Android中,广播(Broadcast)是一种轻量级的消息传递机制,用于在应用程序组件之间传递消息。广播可以在应用程序内部传递,也可以在不同应用程序之间传递。Android系统预定义了一些广播类型,常见的广播类型包括以下几种:** 1. **标准广播(Normal Broadcast):标准广播是一种完全异步执行的广播,所有接收器几乎同时接收到广播消息。标准广播是最常见的广播类型,也是最快速和最高效的方式来传递消息。** 2. **有序广播(Ordered Broadcast):有序广播是一种同步执行的广播,所有接收器按优先级顺序接收广播消息,并且可以有机会修改广播内容。有序广播可以被中断,这意味着优先级低的接收器可以阻止优先级高的接收器接收广播消息。** 3. **粘性广播(Sticky Broadcast):粘性广播是一种特殊类型的广播,它可以在广播发送后被延迟接收。当应用程序启动时,它可以通过getStickyBroadcast()方法获取最后一次发送的广播消息。** 4. **本地广播(Local Broadcast):本地广播是一种只在应用程序内部传递的广播,不会离开应用程序的进程。本地广播比标准广播更快速、更高效、更安全,因为它不需要考虑安全性和隐私问题。** **总的来说,在Android中广播是一种非常重要的机制,可以在应用程序组件之间传递消息,并且可以跨应用程序传递消息。不同的广播类型具有不同的特点和适用场景,开发者需要根据具体的业务需求选择合适的广播类型。** ### 有序广播如何实现的按顺序收到广播 **有序广播(Ordered Broadcast)是一种同步执行的广播,所有接收器按优先级顺序接收广播消息,并且可以有机会修改广播内容。有序广播可以被中断,这意味着优先级低的接收器可以阻止优先级高的接收器接收广播消息。** **有序广播的实现机制是通过广播接收器的优先级和abortBroadcast()方法来实现的。当一个广播被发送时,系统会根据接收器的优先级和注册顺序来确定接收器的执行顺序。具体来说,系统会按照接收器的优先级从高到低的顺序依次调用接收器的onReceive()方法,同时将BroadcastReceiver.PendingResult对象传递给每个接收器。接收器可以使用PendingResult对象来设置广播的处理结果,以及中止广播的传递。** **当一个接收器调用abortBroadcast()方法时,它会阻止低优先级的接收器接收广播消息,从而中断广播的传递。此时,广播的处理过程会在abortBroadcast()方法处停止,不再传递给优先级低的接收器。** **总的来说,有序广播的实现机制是通过广播接收器的优先级和abortBroadcast()方法来实现的。开发者可以根据业务需求设置接收器的优先级,并在接收器中使用PendingResult对象来设置广播的处理结果和中止广播的传递。** ## 持久化存储 **在Android应用程序中,持久化存储是指将数据保存到设备的存储介质中,使得即使应用程序关闭或设备重启,数据仍然可以被恢复。Android提供了多种持久化存储方式,常见的包括以下几种:** 1. **文件存储:可以使用FileInputStream和FileOutputStream等API读写文件。文件存储适用于保存一些简单的数据,如文本文件、图片、音频、视频等,但是不适合用于保存大量的结构化数据。** 2. **SharedPreferences:SharedPreferences是一种轻量级的存储方式,它可以存储简单的键值对数据。SharedPreferences存储的数据会被保存在应用程序的私有目录中,可以被其他组件访问,但只能被应用程序本身修改。** 3. **SQLite数据库:SQLite是一种轻量级的关系型数据库,它提供了SQL语言的支持,可以存储和查询结构化数据。SQLite数据库适用于保存大量的结构化数据,如联系人、日程、设置等。** 4. **Content Provider:Content Provider是Android提供的一种跨进程访问数据的机制,它可以对外提供数据的查询、插入、更新、删除等操作。Content Provider适用于需要将数据暴露给其他应用程序访问的场景,如联系人、音乐、图片等。** 5. **Room数据库:Room是Android提供的一种基于SQLite的持久化存储框架,它提供了注解和编译时类型检查等功能,可以简化SQLite数据库的操作。** ### SharedPreferences的格式 **SharedPreferences是Android提供的一种轻量级的持久化存储方式,可以用来存储简单的键值对数据。SharedPreferences存储的数据会被保存在应用程序的私有目录中,可以被其他组件访问,但只能被应用程序本身修改。SharedPreferences数据的格式如下:** 1. **文件名:SharedPreferences数据以XML文件的形式保存在设备的私有目录中,文件名是由SharedPreferences的名称和.xml后缀组成的,如my_preferences.xml。** 2. **节点:SharedPreferences数据以节点的形式保存在XML文件中,每个节点代表一个键值对。节点的名称是键的名称,节点的值是键对应的值。** 3. **属性:每个节点可以包含多个属性,每个属性代表一个键值对。属性的名称是**“**value**”**,属性的值是键对应的值。** **例如,以下是一个保存了两个键值对数据的SharedPreferences XML文件的格式:** ``` <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="key1">value1</string> <string name="key2">value2</string> </map> ``` **其中,键名为key1和key2,键值分别为value1和value2。节点的名称是字符串类型的键名,节点的值是字符串类型的键值,属性**“**value**”**的值也是字符串类型的键值。** ## 启动流程 **Android启动流程可以分为四个阶段:** 1. **Bootloader阶段:当用户按下设备的电源键时,设备会进入Bootloader模式,启动Bootloader程序,该程序负责初始化硬件和加载操作系统内核。** 2. **内核初始化阶段:内核启动后,会进行硬件初始化和文件系统的挂载,然后启动init进程。init进程会读取init.rc文件,启动Zygote进程和System Server进程,这两个进程是Android应用的核心进程。** 3. **Zygote进程阶段:Zygote进程是一个轻量级的进程,它负责预加载Java类和资源,并在接收到应用程序启动请求时,fork出新的进程来运行应用程序。这样,就可以在应用程序启动时,减少启动时间和占用的系统资源。** 4. **应用程序启动阶段:当用户启动应用程序时,Android系统会启动新的进程并加载应用程序的代码和资源。在应用程序启动后,ActivityManagerService会负责管理和调度应用程序,保证应用程序能够正常运行。** **总之,Android启动流程主要包括Bootloader阶段、内核初始化阶段、Zygote进程阶段和应用程序启动阶段。在这个过程中,系统会完成硬件初始化、文件系统挂载、Java类和资源的预加载、进程的fork和管理等一系列操作,最终将应用程序加载到内存中,并保证其正常运行。** ## 类加载 **Android类加载器是负责加载Java类和资源的组件。在Android中,类加载器主要分为四种类型:系统类加载器、应用程序类加载器、扩展类加载器和引导类加载器。** 1. **引导类加载器:负责加载Java平台核心库,包括java.lang、java.util等基础类,通常使用C++实现,并由虚拟机实现。** 2. **扩展类加载器:负责加载Java的扩展库,通常使用Java实现,从Java扩展目录加载类。** 3. **系统类加载器:负责加载系统中的类和资源,从系统classpath目录加载类,同时也能从用户classpath目录加载类。** 4. **应用程序类加载器:负责加载应用程序中的类和资源,从应用程序classpath目录加载类,通常是dex文件。** **Android类加载器使用委派模型,即当类加载器需要加载某个类时,先委托其父类加载器加载该类,如果父类加载器无法加载该类,则由该类加载器自行加载。这种机制可以避免类的重复加载,并保证类的唯一性。** **总之,Android类加载器是负责加载Java类和资源的组件,主要包括引导类加载器、扩展类加载器、系统类加载器和应用程序类加载器四种类型。在类加载器加载类时,采用了委派模型,保证类的唯一性和避免重复加载。** ## 性能分析&性能优化 **android 性能主要之响应速度 和UI刷新速度。** **可以参考博客:Android系统性能调优工具介绍** **首先从函数的耗时来说,有一个工具TraceView 这是androidsdk自带的工作,用于测量函数耗时的。** **UI布局的分析,可以有2块,一块就是Hierarchy Viewer 可以看到View的布局层次,以及每个View刷新加载的时间。** **这样可以很快定位到那块layout & View 耗时最长。** **还有就是通过自定义View来减少view的层次。** **是的,我了解 Android 的性能优化,以下是我的简单介绍和实践经验:** 1. **内存优化:在 Android 应用中,内存管理是一个非常重要的问题。为了减少内存占用和提高应用的响应速度,我通常会采用以下几种方式进行内存优化:使用轻量级的数据结构和算法,避免内存泄漏和重复的对象创建和销毁,及时释放无用资源和对象等。** 2. **布局优化:在 Android 应用中,布局是影响应用性能和响应速度的一个重要因素。为了提高布局的渲染速度,我通常会采用以下几种方式进行布局优化:避免使用过于复杂的布局,减少布局嵌套,使用 ConstraintLayout 等高性能的布局方式,使用 ViewStub 来延迟加载布局等。** 3. **图片优化:在 Android 应用中,图片是一个比较占用内存和资源的元素。为了提高应用的响应速度和性能,我通常会采用以下几种方式进行图片优化:使用合适大小和格式的图片,压缩图片文件大小,使用图片缓存和延迟加载等。** 4. **网络优化:在 Android 应用中,网络请求是一个比较消耗资源和时间的操作。为了提高应用的网络性能和响应速度,我通常会采用以下几种方式进行网络优化:使用异步线程或协程来处理网络请求,合理设置网络请求超时时间和重试次数,使用 HTTP 缓存和网络请求队列等。** 5. **动画优化:在 Android 应用中,动画是一个比较占用 CPU 和 GPU 资源的元素。为了提高应用的动画性能和流畅度,我通常会采用以下几种方式进行动画优化:使用属性动画代替视图动画,减少动画时间和频率,避免过于复杂的动画效果,使用硬件加速等。** **综上所述,Android 的性能优化需要综合考虑应用的不同方面和环节,采用不同的优化方式和策略。在实践中,我通常会针对应用的性能问题进行分析和优化,同时也会关注 Android 系统的最新优化技术和工具,如 Systrace、Traceview、Profiler 等,以便更好地优化应用性能和用户体验。** ## dalvik虚拟机&Art虚拟机 **Dalvik虚拟机是Android系统在4.4版本及以下使用的虚拟机,而ART虚拟机是从Android 5.0版本开始引入的。相较于Dalvik虚拟机,ART虚拟机有许多优化升级点。** **以下是一些主要的优化升级点:** 1. **AOT编译:Dalvik虚拟机在运行应用程序时会先将应用程序的.dex文件解释成机器码,这个过程是即时编译(JIT)的过程。而ART虚拟机则采用AOT(Ahead-Of-Time)编译的方式,在应用安装的时候就将.dex文件编译成本地机器码,这样可以避免运行时的解释和编译,从而提高应用程序的启动速度和运行效率。** 2. **垃圾回收(GC)优化:ART虚拟机采用了更加高效的垃圾回收算法,例如并发标记清除(concurrent mark and sweep)和增量式垃圾回收(incremental garbage collection),可以在运行时动态地调整垃圾回收的策略,提高垃圾回收的效率和响应速度。** 3. **内存管理优化:ART虚拟机还引入了一种叫做“Zygote”的进程启动机制,这种机制可以通过预加载共享库和类信息,来减少应用程序的内存占用。此外,ART还支持透明压缩,这可以减少应用程序的内存占用,提高系统的整体性能。** 4. **异常处理优化:ART虚拟机的异常处理机制比Dalvik虚拟机更加高效,可以快速地捕获和处理异常,从而减少因异常处理而引起的性能瓶颈。** 5. **应用程序启动速度优化:ART虚拟机可以在应用程序安装时将其编译成本地机器码,这样可以大大减少应用程序的启动时间,提高用户体验。** **总体来说,ART虚拟机在应用程序性能、内存管理、垃圾回收、异常处理等方面都进行了一系列的优化,相较于Dalvik虚拟机,能够提供更加出色的性能和用户体验。** ## 内存抖动&内存泄露 **内存抖动(Memory Jitter)**指的是频繁的内存分配和回收,导致系统产生了大量的垃圾对象,从而导致系统性能下降和内存使用效率低下的现象。内存抖动会导致系统产生大量的GC(Garbage Collection)操作,从而影响应用程序的响应速度和性能。 **内存泄漏(Memory Leak)**指的是内存中的对象没有被垃圾回收器回收,导致这些对象一直存在于内存中,从而占用了系统的内存资源。内存泄漏会导致系统的内存占用逐渐增加,最终导致系统崩溃或者应用程序异常终止。 **1.静态集合类引起内存泄露** **主要是hashmap,Vector等,如果是静态集合 这些集合没有及时setnull的话,就会一直持有这些对象。** **2.remove 方法无法删除set集Objects.hash(firstName, lastName)经过测试,hashcode修改后,就没有办法remove了。** **3.observer 我们在使用监听器的时候,往往是addxxxlistener,但是当我们不需要的时候,忘记removexxxlistener,就容易内存leak。** **广播没有unregisterrecevier** **4.各种数据链接没有关闭,数据库contentprovider,io,sokect等。cursor** **5.内部类:** **java中的内部类(匿名内部类),会持有宿主类的强引用this。** **所以如果是new Thread这种,后台线程的操作,当线程没有执行结束时,activity不会被回收。** **Context的引用,当TextView 等等都会持有上下文的引用。如果有static drawable,就会导致该内存无法释放。** **6.单例** **单例 是一个全局的静态对象,当持有某个复制的类A是,A无法被释放,内存leak。** **处理** 1. **避免静态变量持有 Context:静态变量持有 Context 是常见的内存泄漏原因之一。因此,在使用静态变量时,应尽量避免将 Context 作为静态变量的成员变量。** 2. **及时释放资源:在使用一些需要占用大量内存的资源时,如 Bitmap、IO 流等,应该及时释放资源,避免造成内存泄漏。比如,在使用 Bitmap 时,应该及时调用 recycle() 方法释放内存。** 3. **使用弱引用和软引用:弱引用和软引用是 Java 中用于避免内存泄漏的机制。通过使用弱引用和软引用来管理对象,可以避免对象被持有太久而导致内存泄漏。比如,在使用 Handler 时,可以使用弱引用持有 Activity,避免 Handler 导致 Activity 的内存泄漏。** 4. **使用 LeakCanary 检测内存泄漏:LeakCanary 是一个常用的内存泄漏检测库,在应用开发过程中可以使用 LeakCanary 来检测应用中的内存泄漏问题。LeakCanary 会在应用中检测到内存泄漏时发出警告,并提供详细的调用栈信息和解决方案。** **两者的区别:** **内存抖动是由于频繁的内存分配和回收所导致的,而内存泄漏是由于对象没有被垃圾回收器回收所导致的。解决内存抖动的方法是尽量减少频繁的内存分配和回收,可以使用对象池、避免创建不必要的对象等方法来优化内存使用。解决内存泄漏的方法是找到泄漏的对象,并释放它们所占用的内存资源,可以使用工具类如MAT(Memory Analyzer Tool)来进行内存泄漏的检测和排查。** ## OOM异常 **当程序需要申请一段“大”内存,但是虚拟机没有办法及时的给到,即使做了GC操作以后** **这就会抛出 OutOfMemoryException 也就是OOM** **如何避免OOM** **减少内存对象的占用** **1.ArrayMap/SparseArray代替hashmap** **2.避免在android里面使用Enum** **3.减少bitmap的内存占用** ``` inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。 decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。 ``` **4.减少资源图片的大小,过大的图片可以考虑分段加载** **内存对象的重复利用** **大多数对象的复用,都是利用对象池的技术。** **1.listview/gridview/recycleview contentview的复用** **2.inBitmap 属性对于内存对象的复用ARGB_8888/RBG_565/ARGB_4444/ALPHA_8** **这个方法在某些条件下非常有用,比如要加载上千张图片的时候。** **3.避免在ondraw方法里面 new对象** **4.StringBuilder 代替+** ## 捕获异常 **CrashHandler** **关键是实现Thread.UncaughtExceptionHandler** **然后是在application的oncreate里面注册。** ## ANR **ANR->Application Not Responding** **也就是在规定的时间内,没有响应。** ### 三种类型: **1). KeyDispatchTimeout(5 seconds) —主要类型按键或触摸事件在特定时间内无响应** **2). BroadcastTimeout(10 seconds) —BroadcastReceiver在特定时间内无法处理完成** **3). ServiceTimeout(20 seconds) —小概率类型 Service在特定的时间内无法处理完成** ### **什么时候出现** 1. **主线程被阻塞:如果应用程序中某个操作需要大量时间才能完成,而这个操作又在主线程中执行,那么主线程就会被阻塞,导致应用程序无响应。** 2. **死锁:如果应用程序中存在多个线程,并且这些线程之间存在互相等待的情况,那么就会导致死锁,从而导致应用程序无响应。** 3. **数据库访问阻塞:如果应用程序中有大量的数据库访问操作,而这些操作又在主线程中执行,那么就会导致主线程被阻塞,从而导致应用程序无响应。** ### **如何排查** 1. **查看ANR日志:当应用程序出现ANR时,系统会自动生成ANR日志,可以通过adb命令获取到该日志,从而查看具体的ANR原因。ANR日志通常包括当前线程状态、线程堆栈等信息,可以根据这些信息来确定ANR的原因。** 2. **使用TraceView工具:可以使用TraceView工具来分析应用程序的性能问题,包括ANR问题。TraceView可以帮助开发者分析应用程序中每个线程的执行时间和方法调用,从而确定是否存在性能瓶颈和ANR问题。** 3. **使用第三方工具:可以使用一些第三方工具来帮助排查ANR问题,如Monkey、Systrace、StrictMode等。这些工具可以监控应用程序的性能和响应时间,并帮助开发者定位性能问题和ANR问题。** ### **怎么避免ANR** **ANR(Application Not Responding)问题通常是由于主线程被阻塞或死锁等情况引起的,因此要避免ANR问题,可以采取以下措施:** 1. **尽量不要在主线程中执行耗时操作:主线程是应用程序的UI线程,负责处理UI事件和响应用户操作。如果在主线程中执行耗时操作,就会导致主线程被阻塞,从而引起ANR问题。因此,可以使用异步线程或线程池来执行耗时操作,从而避免阻塞主线程。** 2. **使用Handler和Looper机制:可以使用Handler和Looper机制来实现异步消息处理,从而避免阻塞主线程。通过Handler和Looper机制,可以将耗时操作放在独立的线程中执行,然后通过消息机制将执行结果返回到主线程中更新UI。** 3. **使用IntentService:可以使用IntentService来处理耗时操作,它会在独立的线程中自动执行任务,并在任务执行完毕后自动停止。使用IntentService可以避免在主线程中执行耗时操作,提高应用程序的响应速度和稳定性。** 4. **优化数据库访问:数据库访问通常是耗时操作之一,可以通过优化数据库访问方式来避免ANR问题。例如,可以使用异步查询、批量操作等方式来优化数据库访问效率,减少主线程被阻塞的时间。** **总的来说,要避免ANR问题,需要尽可能避免在主线程中执行耗时操作,并使用异步线程、Handler和Looper机制、IntentService等方式来处理耗时操作。同时,也需要注意优化数据库访问、避免死锁等问题,以提高应用程序的性能和稳定性。** ### **ANR的关键** **是处理超时,所以应该避免在UI线程,BroadcastReceiver 还有service主线程中,处理复杂的逻辑和计算而交给work thread操作。** **1)避免在activity里面做耗时操作,oncreate & onresume** **2)避免在onReceiver里面做过多操作** **3)避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。** **4)尽量使用handler来处理UI thread & workthread的交互。** ## wait()、sleep()、yield()、join() **在Java中,wait()、sleep()、yield()和join()都是线程的控制方法,它们的作用和用法略有不同:** 1. **wait()方法:让当前线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒它。wait()方法通常在synchronized块内使用。** 2. **sleep()方法:让当前线程进入休眠状态,指定的时间到了后自动唤醒。sleep()方法可以在任何地方使用。** 3. **yield()方法:让当前线程让出CPU的使用权,使得其他线程有机会运行。但是,调用yield()方法并不保证让其他线程运行,它只是给其他线程一个运行的机会。** 4. **join()方法:让当前线程等待其他线程执行完毕后再继续执行。join()方法通常在子线程内部使用。** **总的来说,wait()、sleep()、yield()和join()都是线程控制方法,它们的作用和用法有所不同。wait()和notify()方法通常在同步块内使用,用于实现线程之间的同步;sleep()方法通常在需要暂停一段时间后再执行的地方使用;yield()方法通常用于提高多线程程序的效率;join()方法通常在需要等待其他线程执行完毕后再执行的地方使用。** **需要注意的是,这些方法都可能会抛出InterruptedException异常,因此在使用它们时需要注意异常处理。同时,也需要注意避免在不恰当的地方使用它们,否则可能会导致线程不正常的运行或性能下降。** ## MessageQueue **是Android系统中用于存储和处理异步消息的数据结构。MessageQueue是一个基于链表的队列,用于存储待处理的消息。** **MessageQueue的基本原理是先进先出(FIFO),即先加入队列的消息先被处理。当消息被加入MessageQueue时,它会被封装成一个Message对象,并添加到队列尾部。当Looper不断地从MessageQueue中取出消息并进行处理时,消息会被从队列头部取出并传递给对应的Handler进行处理。** **MessageQueue支持线程安全,因此它可以被多个线程同时访问。在Android系统中,每个线程都有一个对应的Looper对象和MessageQueue对象,用于处理该线程中的消息。** **总之,MessageQueue是Android系统中用于存储和处理异步消息的数据结构,是一个基于链表的队列,支持线程安全。它的基本原理是先进先出,用于实现线程间通信和异步消息处理。** ## Handle原理 **Handle是Android中的一个线程间通信机制,用于实现异步消息的发送和处理。Handle的原理是通过MessageQueue和Looper来实现的。** 1. **MessageQueue:MessageQueue是一个消息队列,用于存储待处理的消息。当消息被发送到Handle时,它会被添加到MessageQueue中。** 2. **Looper:Looper是一个消息循环器,用于不断地从MessageQueue中取出消息,并将其分发到对应的Handler中。** 3. **Handler:Handler是消息处理器,用于处理从MessageQueue中取出的消息。当消息被处理后,Handler会将消息从MessageQueue中移除。** **通过这样的方式,Handle可以实现在不同线程之间传递消息,从而实现异步通信的功能。** ## 线程间通信 1. **共享变量:可以通过共享变量在多个线程间进行通信。在多线程编程中,共享变量需要使用锁机制进行同步,以保证多个线程对共享变量的访问是互斥的,避免出现数据竞争等问题。** 2. **等待通知机制:可以使用等待通知机制来实现线程间的协调。通过wait()方法使线程等待,直到其他线程发出通知(notify()或notifyAll()方法)后再进行相应的处理。** 3. **信号量:信号量是一种用于线程间同步的机制,可以用来控制多个线程同时访问某个资源的情况。可以使用Semaphore类来实现信号量,Semaphore提供了acquire()和release()方法,可以分别用来获取和释放信号量。** 4. **管道:管道是一种在两个线程间进行通信的机制,其中一个线程写入数据到管道中,另一个线程从管道中读取数据。在Java中,可以使用PipedInputStream和PipedOutputStream类来实现管道通信。** 5. **消息队列:可以使用消息队列来实现线程间的通信。线程通过向消息队列中发送消息来进行通信,其他线程则从消息队列中获取消息。在Java中,可以使用BlockingQueue类或ConcurrentLinkedQueue类来实现消息队列。** **总的来说,线程间通信需要使用适当的同步机制来保证多个线程之间的协调和安全。不同的线程间通信方式各有优缺点,开发者需要根据具体需求和场景选择合适的线程间通信方式。** **Android** 1. **Handler机制:Handler机制是安卓中常用的一种线程通信方式。通过Handler,我们可以将消息发送到指定的线程,并在该线程中处理消息。可以使用Handler的post方法发送消息,使用sendMessage方法发送带有延时的消息。另外,Handler还可以用来更新UI界面。** 2. **AsyncTask:AsyncTask是安卓中专门用于异步任务处理的类。通过AsyncTask,我们可以在后台线程中执行耗时的操作,然后将结果传递给UI线程。AsyncTask封装了Handler机制,使得我们可以轻松地进行线程通信。** 3. **runOnUiThread方法:runOnUiThread是Activity中的一个方法,可以在UI线程中执行指定的代码块。通过该方法,我们可以在后台线程中执行耗时的操作,然后将结果传递给UI线程,以便更新UI界面。** 4. **Broadcast Receiver:Broadcast Receiver是安卓中的一种广播接收器,可以在不同的组件之间传递消息。通过Broadcast Receiver,我们可以在一个线程中发送广播,然后在另一个线程中接收广播,从而实现线程之间的通信。** 5. **EventBus:EventBus是一种安卓中常用的事件总线框架,可以实现组件之间的解耦和通信。通过EventBus,我们可以在一个线程中发布事件,然后在另一个线程中订阅事件,从而实现线程之间的通信。** **需要根据具体情况选择不同的线程通信方式,以实现应用程序的需求。** ## 多线程间通信&多进程之间通信 **多线程间通信**: **在Java中,多线程间通信可以通过wait()、notify()和notifyAll()方法来实现。这些方法都是Object类中定义的方法,可以用于控制线程的等待和唤醒。具体来说,wait()方法可以让线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒它。notify()方法可以随机唤醒一个等待的线程,而notifyAll()方法可以唤醒所有等待的线程。这些方法需要在同步块内使用,并且必须使用synchronized关键字来获取对象锁。** **多进程之间通信**: **在Android中,多进程之间通信可以通过以下几种方式来实现:** 1. **Intent:可以使用Intent在不同的进程之间传递数据,但是它只适用于少量的数据,而且不适合频繁的数据交换。** 2. **ContentProvider:ContentProvider可以在不同的进程之间共享数据,它可以提供数据访问的接口,并且可以保证数据的安全性和一致性。** 3. **Binder:Binder是一种Android中的IPC机制,可以用于不同进程之间的通信。Binder的工作原理是将一个对象转换成一个能够跨进程传输的代理对象,从而实现进程间通信。** 4. **Messenger:Messenger是一种基于Binder的进程间通信机制,它可以让不同进程之间发送和接收消息,但是它只适合于传递少量的数据。** 5. **AIDL:AIDL是Android Interface Definition Language的缩写,它是一种基于Binder的进程间通信机制。通过定义接口和方法,可以实现不同进程之间的数据交换。** **需要注意的是,多进程之间的通信比较耗费系统资源,因此在使用时需要谨慎考虑。另外,需要注意保护数据的安全性和一致性,避免出现数据冲突和竞争问题。** **进程间的通信:bind机制(IPC->AIDL),linux级共享内存,boradcast,** **Activity 之间,activity & serview之间的通信,无论他们是否在一个进程内。** ## 线程池 **线程池是一种用于管理和调度多个线程的技术,Android提供了ThreadPoolExecutor类来实现线程池。ThreadPoolExecutor是Java标准库中的一个类,它提供了多种线程池的实现方式,并且可以通过自定义参数来控制线程池的大小和行为。** **ThreadPoolExecutor的实现原理如下:** 1. **线程池的核心参数:ThreadPoolExecutor主要包含四个参数:核心线程数、最大线程数、任务队列和线程空闲时间。线程池会根据这些参数来控制线程池中线程的数量和执行任务的行为。** 2. **线程池中的线程:当线程池被创建后,线程池中会创建一些核心线程,这些线程会一直保持存活状态,等待任务的到来。如果任务数量超过了核心线程数,线程池会创建新的线程,直到线程数量达到最大线程数。** 3. **任务队列:当任务数量超过了核心线程数,但是线程数量未达到最大线程数时,新的任务会被添加到任务队列中。任务队列采用先进先出的策略,等待线程来执行任务。** 4. **线程池的拒绝策略:当任务数量超过了最大线程数并且任务队列已满时,线程池会采用拒绝策略来处理新的任务。线程池提供了多种拒绝策略,例如直接抛弃任务、抛出异常、在调用者线程中执行等。** 5. **线程池的关闭:线程池在不再需要时需要进行关闭操作。线程池提供了shutdown()方法和shutdownNow()方法来关闭线程池。shutdown()方法会等待线程池中所有的任务都执行完毕后关闭线程池,而shutdownNow()方法会立即关闭线程池,可能会丢失一些尚未执行的任务。** **总的来说,线程池可以有效地管理多个线程,优化应用程序的性能和资源利用率。但是需要根据具体的应用场景和任务特点,合理地设置线程池的参数,避免线程池大小设置不当导致的性能问题。** ## Devik 进程&linux 进程&线程 **Dalvik进程。** **每一个android app都会独立占用一个dvm虚拟机,运行在linux系统中。** **所以dalvik进程和linux进程是可以理解为一个概念。** ## 系统架构 **Android 是一个基于 Linux 内核的操作系统,它包含多个层次的组件,这些组件共同构成了 Android 的体系结构。以下是 Android 的架构组成部分:** 1. **应用层:应用层是用户和 Android 系统交互的最高层,它包含了各种应用程序,如浏览器、邮件客户端、社交网络应用等。** 2. **应用框架层:应用框架层提供了应用程序开发所需的各种服务和 API,如界面构建、多媒体支持、数据存储等,开发人员可以使用这些服务和 API 来构建应用程序。** 3. **系统运行库层:系统运行库层提供了许多底层服务和 API,如 SQLite 数据库、OpenGL ES 图形库、WebKit 渲染引擎等,这些服务和 API 可以为应用程序提供底层支持。** 4. **硬件抽象层:硬件抽象层是 Android 系统与底层硬件之间的接口层,它为不同的硬件平台提供了统一的接口,让 Android 系统能够在不同的硬件平台上运行。** 5. **Linux 内核层:Linux 内核层是 Android 系统的核心部分,它提供了许多底层功能,如内存管理、进程管理、安全性等。同时,它还提供了许多驱动程序,如 Wi-Fi 驱动、蓝牙驱动、摄像头驱动等,这些驱动程序可以让 Android 系统与硬件平台进行通信。** **这是一个简单的 Android 架构概述,每个层级都有不同的组件和功能。了解 Android 架构可以帮助开发人员更好地理解 Android 系统,并为他们构建高质量的应用程序提供指导。** **从小到上就是:** **linux kernel,lib dalvik vm ,application framework, app** ## 内存限制 **activitymanager.getMemoryClass() 获取内存限制。** ## 数据存储 **在我的项目中,我通常会采用不同的数据管理和处理方案来满足应用的需求和性能要求,包括以下几个方面:** 1. **数据库管理:对于需要存储大量结构化数据的应用,我通常会采用 SQLite 数据库来管理数据。在 Android 应用中,可以使用 Android 自带的 SQLiteOpenHelper 类或第三方库(如 Room)来进行数据库管理。我会使用数据库的事务机制来保证数据的完整性和一致性,同时也会对查询语句进行优化,以提高数据库的查询效率和响应速度。** 2. **缓存管理:对于需要频繁读取和更新的数据,我通常会采用缓存来优化数据的访问速度和响应性能。在 Android 应用中,可以使用 SharedPreferences、DiskLruCache、LRU Cache 等方式来实现数据的缓存。我会根据数据的更新频率和大小,选择合适的缓存方案,并在应用的不同模块中使用缓存来优化数据访问。** 3. **内存管理:在应用的运行过程中,内存管理是一个比较重要的问题。为了减少内存使用和提高应用的响应性能,我通常会使用轻量级的数据结构来存储数据,如 ArrayList、SparseArray 等。同时,我也会尽量避免使用静态变量和匿名内部类等容易引起内存泄漏的方式,确保应用的稳定性和可靠性。** **综上所述,我在我的项目中通常会使用 SQLite 数据库、缓存和内存管理等方案来管理和处理数据。具体采用哪种方案,取决于应用的需求和性能要求,以及开发团队的技能和习惯。** ## 程序结构 **1)main code** ``` unit test ``` **3)mianifest** **4)res->drawable,drawable-xxhdpi,layout,value,mipmap** **mipmap 是一种很早就有的技术了,翻译过来就是纹理映射技术.** **google建议只把启动图片放入。** **5)lib** **6)color** ## 程序权限&文件系统权限 **文件的系统权限是由linux系统规定的,只读,读写等。** **运行时权限,是对于某个系统上的app的访问权限,允许,拒绝,询问。该功能可以防止非法的程序访问敏感的信息。** ## json&xml ### 区别 **JSON(JavaScript Object Notation)和XML(eXtensible Markup Language)都是常见的数据交换格式,用于在应用程序之间传输数据。它们之间的主要区别如下:** 1. **格式:JSON格式是一种轻量级的数据格式,使用简单的键值对和数组结构来描述数据。JSON的语法和JavaScript对象的语法类似,易于阅读和理解。而XML格式是一种基于标签的数据格式,使用尖括号和属性来描述数据。XML的语法较为复杂,需要学习和理解XML的规则和语法。** 2. **体积:JSON格式通常比XML格式更小,因为JSON使用了更少的标记和符号,相对来说更加紧凑,传输速度更快。** 3. **解析:JSON格式的解析速度通常比XML格式更快,因为JSON的数据结构较为简单,解析起来更加高效。** 4. **可读性:JSON格式相对于XML更加易于阅读和理解,因为JSON的语法和JavaScript对象的语法类似,而JavaScript是一种广泛使用的编程语言。** **总的来说,JSON和XML都是常见的数据交换格式,各有优缺点。在实际开发中,应根据具体的业务需求和场景选择合适的数据交换格式。通常情况下,JSON比XML更加轻量、更快速、更易于解析和阅读,因此在移动开发中,JSON是一种更为流行和广泛使用的数据交换格式。** ### xml解析方式 **在Android中,XML解析是一种常见的数据解析方式,可以用来解析XML格式的数据。Android提供了多种XML解析方式,常见的包括以下几种:** 1. **SAX解析:SAX是一种基于事件驱动的XML解析方式,它逐行解析XML文件,根据不同的事件类型触发相应的事件处理方法。SAX解析适用于解析大型XML文件,但需要编写大量的事件处理代码。** 2. **DOM解析:DOM是一种基于内存的XML解析方式,它将整个XML文档读入内存,构建一个XML树,并提供了丰富的API来操作XML节点。DOM解析适用于解析小型XML文件,但需要消耗大量的内存。** 3. **Pull解析:Pull是一种轻量级的XML解析方式,它采用类似游标的方式逐个读取XML节点,可以快速、高效地解析XML文件。Pull解析适用于解析任意大小的XML文件,但不支持事件处理。** **在实际开发中,开发者可以根据具体的需求选择合适的XML解析方式。例如,如果需要解析大型的XML文件,可以选择SAX解析;如果需要解析小型的XML文件,可以选择DOM解析;如果需要快速解析任意大小的XML文件,可以选择Pull解析。另外,Android也提供了一些第三方XML解析库,如XmlPullParser和Simple XML等,可以方便地解析XML格式的数据。** ## 屏幕适配(sp, dp, px) **屏幕适配的方式:xxxdpi, wrap_content,match_parent. 获取屏幕大小,做处理。** **dp来适配屏幕,sp来确定字体大小** **drawable-xxdpi, values-1280*1920等 这些就是资源的适配。** **wrap_content,match_parent, 这些是view的自适应** **weight,这是权重的适配。** **在Android中,sp、dp和px都是表示屏幕像素的单位,它们之间的区别如下:** 1. **px(Pixel):px是屏幕上的物理像素点,它是绝对单位,表示屏幕上的一个点。在不同的设备上,1px可能对应不同的物理像素点。在Android中,像素密度(dpi)越高的设备,每英寸包含的像素点就越多。** 2. **dp(Density-independent Pixel):dp是与屏幕像素密度无关的抽象单位,它表示屏幕上的一个长度。在Android中,1dp等于1/160英寸,因此可以通过dp单位来描述一些相对于屏幕尺寸的布局参数,如TextView的字体大小、View的宽度和高度等。** 3. **sp(Scale-independent Pixel):sp是与屏幕像素密度和字体缩放有关的抽象单位,它表示屏幕上的一个字体大小。在Android中,如果使用dp来设置字体大小,当字体缩放比例改变时,字体大小不会跟随改变。而如果使用sp来设置字体大小,则可以根据字体缩放比例进行相应的调整。** **总的来说,px是绝对单位,dp和sp是相对单位。在Android中,推荐使用dp和sp单位来描述布局和字体大小,以便适配不同的设备和屏幕密度。** ## 界面绘制原理 **Android界面绘制原理主要涉及三个核心组件:View、Canvas和Surface。** **View是Android界面上的基本组件,负责绘制和响应用户事件。View继承自Android的基础类——ViewGroup和ViewRootImpl,其中ViewGroup负责管理子View的布局和绘制,ViewRootImpl负责与系统Window Manager进行通信,将View绘制到屏幕上。** **Canvas是Android绘图的核心类,提供了绘制图形、文本、位图等基本操作的方法。Canvas的绘图操作最终会被转化为对Surface的操作。** **Surface是Android界面的底层渲染引擎,用于将绘制操作转换为实际的像素。Surface可以是一个Window、一个Bitmap或者一个Texture,其中Window是最常用的一种Surface类型,用于绘制到屏幕上。** **在Android中,View的绘制流程是通过ViewRootImpl中的performTraversals方法来完成的。该方法在View的绘制、测量、布局等操作完成后,调用View的draw方法将View绘制到Canvas上,最终将Canvas绘制到Surface上,完成界面的渲染。** **总的来说,Android界面绘制原理就是通过View、Canvas和Surface三个核心组件实现的,其中View负责管理和绘制界面上的各种组件,Canvas提供了基本的绘制方法,Surface将绘制操作转化为实际的像素,并将其渲染到屏幕上。** ## 常用的 UI 组件 1. **TextView:用于显示文本内容的组件,支持多种样式和格式。** 2. **EditText:用于用户输入文本的组件,支持自动补全、验证、格式化等功能。** 3. **Button:用于响应用户点击事件的组件,支持多种样式和形态。** 4. **ImageButton:用于响应用户点击事件的图像按钮组件,支持多种样式和形态。** 5. **ImageView:用于显示图像内容的组件,支持多种格式和动画效果。** 6. **ProgressBar:用于显示进度条的组件,支持不同形态和样式。** 7. **ListView:用于显示列表内容的组件,支持数据绑定、分页、滚动等功能。** 8. **RecyclerView:用于显示列表内容的高性能组件,支持自定义布局和交互效果。** 9. **GridView:用于显示网格内容的组件,支持多种布局和样式。** 10. **TabLayout:用于显示选项卡的组件,支持多种布局和交互效果。** 11. **Spinner:用于显示下拉列表的组件,支持自定义样式和数据绑定。** 12. **DatePicker:用于选择日期的组件,支持多种样式和格式。** 13. **TimePicker:用于选择时间的组件,支持多种样式和格式。** 14. **SeekBar:用于选择进度值的组件,支持多种样式和格式。** 15. **RatingBar:用于显示评分的组件,支持多种样式和交互效果。** **以上是常见的一些 Android UI 组件,实际开发中可能还会使用其他的组件和自定义视图来实现特定的功能和效果。** ## 非UI线程中更新UI 1. **ANR(Application Not Responding):如果在UI线程中执行耗时操作,应用程序就会停止响应,用户界面会出现卡顿和卡死现象,最终导致ANR。** 2. **线程安全问题:如果多个线程同时访问同一个UI控件,可能会导致UI控件状态的不一致或异常。比如,多个线程同时修改同一个TextView的文本,就可能会导致文本的混乱或丢失等问题。** 3. **异常:如果在非UI线程中访问UI控件,就可能会出现IllegalStateException等异常,因为UI控件只能在UI线程中被访问和操作。** **为了避免这些问题,安卓规定UI控件只能在UI线程中被访问和操作。如果需要在非UI线程中更新UI控件,可以使用Handler、AsyncTask等机制,将结果传递到UI线程中,然后在UI线程中更新UI控件。需要注意的是,在进行线程通信时,我们应该尽可能地避免在UI线程中执行耗时操作,以免造成UI界面的卡顿和卡死现象。** ## 机型大小适配 **在Android开发中,要实现机型大小适配,主要有以下几种方法:** 1. **使用dp和sp单位:使用dp和sp单位来描述布局和字体大小,可以适配不同的设备和屏幕密度。在布局文件中使用dp作为长度单位,使用sp作为字体大小单位,可以确保在不同的设备上显示一致。** 2. **使用布局限定符:Android提供了一些布局限定符,如size、screenDensity等,可以根据不同的设备特性进行布局适配。例如,可以使用size限定符来针对不同的屏幕尺寸提供不同的布局文件,使用screenDensity限定符来针对不同的屏幕密度提供不同的资源文件。** 3. **使用百分比布局:可以使用百分比布局来实现适配不同的屏幕尺寸。百分比布局是一种相对布局方式,可以根据不同的屏幕尺寸计算出布局参数的比例,从而实现不同屏幕尺寸的适配。** 4. **使用限制比例布局:Android 7.0及以上版本提供了限制比例布局(ConstraintLayout)功能,可以实现多个控件之间的比例关系,从而适配不同的屏幕尺寸。** 5. **使用自定义View:如果以上方法不能满足需求,还可以使用自定义View来实现屏幕适配。自定义View可以根据不同的屏幕尺寸计算出控件的位置和大小,从而实现适配不同的屏幕尺寸。** ## 适配语言 **在Android开发中,要实现多语言适配,可以采用以下几种方法:** 1. **使用系统资源:Android提供了一些系统资源来支持多语言,如strings.xml文件、drawable文件夹等。可以在不同的values文件夹中,为不同的语言提供相应的资源文件,系统会根据当前设备的语言环境自动加载相应的资源文件。** 2. **使用字符串格式化:在strings.xml文件中,可以使用%s、%d等格式符来代替文本或数字。在程序运行时,可以使用String.format()方法来将格式符替换成相应的值,从而实现多语言适配。** 3. **使用第三方框架:可以使用第三方框架来实现多语言适配,如Android Localization Helper、Android Localization、Multi-Language、LocalizationPlugin等。** 4. **自定义适配方案:如果以上方法不能满足需求,还可以采用自定义适配方案。可以在代码中定义一些静态字符串变量,通过读取配置文件或从网络获取相应的翻译文本,来替换这些静态字符串变量。** ## 屏幕渲染机制 **Android的屏幕渲染机制可以分为以下几个步骤:** 1. **触发渲染:当用户操作屏幕或者有应用程序在后台更新UI时,Android系统会触发渲染操作。** 2. **绘制视图:系统会按照视图树的结构,从根节点开始遍历每一个节点,调用每个节点的onDraw()方法绘制对应的视图。** 3. **生成绘图指令:系统会将每个视图的绘制操作转化为一系列绘图指令,称为“Display List”。** 4. **生成GPU命令:系统会将Display List转化为GPU能够理解的命令,称为“GPU Commands”。** 5. **执行GPU命令:GPU会根据GPU Commands执行绘图操作,将绘制结果渲染到屏幕上。** 6. **合成屏幕:系统会将多个应用程序的绘图结果进行合成,生成最终的屏幕显示结果。** **在这个过程中,还有一些优化策略可以提高屏幕渲染的效率,例如:** 1. **裁剪:在绘制视图时,系统会根据需要显示的区域进行裁剪,减少不必要的绘制操作。** 2. **缓存:系统会对经常使用的视图进行缓存,避免重复的绘制操作。** 3. **合并:系统会将相邻的视图合并为一个绘图指令,减少GPU命令的执行次数。** 4. **异步处理:对于耗时的绘制操作,系统会采用异步处理的方式,避免阻塞UI线程。** **总体来说,Android的屏幕渲染机制非常复杂,但通过以上的优化策略,可以提高渲染效率和用户体验。** ## 布局嵌套深 **在Android布局中,嵌套层级过深会导致一些性能问题和可维护性问题。具体表现如下:** 1. **性能问题:布局嵌套层级越深,渲染UI的时间就越长,CPU和内存的开销也会越大,从而导致应用程序变得越来越卡顿和耗电。** 2. **可维护性问题:嵌套层级越深,布局文件的可读性就越差,代码的维护难度就越大。当需要修改某个UI元素时,需要逐级查找布局文件,并进行相应的修改,工作量比较大。** **因此,在实际的开发过程中,需要尽量减少布局文件的嵌套层级,从而提高程序的性能和可维护性。具体的优化方式包括:** 1. **使用ConstraintLayout或RelativeLayout布局来替代LinearLayout和FrameLayout等布局,能够减少布局文件的嵌套层级。** 2. **对于重复的UI元素,可以使用include标签进行模块化设计,减少布局文件的冗余代码。** 3. **使用ViewStub标签来延迟加载布局文件,可以避免一些不必要的性能开销。** 4. **尽量使用相对布局和百分比布局等可以避免布局嵌套的布局方式,从而减少布局文件的嵌套层级。** **总的来说,Android布局嵌套层级过深会导致一些性能问题和可维护性问题,需要采取一些优化措施来减少布局文件的嵌套层级。在实际的开发过程中,需要对Android布局原理和布局优化有一定的了解,才能更好地设计和实现高效、易维护的布局。** ## [热修复](https://blog.csdn.net/zenmela2011/article/details/125641196) **热修复是指在不重新安装应用或者重新启动应用的情况下,修复应用程序的bug或者添加新的功能。热修复技术可以在应用程序发布后快速修复bug,提高应用程序的稳定性和用户体验。常用的热修复框架有以下几种:** 1. **DexClassLoader热修复框架:利用DexClassLoader动态加载补丁dex文件,将补丁代码注入到应用程序中。这种方法需要重新加载类,可能会导致应用程序的某些功能出现异常。** 2. [Tinker热修复框架](https://blog.csdn.net/EthanCo/article/details/102966546):Tinker是腾讯开发的热修复框架,利用Android的热修复特性,在应用程序运行时动态替换dex文件或者so文件,从而实现热修复。Tinker框架可以避免重新加载类,减少应用程序的异常情况,但是需要在应用程序开发阶段进行一定的配置。 3. **AndFix热修复框架:AndFix是阿里巴巴开发的热修复框架,利用Java的动态代理技术,在运行时替换修复类的字节码,从而实现热修复。AndFix框架使用方便,但是需要在应用程序开发阶段进行一定的配置。** 4. **Robust热修复框架:Robust是美团开发的热修复框架,利用Java的Instrumentation机制,在应用程序启动时动态修改字节码,从而实现热修复。Robust框架可以保证热修复的稳定性和可靠性,但是需要在应用程序开发阶段进行一定的配置。** **总体来说,热修复技术在应用程序的开发和维护中非常重要,不同的热修复框架有不同的优缺点,开发人员需要根据实际情况选择合适的框架。** ### 原理 1. **动态加载技术** **动态加载技术是指在应用程序运行时,动态地将代码加载到内存中,从而达到动态更新应用程序的目的。在安卓中,动态加载技术主要涉及到DexClassLoader和反射机制。** **DexClassLoader是安卓中的一个类加载器,可以加载已经编译好的Dex文件。通过DexClassLoader,我们可以在应用程序运行时动态加载Dex文件中的类,从而实现热修复的功能。** **反射机制是安卓中常用的一种技术,可以在运行时动态地获取类的信息,调用类的方法或修改类的属性值。在热修复中,反射机制可以用于动态地加载类,并调用类中的方法。** 2. **补丁合并技术** **补丁合并技术是指将热修复补丁和原有的代码合并在一起,从而达到更新代码的目的。在安卓中,补丁合并技术主要有两种实现方式:基于类替换和基于字节码替换。** **基于类替换是指通过动态加载技术,将需要修复的类替换成新的类,从而达到更新代码的目的。这种方式需要在补丁中提供新的类文件,并通过动态加载技术将其替换掉原有的类文件。** **基于字节码替换是指通过修改字节码的方式,达到修复代码的目的。这种方式需要在补丁中提供需要修改的字节码,并通过动态加载技术将其注入到原有的字节码中。** **总的来说,安卓热修复机制的原理就是通过动态加载技术和补丁合并技术,实现对已发布应用程序中的代码进行实时更新,从而达到修复bug和安全问题的目的。** ## AIDL **AIDL是Android Interface Definition Language的缩写,是一种Android系统间进程通信(IPC)的机制。AIDL通常用于实现跨进程的方法调用和数据传输。** **AIDL的主要作用是定义一个接口,用于描述服务端和客户端之间的通信方式和数据结构。在AIDL中,服务端和客户端都必须实现相同的接口,并且服务端必须将接口实现注册到系统中。当客户端需要调用服务端的方法时,它会向系统发送请求,并将请求参数通过AIDL接口传递给服务端,服务端根据接口定义执行相应的方法并返回结果给客户端。** **AIDL的使用步骤如下:** 1. **定义AIDL接口:定义一个AIDL接口,用于描述服务端和客户端之间的通信方式和数据结构。** 2. **实现AIDL接口:服务端和客户端都需要实现相同的AIDL接口,并实现接口中定义的方法。** 3. **注册服务端:服务端需要将AIDL接口实现注册到系统中,以便客户端可以通过系统获取到服务端的AIDL实例。** 4. **调用服务端方法:客户端可以通过获取到服务端的AIDL实例,调用服务端中定义的方法,并将请求参数传递给服务端,服务端执行相应的操作并将结果返回给客户端。** **总之,AIDL是Android系统间进程通信(IPC)的机制,它通过定义接口的方式实现服务端和客户端之间的通信和数据传输。在Android开发中,AIDL通常用于实现跨进程的方法调用和数据传输。** ## 动画 **常见的Android动画包括以下几种:** 1. **Tween动画:也称为补间动画,可以实现简单的动画效果,如平移、缩放、旋转等。Tween动画可以通过XML或Java代码来实现。** 2. **帧动画:也称为逐帧动画,是一种通过连续播放多张图片来实现动画效果的方式。帧动画可以通过XML或Java代码来实现。** 3. **属性动画:也称为ValueAnimator动画,可以实现更为复杂的动画效果,如颜色渐变、透明度变化等。属性动画可以通过XML或Java代码来实现。** 4. **视图动画:也称为LayoutAnimation动画,可以实现视图的入场和出场动画效果。视图动画可以通过XML或Java代码来实现。** 5. **物理引擎动画:可以使用物理引擎库,如Box2D或Chipmunk等,实现更为真实的物理动画效果,如重力、碰撞等。** **在使用Android动画时,需要注意以下几点:** 1. **动画效果不宜过于复杂:过于复杂的动画效果可能会影响应用程序的性能和响应速度,从而导致卡顿和ANR等问题。** 2. **动画的持续时间不宜过长:动画的持续时间不宜过长,通常不应超过2秒钟,以保证用户体验和应用程序的流畅性。** 3. **尽量使用硬件加速:在Android 3.0及以上版本中,可以使用硬件加速来提高动画效果的性能和流畅度,从而优化应用程序的体验。** 4. **合理设置动画插值器:可以通过设置动画插值器来控制动画效果的速度和缓动效果,以满足不同的用户需求。** **视图动画,或者说补间动画。只是视觉上的一个效果,实际view属性没有变化,性能好,但是支持方式少。** **属性动画,通过变化属性来达到动画的效果,性能略差,支持点击等事件。android 3.0** **帧动画,通过drawable一帧帧画出来。** **Gif动画,原理同上,canvas画出来。** ## Activity&Fragment 1. **Activity:Activity 是 Android 应用中的一个基本组件,用于表示一个用户界面。每个 Activity 都有自己的布局和生命周期,可以响应用户的输入事件和系统事件。Activity 可以启动其他 Activity,也可以被其他 Activity 启动和销毁。在 Android 应用中,每个 Activity 都必须在 AndroidManifest.xml 文件中进行声明和注册。** 2. **Fragment:Fragment 是 Android 应用中的一个可复用的 UI 组件,用于表示一个用户界面的一部分。每个 Fragment 可以包含自己的布局和生命周期,可以与 Activity 独立存在,也可以嵌入到 Activity 中。Fragment 可以用来实现多窗口界面、动态加载和替换 UI 界面等功能。在 Android 应用中,每个 Fragment 也可以在 XML 或代码中进行声明和使用。** ### 生命周期 **Activity的生命周期:** 1. **onCreate():在Activity被创建时调用,用于进行初始化操作,如加载布局和绑定数据。** 2. **onStart():在Activity可见但没有获取焦点时调用,用于进行一些界面更新和数据加载操作。** 3. **onResume():在Activity获取焦点时调用,用于恢复之前的操作和进行一些用户交互操作。** 4. **onPause():在Activity失去焦点但仍然可见时调用,用于保存数据和释放资源等操作。** 5. **onStop():在Activity不可见时调用,用于释放资源和保存数据等操作。** 6. **onRestart():在Activity从停止状态重新启动时调用,用于进行一些界面更新和数据加载操作。** 7. **onDestroy():在Activity被销毁时调用,用于释放资源和保存数据等操作。** **Fragment的生命周期:** 1. **onAttach():在Fragment与Activity关联时调用,用于获取Context对象。** 2. **onCreate():在Fragment被创建时调用,用于进行初始化操作,如加载布局和绑定数据。** 3. **onCreateView():在Fragment创建视图时调用,用于加载布局和初始化视图。** 4. **onActivityCreated():在Fragment所在的Activity完成创建时调用,用于获取Activity的数据和状态。** 5. **onStart():在Fragment可见但没有获取焦点时调用,用于进行一些界面更新和数据加载操作。** 6. **onResume():在Fragment获取焦点时调用,用于恢复之前的操作和进行一些用户交互操作。** 7. **onPause():在Fragment失去焦点但仍然可见时调用,用于保存数据和释放资源等操作。** 8. **onStop():在Fragment不可见时调用,用于释放资源和保存数据等操作。** 9. **onDestroyView():在Fragment的视图被销毁时调用,用于释放资源和保存数据等操作。** 10. **onDestroy():在Fragment被销毁时调用,用于释放资源和保存数据等操作。** 11. **onDetach():在Fragment与Activity分离时调用,用于释放资源和保存数据等操作。** ## Activity 启动模式 **Activity启动模式是Android应用开发中非常重要的概念,它决定了Activity的启动方式和任务栈的行为。常见的Activity启动模式包括:** 1. **standard:标准模式,每次启动都会创建一个新的Activity实例,并放入新的任务栈中。** 2. **singleTop:栈顶复用模式,如果要启动的Activity已经位于任务栈的栈顶,则不会创建新的Activity实例,而是直接复用栈顶的Activity实例。** 3. **singleTask:栈内复用模式,如果要启动的Activity已经存在于任务栈中,那么会直接使用该Activity实例,并将该Activity上面的所有Activity出栈,使得该Activity成为栈顶。** 4. **singleInstance:单实例模式,如果要启动的Activity是单实例模式,则系统会创建一个新的任务栈,该Activity实例将作为该任务栈的唯一成员存在。** ### startService与bindService的区别 **在Android中,startService()和bindService()是两种不同的服务启动方式,它们有以下区别:** 1. **生命周期不同:startService()启动的服务生命周期与启动它的组件(Activity或Service)无关,即使启动它的组件被销毁,服务也会一直运行。而bindService()启动的服务生命周期与启动它的组件相关联,当启动它的组件被销毁时,服务也会被销毁。** 2. **通信方式不同:startService()启动的服务是通过Intent进行通信,可以在服务中使用onStartCommand()方法来接收和处理Intent,也可以在其他组件中通过Intent发送消息给服务。而bindService()启动的服务是通过IBinder接口进行通信,可以在服务中使用onBind()方法返回IBinder对象,其他组件可以通过该对象与服务进行通信。** 3. **多次启动行为不同:startService()启动的服务可以被多次启动,每次启动时都会调用onStartCommand()方法,并且会创建新的服务实例。而bindService()启动的服务只会被启动一次,多次调用bindService()方法只会返回同一个服务实例。** 4. **启动方式不同:startService()启动服务时不需要与服务建立连接,而bindService()启动服务时需要与服务建立连接。** **综上所述,startService()和bindService()两种启动方式各有优缺点,开发者需要根据实际需求和场景选择合适的启动方式。如果需要在服务和其他组件之间进行频繁的通信,可以使用bindService()方式启动服务;如果只需要启动服务执行某些任务,可以使用startService()方式启动服务。** ### Activity A启动Activity B的场景 **在Android中,Activity的启动模式包括standard、singleTop、singleTask和singleInstance四种。对于从Activity A启动Activity B的场景,它们的生命周期状态如下:** 1. **standard模式:每次启动Activity都会创建一个新的实例,并且该实例会被放入任务栈中。当从Activity A启动Activity B时,Activity A会停止并进入Stopped状态,Activity B会进入Paused状态。** 2. **singleTop模式:如果要启动的Activity已经在任务栈的栈顶,那么不会创建新的实例,而是复用栈顶的实例。如果要启动的Activity不在栈顶,则会创建新的实例,并放入任务栈中。当从Activity A启动Activity B时,如果Activity B已经在栈顶,则不会创建新的实例,Activity A会进入Stopped状态,Activity B会进入Resumed状态;如果Activity B不在栈顶,则会创建新的实例,Activity A会进入Stopped状态,Activity B会进入Paused状态。** 3. **singleTask模式:每个任务栈中最多只能有一个实例,如果要启动的Activity已经在任务栈中,则会将该实例以及它上面的所有Activity弹出栈顶,直到该实例成为栈顶。如果要启动的Activity不在任务栈中,则会创建新的实例,并放入任务栈中。当从Activity A启动Activity B时,如果Activity B已经在任务栈中,则将Activity B以及它上面的所有Activity弹出栈顶,直到Activity B成为栈顶,Activity A会进入Stopped状态,Activity B会进入Resumed状态;如果Activity B不在任务栈中,则会创建新的实例,Activity A会进入Stopped状态,Activity B会进入Paused状态。** 4. **singleInstance模式:同一应用程序中,每个任务栈中只有一个实例,如果要启动的Activity已经在任务栈中,则直接复用该实例。如果要启动的Activity不在任务栈中,则会创建新的任务栈,并在该任务栈中创建新的实例。当从Activity A启动Activity B时,如果Activity B已经在任务栈中,则直接复用该实例,Activity A会进入Stopped状态,Activity B会进入Resumed状态;如果Activity B不在任务栈中,则会创建新的任务栈并在其中创建新的实例,Activity A会进入Stopped状态,Activity B会进入Paused状态。** ### [overridePendingTransition](https://blog.csdn.net/a15286856575/article/details/50883807) ### allowReparent的特点 **在Activity启动模式中,allowReparent属性用于将Activity从一个任务栈转移到另一个任务栈中。如果当前Activity已经存在于任务栈中,并且当前Activity的allowReparent属性设置为true,则可以将该Activity转移到其他任务栈中。这个特性对于实现复杂的应用程序导航和多任务管理非常有用。** ### 栈亲和性 **是指Activity在任务栈中的位置。当一个Activity被启动时,如果它没有指定所属的任务栈,则它将被放入当前任务栈中,并且成为该任务栈的顶部Activity。如果指定了所属的任务栈,则Activity将被放入指定的任务栈中,并且成为该任务栈的顶部Activity。栈亲和性是通过Intent的flag来实现的,常用的flag包括FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_CLEAR_TOP、FLAG_ACTIVITY_SINGLE_TOP等。** ## [自定义View](https://blog.csdn.net/qq_31429205/article/details/107743104) ### [View绘制流程](https://www.jianshu.com/p/dda556c9e653) **百度:** **Android View的绘制流程可以分为三个主要的阶段:测量(Measure)、布局(Layout)和绘制(Draw)。这三个阶段按照顺序依次执行,最终完成View的绘制。** **具体的绘制流程如下:** 1. **Measure阶段:在这个阶段中,系统会测量View的尺寸大小,以便能够正确地进行布局和绘制。View的measure()方法会被调用,该方法会根据View的布局参数和父容器的尺寸计算出View的测量宽度和测量高度,并保存到View的measuredWidth和measuredHeight属性中。** 2. **Layout阶段:在这个阶段中,系统会根据View的测量尺寸和布局参数计算出View的实际尺寸和位置,以便能够正确地显示在屏幕上。View的layout()方法会被调用,该方法会设置View的实际宽度和高度,并确定View在父容器中的位置。** 3. **Draw阶段:在这个阶段中,系统会将View绘制到屏幕上。View的draw()方法会被调用,该方法会将View的内容绘制到Canvas上,然后通过invalidate()方法通知系统进行下一次绘制。如果View需要绘制子View,则会递归调用子View的绘制流程。** **总的来说,Android View的绘制流程是一个按照顺序依次执行的过程,其中Measure阶段用于测量View的尺寸大小,Layout阶段用于确定View的位置和大小,Draw阶段用于将View绘制到屏幕上。了解View的绘制流程可以帮助开发者优化View的性能和实现自定义View。** **GPT3:** **View的绘制流程主要包括以下几个步骤:** 1. **measure:在measure阶段,系统会根据View的LayoutParams、上下文环境等因素,确定View的宽度和高度,以及内部的布局方式。** 2. **layout:在layout阶段,系统会确定View的位置和大小,并对其子View进行布局。** 3. **draw:在draw阶段,系统会根据View的大小和位置,调用View的draw()方法,绘制View的内容和背景。** 4. **invalidate:如果View的内容或背景需要更新,可以调用invalidate()方法通知系统重新绘制。** **在View的绘制流程中,最核心的是draw()方法,它实际上是通过Canvas来绘制View的内容和背景。在绘制过程中,系统会先绘制背景,然后再绘制内容,可以通过重写onDraw()方法来实现自定义的绘制逻辑。** **在绘制过程中,系统还提供了一些有用的绘制工具类,例如Paint、Path、Rect等,可以方便地实现自定义的绘制效果。此外,还可以通过在View上添加Drawable或使用Bitmap等方式来实现更加复杂的绘制效果。** ## surfaceView **SurfaceView是Android提供的一个用于实现异步绘制的控件,它可以在一个独立的线程中进行绘制操作,避免了在UI线程中进行绘制的阻塞问题。** **与普通的View不同,SurfaceView通过创建一个独立的Surface实例来进行绘制操作,该实例包含了一个专门用于绘制的SurfaceHolder对象。在绘制时,SurfaceView会将SurfaceHolder所包含的Surface对象和独立的绘制线程关联起来,使得在独立线程中进行绘制操作时可以直接操作Surface对象,而不需要经过UI线程。** **使用SurfaceView时需要重写SurfaceHolder.Callback接口,实现surfaceCreated()、surfaceChanged()和surfaceDestroyed()方法。其中,surfaceCreated()方法在Surface被创建时调用,surfaceChanged()方法在Surface的大小或格式发生改变时调用,surfaceDestroyed()方法在Surface被销毁时调用。在surfaceCreated()方法中,可以启动独立的绘制线程,在线程中进行异步绘制操作。** **由于SurfaceView可以实现异步绘制,因此在需要进行大量绘制操作的情况下,使用SurfaceView可以有效避免在UI线程中进行绘制时产生的卡顿问题,提高应用的流畅度和响应速度。不过需要注意的是,在使用SurfaceView时需要注意线程同步和资源释放的问题,以免引发线程安全和内存泄漏等问题。** ## [流式布局](https://blog.csdn.net/m0_52197531/article/details/122865878) ## 布局实现的底层原理 **布局实现的底层原理涉及到了Android系统的View体系和UI渲染机制。** **在Android中,每个View都有一个对应的Layout实现,用于确定View的大小和位置。Layout实现通常继承自ViewGroup或其子类,负责管理其子View的布局。** **当一个View需要进行布局时,Android系统会先调用该View的measure()方法,通过measure过程计算出View的尺寸,并将尺寸保存在View的layoutParams属性中。** **然后,系统会调用该View的layout()方法,根据父View的尺寸和该View的layoutParams属性,计算出该View在父View中的位置和大小,并将位置和大小保存在该View的mLeft、mTop、mRight和mBottom属性中。** **最后,系统会调用该View的draw()方法,使用Canvas对象绘制该View的内容和背景,完成布局的渲染。** **整个布局实现的过程,可以简单概括为measure、layout和draw三个阶段,每个阶段都涉及到不同的计算和操作。通过这些计算和操作,Android系统实现了高效、灵活的UI布局和渲染机制,为开发者提供了强大的界面设计和实现能力。** ## RecyclerView和ListView区别&优化 ### 区别 1. **复用机制不同:RecyclerView通过ViewHolder机制实现复用,可以复用视图的布局和控件对象;而ListView使用convertView实现复用,只能复用视图的布局。** 2. **布局管理器不同:RecyclerView支持多种布局管理器,例如LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager等;而ListView只支持垂直方向的线性布局。** 3. **动画效果不同:RecyclerView通过ItemAnimator实现默认的Item动画效果;而ListView没有默认的动画效果。** ### 优化 1. **使用ViewHolder模式:ListView通过使用ViewHolder模式来避免重复创建视图和控件,从而提高性能。** 2. **延迟加载:可以通过实现自定义的Adapter来实现延迟加载,只有当View被显示时才进行数据加载,避免了不必要的性能开销。** 3. **缓存机制:ListView通过缓存机制来减少View的创建次数,可以通过设置setDrawingCacheEnabled()和setDrawingCacheQuality()方法来启用View的缓存。** 4. **异步加载:可以使用AsyncTask或线程池等方式来实现异步加载,避免在UI线程中进行耗时操作。** **在RecyclerView中,除了以上优化方式外,还可以采用以下方式来提高性能:** 1. **使用ItemDecoration:通过实现自定义的ItemDecoration,可以为每个Item添加分隔线、边距和背景等装饰效果,提高列表的可读性和美观性。** 2. **使用ItemAnimator:通过实现自定义的ItemAnimator,可以实现不同的Item动画效果,例如淡入淡出、滑动、翻转等,提高列表的交互性和用户体验。** ## [进程保活](https://blog.csdn.net/qq_37196748/article/details/126741536) **在Android系统中,Service是一种常见的组件,用于执行后台任务或提供长期运行的服务。为了保证Service的正常运行,可以采用以下几种方式来进行Service保活:** 1. **前台Service:将Service设置为前台Service可以使它具有更高的优先级和稳定性,从而保证它能够长期运行。前台Service通常会在状态栏显示一个通知,提醒用户该Service正在运行。** 2. **开机自启动:可以通过广播接收器来实现Service的开机自启动。当设备启动时,广播接收器会收到BOOT_COMPLETED广播,然后启动Service进行自启动。** 3. **Service绑定:将Service与Activity绑定可以使它们之间建立连接,从而实现数据的交互和通信。当Service与Activity建立连接后,即使Activity被销毁,Service仍然可以继续运行。** 4. **JobScheduler:JobScheduler是一种用于Android系统中的任务调度服务,可以用来定期执行某些任务,从而保证Service的运行。JobScheduler可以在特定的时间间隔内执行某些任务,而且还可以在设备空闲时执行。** 5. **第三方保活库:可以使用一些第三方保活库来保证Service的运行。这些保活库通常会在后台运行一些无害的任务,从而使系统认为应用程序是在正常运行状态,从而避免系统关闭Service。** ## WebView优化 **WebView是Android应用中常用的组件之一,它可以在应用中显示网页内容,提供了良好的用户体验。但是WebView的加载速度和性能可能会受到影响,因此在开发中需要注意WebView的优化,常见的优化方法包括:** 1. **启用缓存:WebView默认会缓存页面和资源文件,可以通过设置setCacheMode()方法来启用缓存,以提高页面加载速度。但是在开发过程中需要注意缓存的更新,以避免数据不一致的问题。** 2. **优化JavaScript代码:JavaScript代码是页面加载速度的主要瓶颈之一,可以通过优化JavaScript代码来提高页面加载速度。常见的优化方法包括压缩、混淆、减少不必要的请求等。** 3. **减少资源文件大小:WebView加载的页面通常包含很多资源文件,如图片、样式文件等,可以通过压缩、合并、精简等方法来减少资源文件的大小,从而提高页面加载速度。** 4. **使用硬件加速:硬件加速可以提高WebView的渲染速度,可以通过设置WebView的setLayerType()方法来启用硬件加速。** 5. **使用WebView缓存:可以将常用的WebView页面缓存起来,在下次加载时直接从缓存中获取,从而提高页面加载速度。WebView缓存可以使用内存缓存或者磁盘缓存实现。** 6. **优化页面布局:合理的页面布局可以减少页面渲染的复杂度,从而提高页面加载速度。可以使用CSS样式表、DIV布局等方式来优化页面布局。** ## 网络通信协议 **是的,我熟悉常见的网络通信协议,比如 HTTP、WebSocket 等。以下是在 Android 开发中的使用场景举例:** 1. **HTTP:HTTP 是一种应用层协议,用于在客户端和服务器之间进行通信。在 Android 应用中,HTTP 协议常用于实现应用的网络请求和数据交换。例如,可以使用 HttpURLConnection 或 OkHttp 等库实现 HTTP 请求,并通过 JSON、XML 等格式进行数据传输和解析。** 2. **WebSocket:WebSocket 是一种全双工的应用层协议,用于在客户端和服务器之间进行实时通信。在 Android 应用中,WebSocket 协议常用于实现应用的即时聊天、推送通知等功能。例如,可以使用 OkHttp、Java-WebSocket 等库实现 WebSocket 连接,并使用 JSON、Protobuf 等格式进行数据传输和解析。** **除此之外,还有其他的网络通信协议,比如 TCP/IP、UDP、FTP 等,在 Android 开发中也有不同的使用场景。选择合适的网络通信协议可以根据应用的需求和性能要求来决定,同时也需要注意网络安全、可靠性和效率等方面的考虑。** ## 网络通信原理 **Android网络通信主要涉及到以下几个核心组件:** 1. **URL:用于指定网络地址。** 2. **HttpURLConnection:用于建立HTTP连接,并进行数据传输。** 3. **AsyncTask:用于在后台执行耗时操作,包括网络请求和数据解析。** 4. **JSON/XML解析器:用于解析服务器返回的JSON或XML数据。** 5. **Volley/OkHttp:用于简化网络请求过程,并提供更高效的缓存机制。** **Android网络通信的具体实现步骤如下:** 1. **在Android应用中创建URL对象,指定网络地址。** 2. **使用HttpURLConnection对象建立HTTP连接,并设置请求方法、请求头、请求体等参数。** 3. **发送请求,并接收服务器返回的响应数据,包括状态码、响应头和响应体等内容。** 4. **在后台使用AsyncTask异步任务执行网络请求,并在异步任务的doInBackground()方法中进行数据解析。** 5. **根据服务器返回的数据格式(JSON或XML),使用相应的解析器解析服务器返回的数据,并将解析结果展示在UI界面中。** 6. **使用Volley/OkHttp等网络框架,简化网络请求过程,并提供更高效的缓存机制。** **总之,Android网络通信的实现涉及到URL、HttpURLConnection、AsyncTask、JSON/XML解析器等核心组件,通过这些组件的协作,可以实现Android应用与服务器之间的数据传输和交互。** ## OkHttp的原理 **OkHttp是一款开源的HTTP客户端,由Square公司开发,它的主要设计目标是优雅、简单、高效。OkHttp底层使用了Java的Socket和URLConnection库,提供了一系列高效的API,可以快速发送HTTP请求和接收HTTP响应。** **OkHttp的****主要原理**可以分为以下几点: 1. **连接池:OkHttp在内部维护了一个连接池,可以在发送HTTP请求时复用已经建立好的连接,从而减少连接的建立和销毁操作,提高性能。** 2. **拦截器:OkHttp提供了拦截器机制,可以在请求发送和响应接收过程中对请求和响应进行拦截和修改。这种机制可以方便地对请求和响应进行统一处理,例如添加公共的Header、统一异常处理等。** 3. **异步请求:OkHttp支持异步请求,可以在后台线程中发送HTTP请求和接收HTTP响应,避免阻塞UI线程,提高应用程序的性能和用户体验。** 4. **缓存:OkHttp支持HTTP响应缓存,可以根据响应头中的Cache-Control字段进行缓存控制,从而减少重复的HTTP请求和响应。** 5. **SSL:OkHttp支持HTTPS协议,可以对HTTPS请求进行验证和安全处理,保证数据传输的安全性。** **总体来说,OkHttp具有高效、简单、可靠的特点,在移动应用程序开发中得到广泛应用。** **具体来说,OkHttp实现了以下****核心功能**: 1. **HTTP请求的发送和响应的接收:OkHttp通过封装HttpUrlConnection类和Socket类,实现了发送HTTP请求和接收响应的功能。** 2. **连接池的管理:OkHttp使用连接池来提高网络请求的性能和效率。连接池可以在不同的请求之间共享连接,并且可以限制同时打开的连接数量。** 3. **请求的缓存:OkHttp可以使用内存缓存和磁盘缓存来提高请求的速度和可靠性。缓存可以存储常用的请求结果,避免重复的网络请求。** 4. **异步请求的支持:OkHttp可以使用多线程技术实现异步请求,避免阻塞主线程,提高应用程序的响应速度。** **OkHttp涉及到的****设计模式**包括: 1. **Builder模式:OkHttp使用Builder模式来创建Request对象和Call对象。Builder模式可以让代码更加清晰,易于扩展。** 2. **策略模式:OkHttp使用策略模式来实现不同的网络请求策略。例如,可以选择使用HttpUrlConnection或者Okio库来实现不同的网络请求。** 3. **责任链模式:OkHttp使用责任链模式来处理HTTP请求和响应。请求和响应都可以经过多个拦截器进行处理,每个拦截器都可以对请求和响应进行修改和处理。** **总之,OkHttp是一款流行的开源HTTP客户端,它使用Java语言编写,实现了HTTP请求的发送和响应的接收、连接池的管理、请求的缓存和异步请求的支持等功能。OkHttp使用了Builder模式、策略模式和责任链模式等设计模式来实现这些功能。** ## Glide原理 **Glide是一款Android图片加载库,具有高效、快速、流畅等特点。Glide的主要原理如下:** 1. **缓存机制:Glide采用了多级缓存机制,将图片缓存在内存和磁盘中,以便于下次快速加载。** 2. **生命周期管理:Glide会自动根据控件的生命周期进行图片加载和回收,避免了内存泄漏和OOM等问题。** 3. **调整图片大小:Glide会根据ImageView的大小和布局参数等信息来自动调整图片的大小,避免了因图片过大导致的内存消耗和性能问题。** 4. **适配图片格式:Glide支持多种图片格式,包括JPG、PNG、GIF等,可以根据图片格式来选择合适的解码器,提高图片加载速度。** ## RxJava原理 **RxJava是一款基于响应式编程思想的Java库,具有强大的事件处理能力。RxJava的主要原理如下:** 1. **Observable:Observable是RxJava中的核心概念,它可以发出一系列的事件,包括普通数据、异常和完成信号等。** 2. **Subscriber:Subscriber是订阅者,用于接收Observable发出的事件,可以处理普通数据、异常和完成信号等。** 3. **Operator:Operator是RxJava中的转换器,用于对Observable中的数据进行转换、过滤、组合等操作。** 4. **Scheduler:Scheduler是调度器,用于控制Observable的事件在哪个线程中执行,包括IO线程、计算线程、UI线程等。** 5. **RxJava的链式调用:RxJava支持链式调用,可以将多个操作通过链式结构组合在一起,形成一个完整的事件流处理链。** ## 打包流程 **Android打包流程主要分为以下几个步骤:** 1. **编译源代码:使用Android SDK中的编译工具将Android应用的源代码编译成字节码文件,生成R.java文件和其他必要的资源文件。** 2. **打包资源:使用Android SDK中的aapt工具,将所有资源文件(包括图片、布局文件、字符串、颜色等)打包成APK文件中的资源文件。** 3. **打包DEX文件:使用Android SDK中的dx工具,将所有Java字节码文件打包成一个DEX文件,该文件包含所有Java代码和库的字节码。** 4. **签名应用:使用keytool和jarsigner工具,对应用进行数字签名。数字签名用于验证应用是否被篡改,并防止应用被恶意攻击者修改。** 5. **对应用进行压缩:使用zipalign工具对APK文件进行压缩和优化,以减少应用在设备上的存储空间和启动时间。** 6. **发布应用:将已签名和压缩的APK文件发布到Google Play Store或其他应用商店。** **总之,Android打包流程包括编译源代码、打包资源、打包DEX文件、签名应用、压缩应用和发布应用等步骤。其中,签名应用和发布应用是最后两个重要步骤,签名应用是保证应用完整性和安全性的关键,而发布应用则是将应用发布到市场供用户下载和使用的最终步骤。** ## 应用签名 **在 Android 应用程序开发中,应用签名是指开发人员使用数字证书对应用程序进行签名,以确保应用程序的完整性和真实性。应用签名过程将应用程序的代码与数字证书绑定在一起,并生成唯一的数字签名。这个数字签名可以用于验证应用程序的身份和完整性,以保证应用程序没有被篡改或修改。** **应用程序签名的过程包括以下几个步骤:** 1. **创建密钥库:开发人员需要使用密钥库工具创建一个密钥库文件,这个文件包含开发人员的私钥和公钥。** 2. **创建密钥对:开发人员需要使用密钥库工具创建一个密钥对,这个密钥对包括一个私钥和一个公钥。私钥是用于签名应用程序的,而公钥将在应用程序中使用。** 3. **签名应用程序:开发人员需要使用签名工具将应用程序和密钥对进行签名,以生成数字签名。这个数字签名将被嵌入到应用程序中,以便在应用程序安装和运行时验证应用程序的身份和完整性。** **应用程序签名的作用包括以下几个方面:** 1. **应用程序完整性验证:应用程序签名可以用于验证应用程序是否被篡改或修改。如果应用程序的数字签名不匹配,则表示应用程序已被篡改。** 2. **应用程序身份验证:应用程序签名可以用于验证应用程序的身份。只有经过签名的应用程序才能被认为是由特定开发者开发和发布的。** 3. **应用程序更新:应用程序签名可以用于在更新应用程序时验证应用程序的身份和完整性,以确保用户安装的是正式的应用程序更新。** ## 同步锁 **同步锁(Synchronization Lock),也称为互斥锁(Mutex Lock),是一种线程同步机制,用于防止多个线程同时访问共享资源。同步锁可以保证在同一时刻只有一个线程可以访问共享资源,从而避免了线程之间的竞争和冲突。** **同步锁的基本原理是通过获取和释放锁来控制线程的访问。当一个线程需要访问共享资源时,它首先需要获取同步锁,如果同步锁已经被其他线程持有,则该线程需要等待直到同步锁被释放。当该线程访问完共享资源后,需要释放同步锁,从而允许其他线程获取同步锁并访问共享资源。** **在Java中,同步锁可以使用synchronized关键字来实现。例如,可以使用synchronized关键字来保护一个方法或一个代码块,从而实现线程的同步访问。在使用synchronized关键字时,需要注意以下几点:** 1. **同步锁只能在同一进程内的不同线程之间使用,不能用于不同进程之间的线程通信。** 2. **同步锁只能保护一个代码块或一个方法,不能保护多个方法之间的访问。** 3. **同步锁可以降低程序的性能,因为它会导致线程的阻塞和唤醒,从而增加线程上下文切换的开销。** **总之,同步锁是一种线程同步机制,用于防止多个线程同时访问共享资源。同步锁的基本原理是通过获取和释放锁来控制线程的访问。在Java中,同步锁可以使用synchronized关键字来实现。** ## 用户体验 **我认为移动端的用户体验是非常重要的,它直接关系到用户对应用的使用感受和满意度。为了提高移动端用户体验,我通常会采用以下几种方式进行产品优化和改进:** 1. **设计简洁、直观的界面:在设计移动端应用的界面时,我通常会采用简洁、直观的设计风格,避免界面过于复杂和拥挤。同时,我也会关注用户的视觉习惯和感知,优化布局、字体、颜色等设计元素,以提高用户体验。** 2. **提供便捷、高效的操作方式:在移动端应用中,操作方式的便捷性和高效性是用户体验的关键因素之一。为了提高用户的操作体验,我通常会采用以下几种方式:提供快捷操作入口,采用手势操作方式,减少用户的输入操作等。** 3. **增加应用的交互性和反馈性:在移动端应用中,增加应用的交互性和反馈性可以提高用户对应用的使用体验和满意度。为了增加应用的交互性和反馈性,我通常会采用以下几种方式:增加动画效果,提供实时反馈机制,优化应用的加载速度等。** 4. **关注用户的反馈和需求:用户反馈和需求是优化和改进产品的重要来源之一。在开发和优化应用过程中,我通常会关注用户的反馈和需求,采用用户反馈、调查和数据分析等方式,不断优化和改进应用的用户体验。** **综上所述,移动端的用户体验是非常重要的,它需要综合考虑应用的设计、操作、交互和反馈等方面。在实践中,我通常会采用以上几种方式来优化和改进应用的用户体验,以提高应用的使用价值和用户满意度。** ## 移动应用发展趋势 **移动应用发展趋势方面,我认为未来主要有以下几个方向:** 1. **人工智能和机器学习:人工智能和机器学习技术在移动应用中的应用将越来越广泛,包括智能推荐、语音识别、图像识别等领域,这将改变用户和移动应用的互动方式。** 2. **AR/VR 技术:增强现实和虚拟现实技术在移动应用中的应用将越来越广泛,包括游戏、教育、医疗等领域,这将为用户提供更加沉浸式的体验。** 3. **5G 技术:随着 5G 技术的普及,移动应用的交互方式将更加快速和流畅,同时也将为更多的移动应用提供更高质量的视频、音频和图像等功能。** 4. **IoT 技术:物联网技术将越来越广泛地应用于移动应用中,使得用户能够通过移动应用来控制和监控智能家居、智能车辆和智能设备等。** **在 Android 开发方面,未来的发展趋势可能包括以下几个方向:** 1. **响应式 UI:随着用户对更加快速和流畅的体验的需求不断提高,开发人员将越来越注重设计和开发响应式 UI,以使得应用程序在不同的设备上都能够得到最佳的性能和用户体验。** 2. **安全性和隐私保护:在 Android 应用程序开发中,安全性和隐私保护将越来越受到重视。开发人员将更加注重应用程序的安全性和隐私保护,以保护用户的个人信息和数据。** 3. **模块化开发:模块化开发将成为 Android 开发的主流趋势,开发人员将使用更加灵活的组件化架构来构建应用程序,以方便重用、扩展和测试。** 4. **Kotlin 和 Jetpack:Kotlin 作为一种更加现代化的编程语言,将越来越被开发人员所采用,并且 Jetpack 框架将继续发展和完善,以提供更加丰富和强大的组件和工具,以提高开发人员的生产力和效率。** **总的来说,移动应用和 Android 开发将不断发展和演变,开发人员需要密切关注行业动态和技术趋势,不断学习和适应** # [Vue3框架基础【中概率】](https://cn.vuejs.org/guide/introduction.html) ## vue 的 keep-alive **缓存组件、条件缓存、路由配合条件缓存、不重新加载、activated、deactivated 标准回答 **`<keep-alive>`作用:缓存组件,提升性能,避免重复加载一些不需要经常变动且内容较多的组件。 `<keep-alive>`的使用方法:使用`<keep-alive>`标签对需要缓存的组件进行包裹,默认情况下被`<keep-alive>`标签包裹的组件都会进行缓存,区分被包裹的组件是否缓存有两种方法,第一种是给keepalive 添加属性,组件名称指的是具体组件添加的name,不是路由里面的name。include 包含的组件(可以为字符串,数组,以及正则表达式,只有匹配的组件会被缓存)。exclude 排除的组件(以为字符串,数组,以及正则表达式,任何匹配的组件都不会被缓存)。第二种也是最常用的一种是,和路由配合使用:在路由中添加meta属性。 使用keepalive导致组件不重新加载,也就不会重新执行生命周期的函数,如果要解决这个问题,就需要两个属性进入时触发:activated 退出时触发:deactivated 加分回答 `<keep-alive>`适用的场景:首页展示固定数据的组件,比如banner九宫格`</keep-alive></keep-alive></keep-alive></keep-alive></keep-alive>` # [Dart基础【中高概率】](https://blog.csdn.net/a546036242/article/details/122595156) ## 基础 **dart是****值传递** `Dart` 属于是**强类型语言** ,但可以用 `var` 来声明变量,`Dart` 会**自推导出数据类型**,`var` 实际上是编译期的“语法糖”。**`dynamic` 表示动态类型**, 被编译后,实际是一个 `object` 类型,在编译期间不进行任何的类型检查,而是在运行期进行类型检查。 **2、**`Dart` 中 `if` 等语句只支持 `bool` 类型,`switch` 支持 String 类型。 **3、**`Dart` 中**数组和 `List` 是一样的。** **4、**`Dart` 中,**`Runes` 代表符号文字** , 是 UTF-32 编码的字符串, 用于如 `Runes input = new Runes('\u{1f596} \u{1f44d}');` **5、****`Dart` 支持闭包。** **6、**`Dart` 中 number 类型分为 **int 和 double ,没有 float 类型。** **7、**`Dart` 中 **级联操作符** 可以方便配置逻辑 ## 特性 * **Productive(生产力高,Dart的语法清晰明了,工具简单但功能强大)** * **Fast(执行速度快,Dart提供提前优化编译,以在移动设备和Web上获得可预测的高性能和快速启动。)** * **Portable(易于移植,Dart可编译成ARM和X86代码,这样Dart移动应用程序可以在iOS、Android和其他地方运行)** * **Approachable(容易上手,充分吸收了高级语言特性,如果你已经知道C++,C语言,或者Java,你可以在短短几天内用Dart来开发)** * **Reactive(响应式编程)** ## 重要概念 * **在Dart中,一切都是对象,所有的对象都是继承自Object** * **Dart是强类型语言,但可以用var或 dynamic来声明一个变量,Dart会自动推断其数据类型,dynamic类似c#** * **没有赋初值的变量都会有默认值null** * **Dart支持顶层方法,如main方法,可以在方法内部创建方法** * **Dart支持顶层变量,也支持类变量或对象变量** * **Dart没有public protected private等关键字,如果某个变量以下划线(_)开头,代表这个变量在库中是私有的** * **基本数据类型传值,类传引用。** ## .. **级联操作符** **「..」和「.」不同的是 调用「..」后返回的相当于是 this,而「.」返回的则是该方法返回的值 。** ## 作用域 **Dart 没有 「public」「private」等关键字,默认就是公开的,私有变量使用 下划线 _开头。** ## 单线程模型 **简单来说,Dart 在单线程中是以消息循环机制来运行的,包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。** **当Flutter应用启动后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,当所有微任务队列执行完后便开始执行事件队列中的任务,事件任务执行完毕后再去执行微任务,如此循环往复,生生不息。** ## 多任务并行 **Dart的多线程和前端的多线程有很多的相似之处。Flutter的多线程主要依赖Dart的并发编程、异步和事件驱动机制。** ## Widget的两种类型 **StatelessWidget**: 一旦创建就不关心任何变化,在下次构建之前都不会改变。它们除了依赖于自身的配置信息(在父节点构建时提供)外不再依赖于任何其他信息。比如典型的Text、Row、Column、Container等,都是StatelessWidget。它的生命周期相当简单:初始化、通过build()渲染。 **StatefulWidget**: 在生命周期内,该类Widget所持有的数据可能会发生变化,这样的数据被称为**State**,这些拥有动态内部数据的Widget被称为StatefulWidget。比如复选框、Button等。State会与Context相关联,并且此关联是永久性的,State对象将永远不会改变其Context,即使可以在树结构周围移动,也仍将与该context相关联。当**state与context关联时**,state被视为**已挂载**。StatefulWidget由两部分组成,在初始化时必须要在createState()时初始化一个与之相关的State对象。 ## State 对象的初始化流程 **initState**() : 一旦State对象被创建,initState方法是第一个(构造函数之后)被调用的方法。可通过重写来执行额外的初始化,如初始化动画、控制器等。重写该方法时,应该首先调用super.initState()。在initState中,无法真正使用context,因为框架还没有完全将其与state关联。initState在该State对象的生命周期内将不会再次调用。 **didChangeDependencies**(): 这是第二个被调用的方法。在这一阶段,context已经可用。如果你的Widget链接到了一个InheritedWidget并且/或者你需要初始化一些listeners(基于context),通常会重写该方法。 **build(BuildContext context)**: 此方法在didChangeDependencies()、didUpdateWidget()之后被调用。每次State对象更新(或当InheritedWidget有新的通知时)都会调用该方法!我们一般都在build中来编写真正的功能代码。为了强制重建,可以在需要的时候调用setState((){**…**})方法。 **dispose()**: 此方法在Widget被废弃时调用。可重写该方法来执行一些清理操作(如解除listeners),并在此之后立即调用super.dispose()。 ## Widget 唯一标识Key **在flutter中,每个widget都是被唯一标识的。这个唯一标识在build或rendering阶段由框架定义。该标识对应于可选的Key参数,如果省略,Flutter将会自动生成一个。** **在flutter中,主要有4种类型的Key:GlobalKey(确保生成的Key在整个应用中唯一,是很昂贵的,允许element在树周围移动或变更父节点而不会丢失状态)、LocalKey、UniqueKey、ObjectKey。** ## Navigator **Navigator是在Flutter中负责管理维护页面堆栈的导航器。MaterialApp在需要的时候,会自动为我们创建Navigator。Navigator.of(context),会使用context来向上遍历Element树,找到MaterialApp提供的_NavigatorState再调用其push/pop方法完成导航操作。** ## Future关键字 **Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。** **在Java并发编程开发中,经常会使用Future来处理异步或者延迟处理任务等操作。而在Dart中,执行一个异步任务同样也可以使用Future来处理。在 Dart 的每一个 Isolate 当中,执行的优先级为 : Main > MicroTask > EventQueue。** ## Stream数据流 **在Dart中,Stream 和 Future 一样,都是用来处理异步编程的工具。它们的区别在于,Stream 可以接收多个异步结果,而Future 只有一个。** **Stream 的创建可以使用 Stream.fromFuture,也可以使用 StreamController 来创建和控制。还有一个注意点是:普通的 Stream 只可以有一个订阅者,如果想要多订阅的话,要使用 asBroadcastStream()。** ## Stream 两种订阅 **Stream有两种订阅模式:单订阅(single) 和 多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。** **Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。** ## await for **await for是不断获取stream流中的数据,然后执行循环体中的操作。它一般用在直到stream什么时候完成,并且必须等待传递完成之后才能使用,不然就会一直阻塞。** ## mixin机制 **mixin 是Dart 2.1 加入的特性,以前版本通常使用abstract class代替。简单来说,mixin是为了解决继承方面的问题而引入的机制,Dart为了支持多重继承,引入了mixin关键字,它最大的特殊处在于: mixin定义的类不能有构造方法,这样可以避免继承多个类而产生的父类构造方法冲突。** **mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件。** # JavaScript基础【中概率】 ## 主要特点 **JavaScript是一门强大的编程语言,是一种专为与网页交互而设计的脚本语言,是一种动态类型、弱类型、基于原型的语言。** **JavaScript是客户端和服务器端脚本语言,可以插入到HTML页面中,并且是目前较热门的Web开发语言。同时,JavaScript也是面向对象编程语言。** **特点:** ``` 脚本语言。JavaScript是一种解释型的脚本语言,C、C++等语言先编译后执行,而JavaScript是在程序的运行过程中逐行进行解释。 基于对象。JavaScript是一种基于对象的脚本语言,它不仅可以创建对象,也能使用现有的对象。 简单。JavaScript语言中采用的是弱类型的变量类型,对使用的数据类型未做出严格的要求,是基于Java基本语句和控制的脚本语言,其设计简单紧凑。 动态性。JavaScript是一种采用事件驱动的脚本语言,它不需要经过Web服务器就可以对用户的输入做出响应。在访问一个网页时,鼠标在网页中进行鼠标点击或上下移、窗口移动等操作JavaScript都可直接对这些事件给出相应的响应。 跨平台性。JavaScript脚本语言不依赖于操作系统,仅需要浏览器的支持。因此一个JavaScript脚本在编写后可以带到任意机器上使用,前提上机器上的浏览器支 持JavaScript脚本语言,目前JavaScript已被大多数的浏览器所支持。 ``` ## 数据类型 * **基本数据类型:Number、String、Boolean、Null、Undefined** * **复杂数据类型:Object(Function、Array、Date、RegExp)** ## 常用定义 ### 判断某变量是否为数组数据类型 ``` if (typeof Array.isArray === "undefined"){ Array.isArray = function(arg){ return Object.prototype.toString.call(arg)==="[object Array]"; } } ``` ### ID的Input输入框获取输入值 ``` document.getElementById(ID).value; ``` ### 已知ID的DIV的html内容为xxxx,字体颜色设置 ``` var oDiv = document.getElementById(ID); oDiv.innerHTML = "xxxx"; oDiv.getElementById(ID).style.color = "black"; ``` ### DOM节点被点击执行函数 **先获取到这个DOM节点,然后绑定onclick事件。比如**`myDOM.onclick = fn`或者`myDOM.addEventListener("click",fn);` **或者直接在HTML中绑定**`<div onclick = "fn()"></div>` ### Ajax&JSON **Ajax是异步JavaScript和XML,用于在Web页面中实现异步数据交互。** **优点:** * **可以使得页面不重载全部内容的情况下加载局部内容,降低数据传输量** * **避免用户不断刷新或者跳转页面,提高用户体验** **缺点:** * **对搜索引擎不友好** * **要实现ajax下的前后退功能成本较大** * **可能造成请求数的增加** * **跨域问题限制** **JSON是一种轻量级的数据交换格式,ECMA的一个子集** **优点:轻量级、易于人的阅读和编写,便于机器(JavaScript)解析,支持复合数据类型(数组、对象、字符串、数字)** **(1)JSON 是一种轻量级的数据交换格式。** **(2)JSON 独立于语言和平台,JSON 解析器和 JSON 库支持许多不同的编程语言。** **(3)JSON的语法表示三种类型值,简单值(字符串,数值,布尔值,null),数组,对象** ### 输出今天的日期 ``` var date = new Date(); var year = date.getFullYear(); var month = date.getMonth()+1; var nowDate = date.getDate(); if(month<10){month = "0" + month;} if(nowDate<10){nowDate = "0" + nowDate;} var today = year + "-" + month + "-" + nowDate; ``` ### 特殊的字符进行转义 ``` function escapeHTML(str){ return str.replace(/[<>&"]/g,function(match){ switch(match){ case "<": return "\<"; case ">": return "\>"; case "&": return "\&"; case "\"": return "\""; } }); } ``` ### foo = foo||bar **如果foo不为假则使用原来的值,没有值则把bar的值付给foo。** ### 随机选取10–100之间的10个数字,存入一个数组,并排序 ``` var arr = []; for(var i=0;i<10;i++){ arr.push(Math.random()*0.9*100+10); } arr.sort(function(a,b){return a-b;}); ``` ### 两个数组合并,并删除第二个元素 ``` var arr1 = [1,2,3]; var arr2 = [“a”,“b”,“c”]; var newArr = arr1.contant(arr2); newArr.splice(1,1); ``` ### 添加、移除、移动、复制、创建和查找节点 ``` - appendChild() //添加 - reomveChild() //移除 - insertBefore() //移动 - cloneNode() //复制 - createElement();createTextNode();createDocumentFragment //复制 - getElementById();getElementsByTagName();getElementsByClassName();getElementsByName() //查找 ``` #### 正则表达式构造函数&表达字面量 **当使用RegExp()构造函数的时候,不仅需要转义引号(即**“**表示**”**),并且还需要双反斜杠(即\表示一个\)。** ### 回调函数 **回调函数是可以作为参数传递给另一个函数的函数,并在某些操作完成后执行。下面是一个简单的回调函数示例,这个函数在某些操作完成后打印消息到控制台。** ``` function modifyArray(arr, callback) { // 对 arr 做一些操作 arr.push(100); // 执行传进来的 callback 函数 callback(); } var arr = [1, 2, 3, 4, 5]; modifyArray(arr, function() { console.log("array has been modified", arr); }); ``` ### ES5&ES6 **ECMAScript 5(ES5):ECMAScript 的第 5 版,于 2009 年标准化。这个标准已在所有现代浏览器中完全实现。** **ECMAScript 6(ES6)或 ECMAScript 2015(ES2015):第 6 版 ECMAScript,于 2015 年标准化。这个标准已在大多数现代浏览器中部分实现。** **以下是 ES5 和 ES6 之间的一些主要区别:** ``` // 箭头函数和字符串插值: const greetings = (name) => { return `hello ${name}`; } const greetings = name => `hello ${name}`; ``` ### 闭包 **闭包是在另一个函数(称为父函数)中定义的函数,并且可以访问在父函数作用域中声明和定义的变量。** **变量背包、作用域链、局部变量不销毁、函数体外访问函数的内部变量、内存泄漏、内存溢出、形成块级作用域、柯里化、构造函数中定义特权方法、Vue中数据响应式Observer 标准回答 闭包 一个函数和词法环境的引用捆绑在一起,这样的组合就是闭包(closure)。一般就是一个函数A,return其内部的函数B,被return出去的B函数能够在外部访问A函数内部的变量,这时候就形成了一个B函数的变量背包,A函数执行结束后这个变量背包也不会被销毁,并且这个变量背包在A函数外部只能通过B函数访问。 闭包形成的原理:作用域链,当前作用域可以访问上级作用域中的变量 闭包解决的问题:能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。 闭包带来的问题:由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。 加分回答 闭包的应用,能够模仿块级作用域,能够实现柯里化,在构造函数中定义特权方法、Vue中数据响应式Observer中使用闭包等。** **闭包可以访问三个作用域中的变量:** * **在自己作用域中声明的变量;** * **在父函数中声明的变量;** * **在全局作用域中声明的变量。** ``` var globalVar = "abc"; // 自调用函数 (function outerFunction (outerArg) { // outerFunction 作用域开始 // 在 outerFunction 函数作用域中声明的变量 var outerFuncVar = 'x'; // 闭包自调用函数 (function innerFunction (innerArg) { // innerFunction 作用域开始 // 在 innerFunction 函数作用域中声明的变量 var innerFuncVar = "y"; console.log( "outerArg = " + outerArg + " " + "outerFuncVar = " + outerFuncVar + " " + "innerArg = " + innerArg + " " + "innerFuncVar = " + innerFuncVar + " " + "globalVar = " + globalVar); // innerFunction 作用域结束 })(5); // 将 5 作为参数 // outerFunction 作用域结束 })(7); // 将 7 作为参数 /** outerArg = 7 outerFuncVar = x innerArg = 5 innerFuncVar = y globalVar = abc */ ``` ### this **在 JavaScript 中,this 是指正在执行的函数的“所有者”,或者更确切地说,指将当前函数作为方法的对象。** ``` function foo() { console.log( this.bar ); } var bar = "global"; var obj1 = { bar: "obj1", foo: foo }; var obj2 = { bar: "obj2" }; foo(); // "global" obj1.foo(); // "obj1" foo.call( obj2 ); // "obj2" new foo(); // undefined ``` ### Revealing Module Pattern 设计模式 **暴露模块模式(Revealing Module Pattern)是模块模式的一个变体,目的是维护封装性并暴露在对象中返回的某些变量和方法。如下所示:** ``` var Exposer = (function() { var privateVariable = 10; var privateMethod = function() { console.log('Inside a private method!'); privateVariable++; } var methodToExpose = function() { console.log('This is a method I want to expose!'); } var otherMethodIWantToExpose = function() { privateMethod(); } return { first: methodToExpose, second: otherMethodIWantToExpose }; })(); Exposer.first(); // 输出: This is a method I want to expose! Exposer.second(); // 输出: Inside a private method! Exposer.methodToExpose; // undefined ``` ### Java&JavaScript **Java是一门十分完整、成熟的编程语言。相比之下,JavaScript是一个可以被引入HTML页面的编程语言。这两种语言并不完全相互依赖,而是针对不同的意图而设计的。 Java是一种面向对象编程(OOPS)或结构化编程语言,类似的如C ++或C,而JavaScript是客户端脚本语言,它被称为非结构化编程。** ### typeof返回 **string,boolean,number,undefined,function,object,symbol** ### 3种强制类型转换和2种隐式类型转换 **强制(parseInt,parseFloat,number)** ** 隐式(== ===)** ### split() & join() **前者是将字符串切割成数组的形式,后者是将数组转换成字符串** ### pop() push() unshift() shift() **push()尾部添加 pop()尾部删除** ** unshift()头部添加 shift()头部删除** ### 事件委托 **利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行!** ### 闭包 **闭包就是能够读取其他函数内部变量的函数,使得函数不被GC回收,如果过多使用闭包,容易导致内存泄露** ### 阻止事件冒泡 **ie:阻止冒泡ev.cancelBubble = true;非IE ev.stopPropagation();** ### 阻止默认事件 **(1)return false;(2) ev.preventDefault();** ### ”==”&“ === ” **前者会自动转换类型,再判断是否相等** ** 后者不会自动类型转换,直接去比较** ### 函数声明&函数表达式 **在Javscript中,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非是一视同仁的,解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问),至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析执行。** ### innerHTML&outerHTML **innerHTML(元素内包含的内容)** **outerHTML(自己以及元素内的内容)** ### 箭头函数&普通函数 1. **箭头函数的 **`this` 对象,是定义时所在的对象,而普通函数的 `this` 对象,是调用时所在的对象。 2. **箭头函数内部没有 **`arguments` 变量,而普通函数可以使用 `arguments` 变量获取参数。 3. **箭头函数无法使用 **`new` 关键字来实例化,是因为它没有原型(prototype)。 ## 数据结构 ### 数组有哪些方法会改变自身 **shift** :删除第一个元素,并返回删除的元素 **unshift** : 头部插入一个新的元素,返回数组的长度 **pop** :删除最后一个元素,并返回删除的元素 **push** :尾喉新增一个元素,并返回新的长度 **reverse** :反转数组 **sort** :对数组进行排序,默认排序顺序规则是将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的 **splice** : 可以是先删除,新增增,替换数组元素,返回被删除数组,无删除则返回空 ### [Set 和 WeakSet](https://www.1024nav.com/front-junior/js/data-structure#set-%E5%92%8C-weakset-%E7%9A%84%E5%8C%BA%E5%88%AB) ### [Map 和 WeakMap](https://www.1024nav.com/front-junior/js/data-structure#%E5%AF%B9%E8%B1%A1%E5%92%8C-map-%E5%92%8C-weakmap-%E7%9A%84%E5%8C%BA%E5%88%AB) ## [基础面试题](https://www.1024nav.com/front-junior/js-basic) ## 基础-武汉微派 ### 事件循环 **事件循环(Event Loop)是一种编程模型,通常用于处理异步操作,比如响应用户输入、网络请求、文件读写等。在事件循环中,程序会不断地监听事件队列,当有事件发生时,就会触发相应的回调函数来处理这个事件。** **事件循环通常由以下几个部分组成:** 1. **事件队列(Event Queue):所有的事件都会被添加到事件队列中,等待事件循环处理。** 2. **事件循环(Event Loop):不断地从事件队列中取出事件并处理,直到事件队列为空。** 3. **回调函数(Callback):事件发生时会触发相应的回调函数来处理这个事件。** **当我们执行一段异步代码时,比如发起一个网络请求或者读取一个文件,操作系统会将这个异步操作交给相应的库进行处理。当操作完成时,库会将结果添加到事件队列中,并注册一个回调函数。当事件循环从事件队列中取出这个事件时,就会触发这个回调函数来处理操作结果。这样就实现了异步操作的回调处理。** **在JavaScript中,事件循环通常是由浏览器或者Node.js提供的,开发者可以使用Promise、async/await等语法来编写异步代码,并将回调函数作为参数传递给异步函数,等待事件循环处理。** ### 节流和防抖 **节流和防抖是前端开发中常用的两种优化技术,它们都可以用来限制事件的频率,提升页面性能和用户体验。** 1. **节流** **节流(Throttle)是指在一段时间内,只执行一次事件处理函数。通俗点说,就是对于频繁触发的事件,例如鼠标滚动、窗口resize等,只在一定时间间隔内执行一次事件处理函数,避免因过多的操作导致页面性能下降或浏览器崩溃。** **实现方式:** **使用定时器实现,通过设置一个时间间隔,在这个时间间隔内只执行一次事件处理函数。** ``` javascript function throttle(fn, delay) { let timer = null; return function() { if (!timer) { timer = setTimeout(() => { fn.apply(this, arguments); timer = null; }, delay); } }; } ``` 2. **防抖** **防抖(Debounce)是指在事件触发后,在一定时间间隔内,只有最后一次事件被执行,中间的事件被抛弃。防抖的实现方式和节流类似,但是防抖是在最后一次触发事件后一段时间内执行事件处理函数,而节流则是在一定时间间隔内执行一次事件处理函数。** **实现方式:** **使用定时器实现,每次事件触发时清除上一个定时器,并重新设置一个新的定时器,在一定时间间隔后执行事件处理函数。** ``` function debounce(fn, delay) { let timer = null; return function() { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, arguments); }, delay); }; } ``` **综上所述,节流和防抖是两种常见的前端性能优化技术,可以应用于很多需要限制事件频率的场景,例如窗口resize、鼠标滚动、搜索框输入等。需要根据具体的场景和需求选择合适的技术进行优化。** # TypeScript基础【前端中高概率】 ## 主要特点 * **跨平台:TypeScript 编译器可以安装在任何操作系统上,包括 Windows、macOS 和 Linux。** * **ES6 特性:TypeScript 包含计划中的 ECMAScript 2015 (ES6) 的大部分特性,例如箭头函数。** * **面向对象的语言:TypeScript 提供所有标准的 OOP 功能,如类、接口和模块。** * **静态类型检查:TypeScript 使用静态类型并帮助在编译时进行类型检查。因此,你可以在编写代码时发现编译时错误,而无需运行脚本。** * **可选的静态类型:如果你习惯了 JavaScript 的动态类型,TypeScript 还允许可选的静态类型。** * **DOM 操作:您可以使用 TypeScript 来操作 DOM 以添加或删除客户端网页元素。** * ## **使用 TypeScript 有什么好处?** 1. **TypeScript 更具表现力,这意味着它的语法混乱更少。** 2. **由于高级调试器专注于在编译时之前捕获逻辑错误,因此调试很容易。** 3. **静态类型使 TypeScript 比 JavaScript 的动态类型更易于阅读和结构化。** 4. **由于通用的转译,它可以跨平台使用,在客户端和服务器端项目中。** ## 优点&缺点 ### 优点 **TypeScript有以下优点。** * **它提供了可选静态类型的优点。在这里,Typescript提供了可以添加到变量、函数、属性等的类型。** * **Typescript能够编译出一个能在所有浏览器上运行的JavaScript版本。** * **TypeScript总是在编译时强调错误,而JavaScript在运行时指出错误。** * **TypeScript支持强类型或静态类型,而这不是在JavaScript中。** * **它有助于代码结构。** * **它使用基于类的面向对象编程。** * **它提供了优秀的工具支持和智能感知,后者在添加代码时提供活动提示。** * **它通过定义模块来定义名称空间概念。** ### 缺点 **TypeScript有以下缺点:** * **TypeScript需要很长时间来编译代码。** * **TypeScript不支持抽象类。** * **如果我们在浏览器中运行TypeScript应用程序,需要一个编译步骤将TypeScript转换成JavaScript。** * **Web开发人员使用了几十年的JavaScript,而TypeScript不是都是新东西。** * **要使用任何第三方库,必须使用定义文件。并不是所有第三方库都有可用的定义文件。** * **类型定义文件的质量是一个问题,即如何确保定义是正确的?** ## 内置数据类型 **数字类型:用于表示数字类型的值。TypeScript 中的所有数字都存储为浮点值。** **布尔类型:一个逻辑二进制开关,包含true或false** **数组类型:跟**`javascript`一致,通过`[]`进行包裹,有两种写法`[]`和`Array<元素类型>` **元祖类型[tuple]: 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,赋值的类型、位置、个数需要和定义(生明)的类型、位置、个数一致** ``` let tupleArr:[number, string, boolean]; tupleArr = [12, '34', true]; //ok typleArr = [12, '34'] // no ok ``` **枚举类型: **`enum`类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字 `any`类型: 可以指定任何类型的值,在编程阶段还不清楚类型的变量指定一个类型,不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查,这时候可以使用`any`类型 **使用**`any`类型允许被赋值为任意类型,甚至可以调用其属性、方法 **Null &undefined类型: 在**`JavaScript` 中 `null`表示 “什么都没有”,是一个只有一个值的特殊类型,表示一个空对象引用,而`undefined`表示一个没有设置值的变量 **未定义类型:一个未定义的字面量,它是所有变量的起点。** **void 类型:分配给没有返回值的方法的类型。** **unknown:如果你不知道预先期望哪种类型,但想稍后分配它,则应该使用该any关键字,并且该关键字将不起作用。** 1. **unknown 是类型安全的。** 2. **同 any 一样,任何值都可以赋给 unknown,但是 unknown 不可以赋值给其它类型,只能赋值给 any 和 unknown。** 3. **unknown 没有被断言或细化到一个确切类型之前,是不允许在其上进行任何操作的。** ## 常用定义 ### 接口 **接口为使用该接口的对象定义契约或结构。** **TypeScript 接口定义了指定对象外观的契约。它用于在对象上强制执行某些构造或保证对象实现某些属性或方法。** **接口可用于使代码可重用和紧凑,或者通过使代码的意图更清晰来使代码更具可读性。** **接口是用关键字定义的interface,它可以包含使用函数或箭头函数的属性和方法声明。** ``` interface IEmployee { empCode: number; empName: string; getSalary: (number) => number; // arrow function getManagerName(number): string; } ``` ### 声明合并 **声明合并是编译器随后合并两个或多个独立声明的过程。将具有相同名称的声明声明为单个定义。这个合并的定义具有两个原始声明的特性。** **最简单也是最常见的声明合并类型是接口合并。在最基本的层次上,merge将两个声明的成员机械地连接到一个具有相同名称的接口中。** **注: 在TypeScript中不是所有的合并都允许。目前,类不能与其他类或变量合并。** ### 模块 **TypeScript 中的模块是相关变量、函数、类和接口的集合。** **你可以将模块视为包含执行任务所需的一切的容器。可以导入模块以轻松地在项目之间共享代码。** ``` module module_name{ class xyz{ export sum(x, y){ return x+y; } } } ``` **TypeScript 中的模块可以在声明类、函数或变量之前使用 export 关键字来定义。然后可以使用 import 关键字以及包含导出的文件的路径将这些导出导入到另一个文件中。** ### 命名空间 **TypeScript 命名空间提供了一种以类似于模块的方式组织代码的方式,但语法略有不同。** **命名空间允许将代码分组在通用名称下,从而避免名称冲突。但是,在 TypeScript 中,命名空间不像模块那样常用。** **可以使用 namespace 关键字后跟命名空间名称来定义 TypeScript 命名空间。** **您可以使用声明关键字将代码添加到命名空间。** ``` namespace MyApp { export function doSomething() { console.log('Doing something'); } } MyApp.doSomething(); // Doing something ``` ### 类型断言 **TypeScript 中的类型断言的工作方式类似于其他语言中的类型转换,但没有 C# 和 Java 等语言中可能的类型检查或数据重组。类型断言对运行时没有影响,仅由编译器使用。** **类型断言本质上是类型转换的软版本,它建议编译器将变量视为某种类型,但如果它处于不同的形式,则不会强制它进入该模型。** ### var&let&const **var是严格范围变量的旧风格。你应该尽可能避免使用,var因为它会在较大的项目中导致问题。** **let是在 TypeScript 中声明变量的默认方式。与var相比,let减少了编译时错误的数量并提高了代码的可读性。** **const创建一个其值不能改变的常量变量。它使用相同的范围规则,let并有助于降低整体程序的复杂性。** ### 装饰器 **装饰器是一种特殊的声明,它允许你通过使用@**<name>**注释标记来一次性修改类或类成员。每个装饰器都必须引用一个将在运行时评估的函数。** **它们可以附加到:** * **类声明** * **方法** * **配件** * **特性** * **参数** **注意:默认情况下不启用装饰器。要启用它们,你必须experimentalDecorators从tsconfig.json文件或命令行编辑编译器选项中的字段。** ### 泛型 **TypeScript 中的泛型提供了一种通过编写可处理不同类型的函数、类和接口来编写可重用且灵活的代码的方法。** **泛型可以用来实现类型安全,提高代码的可读性和可维护性。** **TypeScript 中的泛型是在函数、类和接口声明中使用方括号 <> 定义的,带有类型的占位符。并且可以在使用函数、类或接口时指定形状。** ``` function identity<T>(arg: T): T { return arg; } const result = identity<string>('Hello'); console.log(result); // Hello ``` ### 函数重载 **要在 TypeScript 中重载函数,只需创建两个名称相同但参数/返回类型不同的函数。两个函数必须接受相同数量的参数。这是 TypeScript 中多态性的重要组成部分。** ``` function add(a:string, b:string):string; function add(a:number, b:number): number; function add(a: any, b:any): any { return a + b; } add("Hello ", "Steve"); // returns "Hello Steve" add(10, 20); // returns 30 ``` ### 三斜线指令 **三斜线指令是单行注释,包含用作编译器指令的 XML 标记。每个指令都表示在编译过程中要加载的内容。三斜杠指令仅在其文件的顶部工作,并且将被视为文件中其他任何地方的普通注释。** * **/// **<reference path="..." />** 是最常见的指令,定义文件之间的依赖关系。** * **/// **<reference types="..." />**类似于path但定义了包的依赖项。** * **/// **<reference lib="..." />**允许您显式包含内置lib文件。** ### rest参数 **其余参数允许你将不同数量的参数(零个或多个)传递给函数。当你不确定函数将接收多少参数时,这很有用。其余符号之后的所有参数**…**都将存储在一个数组中。** ``` function Greet(greeting: string, ...names: string[]) { return greeting + " " + names.join(", ") + "!"; } Greet("Hello", "Steve", "Bill"); // returns "Hello Steve, Bill!" Greet("Hello");// returns "Hello !" ``` **rest 参数必须是参数定义的最后一个,并且每个函数只能有一个 rest 参数。** ### 箭头/lambda 函数 **胖箭头函数是用于定义匿名函数的函数表达式的速记语法。它类似于其他语言中的 lambda 函数。箭头函数可让你跳过function关键字并编写更简洁的代码。** ``` let sum = (a: number, b: number): number => { return a + b; } console.log(sum(20, 30)); //returns 50 ``` ### 类&Js **类表示一组相关对象的共享行为和属性。** ``` class Student { studCode: number; studName: string; constructor(code: number, name: string) { this.studName = name; this.studCode = code; } } ``` **TypeScript 类提供了一种创建具有特定属性和方法的对象的方法。它们类似于其他面向对象编程语言中的类,提供了一种编写可重用和模块化代码的方法。** **TypeScript 中的类是 JavaScript 原型的语法糖,并被翻译成 JavaScript 的构造函数。** ### as语法 **as是TypeScript中类型断言的附加语法,引入as-语法的原因是原始语法(**<type>**)与JSX冲突。** **当使用带有JSX的TypeScript时,只允许as风格的断言。** ``` et empCode: any = 111; let employeeCode = code as number; ``` ### Omit 类型 **Omit 以一个类型为基础支持剔除某些属性,然后返回一个新类型。** ``` interface Todo { title: string; description: string; completed: boolean; createdAt: number; } type TodoPreview = Omit<Todo, "description">; ``` ### JSX 模式 **JSX 是一种可嵌入的类似于 XML 的语法,允许你创建 HTML。TypeScript 支持嵌入、类型检查和将 JSX 直接编译为 JavaScript。** **TypeScript有内置的支持preserve,react和react-native。** * **preserve 保持 JSX 完整以用于后续转换。** * **react不经过 JSX 转换,而是react.createElement作为.js文件扩展名发出和输出。** * **react-native结合起来preserve,react因为它维护所有 JSX 和输出作为.js扩展。** ### .map 文件 **甲.map文件是源地图,显示原始打字稿代码是如何解释成可用的JavaScript代码。它们有助于简化调试,因为你可以捕获任何奇怪的编译器行为。** **调试工具还可以使用这些文件来允许你编辑底层的 TypeScript 而不是发出的 JavaScript 文件。** ### getter/setter **Getter 和 setter 是特殊类型的方法,可帮助你根据程序的需要委派对私有变量的不同级别的访问。** **Getters 允许你引用一个值但不能编辑它。Setter 允许你更改变量的值,但不能查看其当前值。这些对于实现封装是必不可少的。** ``` const fullNameMaxLength = 10; class Employee { private _fullName: string = ""; get fullName(): string { return this._fullName; } set fullName(newName: string) { if (newName && newName.length > fullNameMaxLength) { throw new Error("fullName has a max length of " + fullNameMaxLength); } this._fullName = newName; } } let employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { console.log(employee.fullName); } ``` ### async/await **TypeScript 中的 async/await 用于编写易于阅读和维护的异步代码。异步函数返回一个promise,并且await 关键字可用于在继续执行代码之前等待promise的解决。这允许以类似于同步代码的方式编写异步代码,使其更易于理解和维护。** ``` async function fetchData(): Promise<string> { const response = await fetch('https://api.example.com'); const data = await response.json(); return data; } fetchData().then(data => { console.log(data); }); ``` ### 子类调用基类构造函数 **你可以使用该super()函数来调用基类的构造函数。** ``` class Animal { name: string; constructor(theName: string) { this.name = theName; } move(distanceInMeters: number = 0) { console.log(`${this.name} moved ${distanceInMeters}m.`); } } class Snake extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 5) { console.log("Slithering..."); super.move(distanceInMeters); } } ``` ### 哪些范围&Js对比 * **全局作用域:在任何类之外定义,可以在程序中的任何地方使用。** * **函数/类范围:在函数或类中定义的变量可以在该范围内的任何地方使用。** * **局部作用域/代码块:在局部作用域中定义的变量可以在该块中的任何地方使用。** **TypeScript 是 JavaScript 的开源语法超集,可编译为 JavaScript。所有原始 JavaScript 库和语法仍然有效,但 TypeScript 增加了 JavaScript 中没有的额外语法选项和编译器功能。** **TypeScript 还可以与大多数与 JavaScript 相同的技术接口,例如 Angular 和 jQuery。** ### 类型检查 **Typescript 使用类型系统在编译时执行类型检查。这可以通过允许在代码运行之前检测到错误来提高代码的可靠性和可维护性。** **TypeScript 的类型系统是可选的,因此,开发人员可以随心所欲地使用它。** ## 编译 **你需要调用 TypeScript 编译器tsc来编译文件。你需要安装 TypeScript 编译器,你可以使用npm.** ``` npm install -g typescript tsc <TypeScript File Name> ``` # [Java基础【前端中高概率】](https://javaguide.cn/java/basis/java-basic-questions-01.html) ## Integer能否直接用等号比较 **在Java中,对于基本数据类型(如int、float等),可以使用==运算符来比较它们的值是否相等;而对于对象类型(如Integer、Float等),则需要使用equals()方法来比较它们的值是否相等。** **虽然Integer类是一个对象类型,但是在一些情况下,由于Java的自动装箱和拆箱机制,Integer对象可以自动转换为int基本类型,从而可以直接使用==运算符来比较它们的值是否相等。例如:** ``` css Integer a = 10; int b = 10; if (a == b) { // do something } ``` **在这个例子中,由于Java会自动将Integer对象a转换为int基本类型,所以可以直接使用==运算符来比较a和b的值是否相等。但是,这种用法并不是推荐的,因为它可能会引发一些意外的问题。比如,当Integer的值大于127时,它们的地址可能不相同,导致使用==运算符比较结果不正确。** **因此,一般情况下,为了避免这种问题,应该使用equals()方法来比较Integer对象的值是否相等,例如:** ``` css Integer a = 10; Integer b = 10; if (a.equals(b)) { // do something } ``` **这种用法更加稳妥,可以避免因自动装箱和拆箱机制引发的问题。** ## 锁升级 **锁升级是指在多线程环境下,当线程在竞争同一把锁时,根据线程的状态和竞争情况,将锁的粒度从粗到细进行升级,以提高并发性能和减少线程等待时间的过程。** **在Java中,常见的锁升级方式包括以下几种:** 1. **自旋锁:当线程尝试获取锁时,如果发现锁已经被其他线程占用,则不断进行自旋等待,直到其他线程释放锁,或者达到最大自旋次数。** 2. **偏向锁:偏向锁是一种针对单线程的优化机制,当线程第一次获取锁时,将锁的持有线程ID记录下来,下次获取锁时直接判断是否是同一线程持有锁,避免了多次加锁和解锁的开销。** 3. **轻量级锁:轻量级锁是一种基于CAS(Compare and Swap)的乐观锁机制,当多个线程竞争同一把锁时,先使用CAS尝试将锁对象状态从“无锁状态”变为“轻量级锁状态”,如果成功,则表示获取锁成功;否则,需要升级锁。** 4. **重量级锁:重量级锁是一种基于操作系统互斥量的悲观锁机制,当轻量级锁升级失败后,就需要使用重量级锁来保证线程同步。** **总之,锁升级是一种优化多线程并发性能和减少线程等待时间的方法,Java中常见的锁升级方式包括自旋锁、偏向锁、轻量级锁和重量级锁等。开发者需要根据具体的需求和场景选择合适的锁升级方式,以提高并发性能和保证线程安全。** ## String为什么不能被继承 **在Java中,String被设计为final类,因此不能被继承。** **String类是Java中常用的字符串类型,它是通过字符数组实现的不可变对象。由于String类被设计为final类,因此它的所有方法都不能被重写,也不能通过继承来扩展其功能。这种设计是出于多方面的考虑,比如:** 1. **安全性:由于String对象是不可变的,因此可以避免在多线程环境下出现并发问题。** 2. **性能:由于String对象是不可变的,因此可以在多个地方共享同一个String对象,从而减少内存的占用和提高程序的性能。** 3. **设计上的考虑:String作为Java中最常用的类之一,其设计要求非常高,需要考虑各种场景下的使用情况,包括性能、安全性、可维护性等多个方面。** **总之,String被设计为final类,不能被继承,这种设计是出于多方面的考虑,包括安全性、性能、设计上的考虑等多个方面。在开发过程中,开发者可以通过其他方式来扩展String的功能,比如通过StringBuffer或StringBuilder类来进行字符串操作。** ## String和StringBuilder的区别 **String和StringBuilder都是Java中常用的字符串类型,它们之间的主要区别在于可变性、线程安全性和性能等方面。** 1. **可变性:String类型的对象是不可变的,即一旦创建,就无法更改其值,每次对String类型对象进行修改时,都会生成一个新的String对象,而原来的对象不会改变;而StringBuilder是可变的,可以进行增删改操作,每次修改不会创建新的对象,而是在原对象上进行修改。** 2. **线程安全性:String类型是线程安全的,即多线程访问同一个String对象时不会出现并发问题,而StringBuilder是非线程安全的,多线程访问同一个StringBuilder对象时需要进行同步处理,否则可能出现并发问题。** 3. **性能:由于String类型的对象是不可变的,每次进行字符串操作都需要创建新的对象,因此在大量的字符串拼接操作时,String的性能会比较低下;而StringBuilder是可变的,每次操作都是在原对象上进行,因此在大量的字符串操作时,StringBuilder的性能会比较高。** **总之,String和StringBuilder都是Java中常用的字符串类型,它们之间的区别主要在可变性、线程安全性和性能等方面。开发者需要根据具体的需求和场景选择合适的字符串类型,以保证代码的正确性和可靠性。** ## 双亲委派 **双亲委派(Parent Delegation)是Java类加载机制中的一种机制,它规定了类加载器在加载类时的行为。** **根据双亲委派机制,当一个类加载器(除了Bootstrap ClassLoader)需要加载一个类时,它首先会委托给它的父类加载器去加载,如果父类加载器无法加载,才会自己去加载。如果自己加载成功,会将加载结果缓存起来,并返回给请求者;如果加载失败,会返回ClassNotFoundException。** **这种机制有以下好处:** 1. **避免类的重复加载:由于每个类加载器都有自己的命名空间,如果没有双亲委派机制,可能会导致同一个类被不同的类加载器加载多次,从而引发各种问题。** 2. **提高安全性:由于Java类库的类都是由Bootstrap ClassLoader加载的,因此可以保证核心Java类库不会被非法篡改,提高了安全性。** 3. **提高效率:由于双亲委派机制可以避免重复加载类,从而减少了内存的消耗和运行时的开销,提高了效率。** **总之,双亲委派是Java类加载机制中的一种机制,它规定了类加载器在加载类时的行为,避免了类的重复加载,提高了安全性和效率。开发者在开发过程中需要了解这种机制,并且根据需要可以自定义类加载器来加载自己的类。** ## JavaGC机制 **百度:** **Java垃圾回收(Garbage Collection)机制是Java语言的一大特色,它通过自动回收内存中无用的对象,使得程序员不再需要关注内存管理问题。Java垃圾回收机制可以分为以下几个步骤:** 1. **标记:垃圾回收器首先会遍历整个堆内存,标记所有存活的对象。这个过程可以通过根搜索算法(Root Tracing Algorithm)实现,通过标记所有能够从程序的根对象(如线程栈、静态变量等)访问到的对象,判断哪些对象是存活的,哪些对象可以被回收。** 2. **清除:垃圾回收器会清除所有标记为无用的对象,并释放这些对象占用的内存空间。清除无用对象的过程称为“Sweeping”,可以使用可达性分析算法(Reachability Analysis)实现。** 3. **压缩:垃圾回收器在清除无用对象后,可能会在堆内存中产生内存碎片,妨碍了新对象的分配。为了解决这个问题,垃圾回收器会进行内存压缩操作,将存活的对象移动到一端,从而减少内存碎片,使得新对象的分配更加高效。** **Java垃圾回收机制还包括了以下两种优化策略:** 1. **分代垃圾回收:Java堆内存被分为不同的代,通常将Java堆内存分为Young Generation和Old Generation两个区域。Young Generation用于存放新生成的对象,Old Generation用于存放存活时间较长的对象。由于Young Generation中的对象生命周期较短,因此可以采用更快的垃圾回收算法,提高垃圾回收的效率。** 2. **并发垃圾回收:Java垃圾回收器采用了并发垃圾回收策略,即在应用程序运行时,垃圾回收器可以和应用程序线程并发执行,减少垃圾回收对应用程序的影响,提高应用程序的响应速度。** **总体来说,Java垃圾回收机制是一种自动化的内存管理机制,避免了程序员手动管理内存的问题。但是需要注意的是,垃圾回收器并不是完美的,它也会对应用程序的性能产生影响,因此需要合理地设置垃圾回收策略和垃圾回收器的参数,从而达到最优的垃圾回收效果。** **GPT3:** **垃圾回收(Garbage Collection)是现代编程语言中的一项重要特性,它可以自动回收不再使用的内存,从而避免了内存泄漏和内存溢出等问题。而垃圾回收的算法,是实现垃圾回收的核心。** **Java中的垃圾回收算法主要包括以下几种:** 1. **标记-清除算法(Mark-Sweep):这是一种最基本的垃圾回收算法,它通过标记所有可达的对象,然后清除所有未标记的对象来回收内存。但是,这种算法存在内存碎片的问题,可能会导致内存分配效率下降。** 2. **复制算法(Copy):这种算法将内存分为两个区域,每次只使用其中一个区域,当这个区域用完后,将其中还存活的对象复制到另一个区域,并将原来的区域全部清空,这样就可以避免内存碎片的问题。但是,这种算法需要较大的内存开销,因为每次只使用其中一个区域。** 3. **标记-整理算法(Mark-Compact):这种算法结合了标记-清除算法和复制算法的优点,它首先通过标记所有可达的对象,然后将所有存活的对象移到内存的一端,然后将剩余的空间全部清空,从而避免了内存碎片的问题。** 4. **分代收集算法(Generational):这种算法将内存分为不同的年代,根据对象的存活时间将其分为不同的年代,不同年代的对象采用不同的垃圾回收算法。一般来说,新生代的对象生命周期较短,采用复制算法;老年代的对象生命周期较长,采用标记-整理算法。** **总之,垃圾回收算法是实现垃圾回收的核心,Java中主要包括标记-清除算法、复制算法、标记-整理算法和分代收集算法等多种算法。在实际开发中,开发者需要了解各种算法的优缺点,并根据具体的需求和场景选择合适的算法来实现垃圾回收。** ## JNI **JNI(Java Native Interface)是Java提供的一种机制,用于在Java和本地代码(如C/C++)之间进行交互。在Android开发中,JNI常常用于将Java代码和C/C++代码混合编程,以实现高性能的计算和底层操作。** **下面是一些使用JNI的经验和技巧:** 1. **熟悉JNI的调用方式:JNI的调用方式较为复杂,需要了解JNIEnv、jclass、jobject等概念,以及JNIEnv提供的方法和接口。在开发过程中,可以参考JNI官方文档和相关教程,进行学习和实践。** 2. **使用Java Native Access(JNA)简化JNI调用:JNA是一种Java库,可以简化Java和本地代码之间的交互。它提供了一些高级接口和工具,可以使得JNI调用变得更加简单和易用。** 3. **对JNI函数进行异常处理:在JNI调用过程中,可能会发生异常情况,如内存泄露、空指针引用等。为了保证程序的稳定性和安全性,需要对JNI函数进行异常处理,及时释放资源,避免程序崩溃。** 4. **使用合适的数据类型:JNI支持多种数据类型的转换,如基本数据类型、数组、字符串等。在使用JNI时,需要根据具体的场景,选择合适的数据类型,从而实现高效的数据传递和处理。** 5. **避免频繁调用JNI函数:由于JNI调用是一种相对较慢的操作,因此需要避免频繁调用JNI函数,尽可能减少JNI调用次数,以提高程序的性能和效率。** **总的来说,JNI是一种非常有用的技术,可以将Java和本地代码混合编程,实现高性能的计算和底层操作。在使用JNI时,需要熟悉JNI调用方式,进行异常处理,选择合适的数据类型,避免频繁调用JNI函数,从而实现高效的JNI调用。** ## RxJava **作为一种强大的响应式编程框架,RxJava在Android应用开发中得到了广泛的应用。我作为一名智能助手,并没有具体的使用经验,但我了解RxJava的一些基本概念和用法,可以简单介绍一下:** 1. **观察者模式:RxJava基于观察者模式,通过Observable(被观察者)和Observer(观察者)两个对象进行数据传递和处理。** 2. **数据流操作:RxJava支持多种数据流操作,如变换、过滤、合并、分组等。通过组合这些操作,可以实现高效的数据流处理。** 3. **线程切换:RxJava支持线程切换,可以在主线程、IO线程、计算线程等多个线程中进行数据流处理,避免UI线程阻塞,提高程序的性能。** 4. **错误处理:RxJava提供了丰富的错误处理机制,如onErrorReturn、onErrorResumeNext、retry等方法,可以帮助开发人员更好地处理程序中的错误情况。** 5. **背压支持:RxJava 2.0之后,新增了对背压(backpressure)的支持,可以更好地处理数据流中的大量数据,避免内存泄漏和程序崩溃。** **总的来说,RxJava是一种非常强大的响应式编程框架,可以大幅度提高程序的性能和可维护性** ## HashMap实现机制&线程安全 **原理**: **HashMap是Java中常用的数据结构之一,它是基于哈希表实现的键值对存储结构。HashMap允许null作为键和值,支持线程不安全的操作,也可以通过Collections.synchronizedMap(Map)方法将HashMap包装成线程安全的Map。** **HashMap内部实现了一个哈希表,用于存储键值对。当添加一个键值对时,HashMap会根据键的哈希值来计算出存储位置,如果该位置已经被占用,则使用链表或红黑树来解决冲突,从而实现高效的键值对查找和插入。** **HashMap的核心数据结构是一个Node数组,每个Node节点包含了键、值和指向下一个节点的指针。当添加一个键值对时,HashMap会根据键的哈希值计算出数组下标,如果该位置没有节点,则直接添加节点。如果该位置已经存在节点,则使用链表或红黑树来解决冲突。如果链表长度大于等于8,则将链表转化为红黑树,从而提高查找效率。** **以下是HashMap的基本操作:** 1. **添加键值对:使用put(key, value)方法添加一个键值对。** 2. **获取值:使用get(key)方法获取对应键的值。** 3. **删除键值对:使用remove(key)方法删除指定键的键值对。** 4. **遍历键值对:使用entrySet()方法获取所有键值对的Set集合,然后进行遍历操作。** **HashMap的时间复杂度主要取决于哈希表的大小和冲突情况,最坏情况下为O(n),平均情况下为O(1)。在实际开发中,可以通过调整负载因子和初始化容量来优化HashMap的性能。** **HashMap是一种基于哈希表的数据结构,用于存储键值对。HashMap的****实现机制**如下: 1. **数组+链表/红黑树:HashMap内部维护了一个数组,每个数组元素称为桶,每个桶可以存储多个键值对。如果多个键值对的哈希值相同,那么它们会被存储在同一个桶中。当桶中的元素数量超过了一定阈值,HashMap会将链表转换为红黑树,提高查找效率。** 2. **哈希函数:HashMap使用哈希函数将键映射到数组索引位置。在Java中,哈希函数通常是由对象的hashCode()方法和位运算符组成的。** 3. **链表/红黑树:每个桶中存储的元素是一个链表或者红黑树。当桶中元素数量比较少时,使用链表,当元素数量比较多时,将链表转换为红黑树,提高查找效率。** **HashMap****不是线程安全**的,如果在多线程环境下使用HashMap可能会出现线程安全问题。常见的解决方案有以下几种: 1. **使用Collections.synchronizedMap()方法创建同步的HashMap,该方法返回一个线程安全的Map对象,但是在高并发场景下性能较差。** 2. **使用ConcurrentHashMap代替HashMap,ConcurrentHashMap是一种线程安全的哈希表实现,它使用锁分离技术,通过将哈希表分成多个Segment,在每个Segment上进行独立的操作,从而实现线程安全和高并发。** 3. **使用读写锁(ReentrantReadWriteLock)实现HashMap的线程安全,读取操作可以同时执行,写操作需要互斥,可以提高并发度。** **总的来说,线程安全是在高并发场景下必须考虑的问题,开发人员需要根据具体的场景选择合适的线程安全方案,从而确保程序的正确性和高性能。** ## ThreadLocal **在Java中,ThreadLocal是一个线程本地变量,它提供了一种在多线程环境下,使得每个线程都可以拥有自己的变量副本的方法。ThreadLocal可以用来解决多线程并发访问时的数据共享问题,从而提高了应用程序的性能和并发能力。** **使用ThreadLocal需要注意以下几点:** 1. **每个线程拥有自己的变量副本:使用ThreadLocal可以让每个线程拥有自己的变量副本,这样每个线程可以独立地操作自己的变量,而不会影响其他线程。** 2. **初始值:可以为ThreadLocal设置一个初始值,这个值会被赋给第一次访问该ThreadLocal的线程。** 3. **内存泄漏:如果使用不当,ThreadLocal可能会导致内存泄漏问题。当一个线程结束时,如果没有手动将其使用的ThreadLocal对象清除掉,那么这些对象将会一直存在于内存中,直到应用程序结束。** 4. **适用场景:ThreadLocal适用于多线程环境下,需要将数据与线程进行绑定的场景,如线程池、Web应用程序中的会话管理等。** **ThreadLocal是一种非常实用的工具,但也需要谨慎使用。在使用ThreadLocal时,应该注意避免内存泄漏问题,并且确保使用场景符合ThreadLocal的适用范围。** ## <? extends T>不能add元素 **使用**`<? extends T>`作为泛型限定符时,表示泛型类型参数是T的一个子类,但具体是哪个子类是不确定的。在这种情况下,不能对其进行添加元素的操作,因为我们不知道这个具体的子类的类型参数是否支持添加。在这种情况下,只能进行读取操作,例如从列表中获取元素,但不能对其进行添加、修改或删除元素的操作。这是由于Java泛型类型擦除的机制所导致的,具体类型信息在运行时是不可知的,因此编译器无法保证添加的元素类型是安全的。如果需要对列表进行添加元素的操作,应该使用`<? super T>`作为泛型限定符,表示泛型类型参数是T的一个超类。这样做可以确保添加元素时是安全的,因为我们知道类型参数是T的超类,可以接受T或T的任何子类。 ## 重写equals方法需要重写hashCode方法 **在Java中,如果你重写了一个对象的equals方法,那么必须同时重写它的hashCode方法,以保证它们在使用哈希表等数据结构时能够正常工作。** **hashCode方法是用来计算哈希值的,它的返回值用于在哈希表中确定对象的存储位置。而equals方法则用来比较两个对象是否相等。在使用哈希表等数据结构时,当两个对象的hashCode值相等时,它们会被认为是相等的,因此必须确保两个相等的对象具有相同的hashCode值,否则它们可能会被哈希表存储到不同的位置,从而导致无法正确地进行查找和比较。** **因此,如果你重写了一个对象的equals方法,那么必须同时重写它的hashCode方法,并且保证相等的对象具有相同的hashCode值。通常情况下,可以使用对象的所有字段来计算hashCode值,以确保相等的对象具有相同的hashCode值。如果hashCode方法的实现不正确,可能会导致哈希表等数据结构无法正常工作,从而影响应用程序的正确性和性能。** # 游戏 ## 砖块破碎效果 **砖块破碎效果通常用于游戏中的打砖块游戏等场景,其实现主要包括以下步骤:** 1. **确定砖块的碰撞检测:在游戏中,需要检测小球与砖块之间的碰撞情况,通常采用物理引擎进行碰撞检测。** 2. **确定砖块的生命值:为了实现砖块破碎的效果,需要为每个砖块设置生命值属性。当小球撞击砖块时,生命值减少,直到生命值为0时,砖块就会被破碎。** 3. **砖块的破碎动画效果:当砖块生命值为0时,需要展现破碎的动画效果。可以使用帧动画或者粒子效果等技术来实现破碎动画。** 4. **砖块的消失效果:当砖块破碎后,需要从游戏场景中移除该砖块,并且在游戏分数中增加相应的分数。** **总之,砖块破碎效果是游戏开发中比较常见的一种效果,需要通过物理引擎检测碰撞、设置砖块生命值、实现破碎动画和砖块消失效果等步骤来实现。** ## 帧同步&状态同步 ### 帧同步 **帧同步适用于需要高精度时间控制的游戏,如竞技游戏和格斗游戏。在这些游戏中,需要精确控制每个帧的数据,以确保游戏的平衡性和公平性。** **在帧同步中,常用的协议有以下几种:** 1. **UDP协议:UDP协议是一种不可靠的协议,可以提高数据传输的速度,适用于需要低延迟和高速传输的场景。在帧同步中,UDP协议通常用于传输游戏状态数据。** 2. **KCP协议:KCP协议是一种快速可靠的协议,可以提供高效的帧同步数据传输,适用于帧同步中。** 3. **RUDP协议:RUDP协议是一种基于UDP协议的可靠数据传输协议,可以在保证传输速度的同时,确保数据的可靠性和有序性,适用于帧同步中。** 4. **ENet协议:ENet协议是一种轻量级的网络通信库,适用于实现高效、可靠和安全的网络通信,适用于帧同步中。** **总的来说,帧同步的使用场景主要是需要高精度时间控制的游戏,常用的协议包括UDP、KCP、RUDP和ENet协议。选择合适的协议需要根据实际情况进行权衡和选择。** ### 状态同步 **状态同步适用于需要保持游戏状态一致性的网络游戏,如MMORPG游戏和射击游戏。这类游戏中,多个玩家同时参与游戏,需要保持游戏状态的一致性,以确保玩家之间的游戏体验和公平性。状态同步可以确保客户端和服务器之间的状态保持一致,从而保证游戏的公平性和稳定性。** **在状态同步中,常用的协议有以下几种:** 1. **TCP协议:TCP协议是一种可靠的协议,可以确保数据传输的可靠性和有序性,因此适用于状态同步中。但是,TCP协议的缺点是延迟较高,可能会影响游戏体验。** 2. **UDP协议:UDP协议是一种不可靠的协议,可以提高数据传输的速度,适用于一些需要低延迟和高速传输的场景。在状态同步中,UDP协议通常用于传输非关键数据,如玩家位置等。** 3. **RUDP协议:RUDP协议是一种基于UDP协议的可靠数据传输协议,可以在保证传输速度的同时,确保数据的可靠性和有序性,适用于状态同步中。** 4. **ENet协议:ENet协议是一种轻量级的网络通信库,适用于实现高效、可靠和安全的网络通信,适用于状态同步中。** **总的来说,状态同步的使用场景主要是需要保持游戏状态一致性的网络游戏,常用的协议包括TCP、UDP、RUDP和ENet协议。选择合适的协议需要根据实际情况进行权衡和选择。** ### 区别 **帧同步和状态同步是网络游戏中两种常用的同步方式,它们有以下的区别:** 1. **数据传输方式:帧同步是以时间为单位将游戏状态在客户端和服务器之间进行同步,状态同步是将游戏状态在客户端和服务器之间进行传输,以确保客户端和服务器的状态保持一致。** 2. **网络延迟处理方式:帧同步是等待客户端和服务器都收到同一个帧的数据后再进行下一帧的数据传输,状态同步是通过根据最慢的客户端的延迟来保证数据的一致性。** 3. **适用范围:帧同步适用于一些需要高精度时间控制的游戏,如竞技游戏和格斗游戏,而状态同步更适用于一些需要保持游戏状态一致性的游戏,如MMORPG游戏和射击游戏。** 4. **编程复杂度:相对而言,帧同步的编程复杂度要高于状态同步。因为帧同步需要在客户端和服务器之间进行严格的时间同步,需要考虑多方面的因素,如网络延迟、帧率、同步精度等。** **总的来说,帧同步和状态同步都是网络游戏中常用的同步方式。在选择同步方式时,需要根据游戏的特点和需求来确定合适的同步方式,并根据实际情况进行调整和优化。** ## 网络延迟太高,如何优化体验 **如果网络延迟太高,玩家操作与服务器之间的往返时间过长,会导致游戏看起来卡顿,玩家体验非常差。为了提高玩家的游戏体验,可以使用插值算法来平滑游戏的显示效果。** **插值算法通常可以在客户端上实现,具体步骤如下:** 1. **记录服务器和客户端之间的网络延迟:在客户端上记录每次与服务器通信的网络延迟,并计算出平均延迟和标准差。** 2. **记录客户端的操作:客户端应该记录每次操作的时间戳,以及操作对应的状态。** 3. **实现插值算法:在客户端上实现插值算法,通过计算当前时间与上一个操作时间之间的时间差,以及延迟时间和标准差,来计算出需要插值的操作状态,从而平滑游戏的显示效果。** 4. **对插值后的操作进行渲染:客户端应该对插值后的操作状态进行渲染,从而展现平滑的游戏效果。** **总之,插值算法可以帮助在网络延迟较高的情况下,通过对两帧之间的操作进行插值运算,从而实现平滑的游戏显示效果。在客户端上记录网络延迟和实现插值算法是实现此功能的关键步骤。** ## 渲染管线 **渲染管线(Graphics Rendering Pipeline)是计算机图形学中的一个概念,指的是将3D场景中的图形对象转换为2D屏幕上的像素的过程。渲染管线可以分为以下几个基本步骤:** 1. **几何处理阶段(Geometry Processing Stage):该阶段主要是对输入的3D场景中的几何对象进行处理,包括顶点数据的处理、坐标变换、三角形剪裁等操作。这个阶段的输出是顶点数据和相应的顶点属性。** 2. **光栅化阶段(Rasterization Stage):该阶段将几何处理阶段输出的顶点数据转换为屏幕上的像素,计算出每个像素对应的颜色和深度值。这个阶段的输出是屏幕上的像素。** 3. **片元处理阶段(Fragment Processing Stage):该阶段对每个像素进行处理,包括对像素的颜色进行插值、应用纹理、进行深度测试等操作,最终输出每个像素的最终颜色值。** 4. **输出合成阶段(Output Merger Stage):该阶段将片元处理阶段输出的像素颜色值进行合并,生成最终的屏幕图像。** **总之,渲染管线是将3D场景中的几何对象转换为2D屏幕上的像素的过程,包括几何处理阶段、光栅化阶段、片元处理阶段和输出合成阶段等基本步骤。不同的图形API和图形硬件可以有不同的实现方式,但是大部分渲染管线的基本流程是相似的。** # [Go基础【前端低概率】](https://blog.csdn.net/weixin_50941083/article/details/125590486) # [C基础【前端低概率】](https://blog.csdn.net/weixin_55305220/article/details/116665000) # 计算机基础【高概率】 ## 设计模式【专业基础(中概率)】 ### MVP 和 MVVM 1. **MVP (Model-View-Presenter):MVP 是一种基于分层架构的设计模式,用于将应用的数据模型、用户界面和业务逻辑分离。在 MVP 中,Model 负责处理应用的数据模型,View 负责呈现用户界面,Presenter 作为中间层协调 Model 和 View 之间的通信和交互。MVP 可以提高应用的可测试性和可维护性,同时也可以提高开发效率和代码复用度。** 2. **MVVM (Model-View-ViewModel):MVVM 是一种基于数据绑定的设计模式,用于将应用的数据模型、用户界面和业务逻辑分离。在 MVVM 中,Model 负责处理应用的数据模型,View 负责呈现用户界面,ViewModel 作为中间层负责将数据绑定到 View 上,并响应用户的输入事件。MVVM 可以提高应用的可测试性和可维护性,同时也可以提高用户体验和开发效率。** ### 多人合作一致性 **如果有多人合作开发一个框架,为了确保各自的功能不冲突,可以采用以下方法:** 1. **规范接口和功能模块:为了避免不同开发人员之间的功能模块和接口之间的冲突,可以在开发之前定义好规范的接口和功能模块,并对其进行文档化和版本控制。** 2. **拆分模块:可以将整个框架分为多个小模块,每个开发人员只负责开发自己的模块,避免多人同时修改同一个模块而导致的冲突。** 3. **使用版本控制工具:使用版本控制工具(如Git)可以帮助开发人员更好地管理代码,确保不同开发人员的代码不会冲突,同时也可以回退到之前的版本以便修复错误。** 4. **定期同步和集成:开发人员应该定期进行代码同步和集成,以确保所有人的代码都是最新的,并能够发现和解决冲突。** 5. **进行代码审查:进行代码审查可以帮助开发人员发现和解决潜在的冲突和错误。** 6. **保持沟通:开发人员之间应该保持良好的沟通,及时协商解决可能存在的冲突或问题。** **总之,合作开发一个框架需要开发人员之间保持良好的协作和沟通,并采用一些有效的方法来避免冲突和错误。** ### 优点和弊端 **设计模式是一种在软件开发中广泛应用的经验性方法,它是对软件设计中经常出现的问题和解决方案的一种总结和抽象。设计模式提供了一套通用的解决方案,能够帮助软件开发人员更快、更高效地解决常见的设计问题。** **设计模式的优点包括:** 1. **提高代码重用性:设计模式提供了一些经过验证的解决方案,能够帮助开发人员更快速地设计出可重用的代码。** 2. **提高代码的可读性和可维护性:设计模式采用通用的解决方案,可以提高代码的可读性和可维护性。** 3. **更好的系统设计:设计模式可以帮助开发人员更好地设计出可扩展、可重用的系统。** 4. **提高开发效率:设计模式可以帮助开发人员更快速地解决问题,从而提高开发效率。** **但是,设计模式也存在一些弊端:** 1. **学习曲线陡峭:设计模式需要开发人员具备一定的经验和知识储备,因此需要花费时间学习。** 2. **可能过度设计:过度使用设计模式可能会导致代码过于复杂,增加维护的难度。** 3. **不适用于所有情况:设计模式并不是万能的,有些场景可能并不适合应用某些设计模式。** **总的来说,设计模式在软件开发中具有重要的意义和价值。通过运用设计模式,可以提高代码质量和开发效率,降低系统的维护成本。但是,在使用设计模式时需要注意不要过度设计,以及要根据实际情况选择合适的设计模式。** **常见的设计模式:**  **常见的软件设计原则:**  **设计模式是一种经过实践验证的、被广泛应用的软件开发经验的总结,它包含了一系列常用的设计思想和方法,能够帮助开发人员更好地解决软件设计和实现中的问题。** **我作为一名智能助手,了解常用的设计模式,其中一些主要的设计模式包括:** 1. **单例模式:确保一个类只有一个实例,并提供全局访问点。** 2. **工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。** 3. **观察者模式:定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。** 4. **适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而无法一起工作的那些类可以一起工作。** 5. **建造者模式:将一个复杂对象的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。** **在开发过程中,我见过的最常用的设计模式是单例模式、工厂模式和观察者模式。单例模式可以保证一个类只有一个实例,避免资源浪费和程序崩溃。工厂模式可以将对象的创建和使用分离,提高代码的灵活性和可扩展性。观察者模式可以让多个对象之间的依赖关系更加松耦合,避免出现紧耦合的代码。** **总的来说,设计模式是软件开发中非常重要的一部分,能够帮助开发人员更好地解决软件设计和实现中的问题,提高程序的可维护性、可扩展性和可读性。在实际的开发过程中,需要根据具体的业务场景和需求,选择合适的设计模式,并遵循设计模式的原则和规范,从而编写出高质量、易维护的代码。** **[23 种设计模式详解(全23种)](https://blog.csdn.net/qq_25086397/article/details/125103647)** ## 软工专业基础【**专业基础(中高概率)**】 ### 何为软件工程? **软件工程指的就是将工程思想应用于软件开发。** **上面是我对软件工程的定义,我们再来看看比较权威的定义。IEEE 软件工程汇刊给出的定义是这样的: (1)将系统化的、规范的、可量化的方法应用到软件的开发、运行及维护中,即将工程化方法应用于软件。 (2)在(1)中所述方法的研究。** **总之,软件工程的终极目标就是:****在更少资源消耗的情况下,创造出更好、更容易维护的软件。** ### 软件开发过程 > **软件开发过程(英语:software development process),或软件过程(英语:software process),是软件开发的开发生命周期(software development life cycle),其各个阶段实现了软件的需求定义与分析、设计、实现、测试、交付和维护。软件过程是在开发与构建系统时应遵循的步骤,是软件开发的路线图。** * **需求分析 :分析用户的需求,建立逻辑模型。** * **软件设计 : 根据需求分析的结果对软件架构进行设计。** * **编码 :编写程序运行的源代码。** * **测试 : 确定测试用例,编写测试报告。** * **交付 :将做好的软件交付给客户。** * **维护 :对软件进行维护比如解决 bug,完善功能。** ### 蔚蓝的冲刺 **蔚蓝的冲刺(Blue Ocean Sprint)是敏捷软件开发方法中的一种实践方法,旨在推动敏捷团队的创新和成果交付速度。** **蔚蓝的冲刺通常在Scrum框架的Sprint之间进行,其目的是为了在短时间内聚焦于特定的开发目标,例如新功能的开发或者重构代码等。其主要实现效果如下:** 1. **提高成果交付速度:蔚蓝的冲刺鼓励团队在短时间内完成特定的开发目标,这有助于提高成果交付的速度,减少项目开发周期。** 2. **提高团队的创新能力:通过蔚蓝的冲刺,团队可以聚焦于特定的开发目标,从而更好地发掘和实现新的创新点。** 3. **提高团队合作能力:蔚蓝的冲刺需要团队成员在较短的时间内完成开发目标,这需要团队成员更好地协作和配合,从而提高团队合作能力。** 4. **增强团队动力:蔚蓝的冲刺可以帮助团队成员更好地实现目标,从而增强团队成员的动力和自信心。** **总的来说,蔚蓝的冲刺是一种有效的实践方法,可以帮助敏捷团队更好地实现目标,提高开发效率和质量,同时也有助于提高团队的合作和创新能力。** ### 软件开发模型 **软件开发模型有很多种,比如瀑布模型(Waterfall Model)、快速原型模型(Rapid Prototype Model)、V模型(V-model)、W模型(W-model)、敏捷开发模型。其中最具有代表性的还是 ****瀑布模型** 和 **敏捷开发** 。 **瀑布模型** 定义了一套完成的软件开发周期,完整地展示了一个软件的的生命周期。  **敏捷开发模型** 是目前使用的最多的一种软件开发模型。[MBA智库百科对敏捷开发的描述](https://wiki.mbalib.com/wiki/%E6%95%8F%E6%8D%B7%E5%BC%80%E5%8F%91) **是这样的:** > **敏捷开发** 是一种以人为核心、迭代、循序渐进的开发方法。在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。换言之,就是把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态。 **像现在比较常见的一些概念比如 ****持续集成** 、**重构** 、**小版本发布** 、**低文档** 、**站会** 、**结对编程** 、**测试驱动开发** 都是敏捷开发的核心。 ### 软件开发的基本策略 #### 软件复用 **我们在构建一个新的软件的时候,不需要从零开始,通过复用已有的一些轮子(框架、第三方库等)、设计模式、设计原则等等现成的物料,我们可以更快地构建出一个满足要求的软件。** **像我们平时接触的开源项目就是最好的例子。我想,如果不是开源,我们构建出一个满足要求的软件,耗费的精力和时间要比现在多的多!** #### 分而治之 **构建软件的过程中,我们会遇到很多问题。我们可以将一些比较复杂的问题拆解为一些小问题,然后,一一攻克。** **我结合现在比较火的软件设计方法—领域驱动设计(Domain Driven Design,简称 DDD)来说说。** **在领域驱动设计中,很重要的一个概念就是****领域(Domain)**,它就是我们要解决的问题。在领域驱动设计中,我们要做的就是把比较大的领域(问题)拆解为若干的小领域(子域)。 #### 逐步演进 **软件开发是一个逐步演进的过程,我们需要不断进行迭代式增量开发,最终交付符合客户价值的产品。** **这里补充一个在软件开发领域,非常重要的概念:****MVP(Minimum Viable Product,最小可行产品**)。 **这个最小可行产品,可以理解为刚好能够满足客户需求的产品。利用最小可行产品,我们可以也可以提早进行市场分析,这对于我们在探索产品不确定性的道路上非常有帮助。可以非常有效地指导我们下一步该往哪里走。** #### 优化折中 **软件开发是一个不断优化改进的过程。任何软件都有很多可以优化的点,不可能完美。我们需要不断改进和提升软件的质量。** **但是,也不要陷入这个怪圈。要学会折中,在有限的投入内,以最有效的方式提高现有软件的质量。** ## 计算机网络 ### 浏览器中输入URL返回页面过程? 1. **解析域名,找到主机 IP。** 2. **浏览器利用 IP 直接与网站主机通信,三次握手,建立 TCP 连接。浏览器会以一个随机端口向服务端的 web 程序 80 端口发起 TCP 的连接。** 3. **建立 TCP 连接后,浏览器向主机发起一个HTTP请求。** 4. **参数从客户端传递到服务器端。** 5. **服务器端得到客户端参数之后,进行相应的业务处理,再将结果封装成 HTTP 包,返回给客户端。** 6. **服务器端和客户端的交互完成,断开 TCP 连接(4 次挥手)。** 7. **浏览器解析响应内容,进行渲染,呈现给用户。** ### DNS 域名解析的过程 **在网络中定位是依靠 IP 进行身份定位的,所以 URL 访问的第一步便是先要得到服务器端的 IP 地址。而得到服务器的 IP 地址需要使用 DNS(Domain Name System,域名系统)域名解析,DNS 域名解析就是通过 URL 找到与之相对应的 IP 地址。** **DNS 域名解析的大致流程如下:** 1. **先检查浏览器中的 DNS 缓存,如果浏览器中有对应的记录会直接使用,并完成解析;** 2. **如果浏览器没有缓存,那就去查询操作系统的缓存,如果查询到记录就可以直接返回 IP 地址,完成解析;** 3. **如果操作系统没有 DNS 缓存,就会去查看本地 host 文件,Windows 操作系统下,host 文件一般位于 **“**C:\Windows\System32\drivers\etc\hosts**”**,如果 host 文件有记录则直接使用;** 4. **如果本地 host 文件没有相应的记录,会请求本地 DNS 服务器,本地 DNS 服务器一般是由本地网络服务商如移动、联通等提供。通常情况下可通过 DHCP 自动分配,当然也可以自己手动配置。目前用的比较多的是谷歌提供的公用 DNS 是 8.8.8.8 和国内的公用 DNS 是 114.114.114.114。** 5. **如果本地 DNS 服务器没有相应的记录,就会去根域名服务器查询了。为了能更高效完成全球所有域名的解析请求,根域名服务器本身并不会直接去解析域名,而是会把不同的解析请求分配给下面的其他服务器去完成。** ### OSI 七层模型  **OSI 的专家缺乏实际经验,他们在完成 OSI 标准时缺乏商业驱动力** **OSI 的协议实现起来过分复杂,而且运行效率很低** **OSI 制定标准的周期太长,因而使得按 OSI 标准生产的设备无法及时进入市场(20 世纪 90 年代初期,虽然整套的 OSI 国际标准都已经制定出来,但基于 TCP/IP 的互联网已经抢先在全球相当大的范围成功运行了)** **OSI 的层次划分不太合理,有些功能在多个层次中重复出现。** ### 说说TCP报文首部有哪些字段,其作用又分别是什么? * **16位端口号:源端口号,主机该报文段是来自哪里;目标端口号,要传给哪个上层协议或应用程序** * **32位序号:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。** * **32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到的TCP报文段的序号值加1。** * **4位头部长度:表示tcp头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。** * **6位标志位:URG(紧急指针是否有效),ACk(表示确认号是否有效),PSH(缓冲区尚未填满),RST(表示要求对方重新建立连接),SYN(建立连接消息标志接),FIN(表示告知对方本端要关闭连接了)** * **16位窗口大小:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。** * **16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。** * **16位紧急指针:一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。** ### TCP有哪些特点? * **TCP是面向连接的运输层协议。** * **点对点,每一条TCP连接只能有两个端点。** * **TCP提供可靠交付的服务。** * **TCP提供全双工通信。** * **面向字节流。** ### TCP和UDP的区别? 1. **TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接。** 2. **TCP提供可靠的服务;UDP不保证可靠交付。** 3. **TCP面向字节流,把数据看成一连串无结构的字节流;UDP是面向报文的。** 4. **TCP有拥塞控制;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。** 5. **每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的通信方式。** 6. **TCP首部开销20字节;UDP的首部开销小,只有8个字节。** ### TCP 和 UDP 分别对应的常见应用层协议有哪些? **基于TCP的应用层协议有:HTTP、FTP、SMTP、TELNET、SSH** * **HTTP:HyperText Transfer Protocol(超文本传输协议),默认端口80** * **FTP: File Transfer Protocol (文件传输协议), 默认端口(20用于传输数据,21用于传输控制信息)** * **SMTP: Simple Mail Transfer Protocol (简单邮件传输协议) ,默认端口25** * **TELNET: Teletype over the Network (网络电传), 默认端口23** * **SSH:Secure Shell(安全外壳协议),默认端口 22** **基于UDP的应用层协议:DNS、TFTP、SNMP** * **DNS : Domain Name Service (域名服务),默认端口 53** * **TFTP: Trivial File Transfer Protocol (简单文件传输协议),默认端口69** * **SNMP:Simple Network Management Protocol(简单网络管理协议),通过UDP端口161接收,只有Trap信息采用UDP端口162。** ### TCP的粘包和拆包 **TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。** **为什么会产生粘包和拆包呢?** * **要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;** * **接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;** * **要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;** * **待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。** **解决方案:** * **发送端将每个数据包封装为固定长度** * **在数据尾部增加特殊字符进行分割** * **将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。** ### 说说TCP是如何确保可靠性的呢? * **首先,TCP的连接是基于三次握手,而断开则是基于四次挥手。确保连接和断开的可靠性。** * **其次,TCP的可靠性,还体现在有状态;TCP会记录哪些数据发送了,哪些数据被接收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。** * **再次,TCP的可靠性,还体现在可控制。它有数据包校验、ACK应答、超时重传(发送方)、失序数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)和拥塞控制等机制。** ### TCP的滑动窗口机制 **TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。** **TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。** ### TCP/IP 四层模型 1. **应用层** 2. **传输层** 3. **网络层** 4. **网络接口层**  #### 应用层 **应用层位于传输层之上,主要提供两个终端设备上的应用程序之间信息交换的服务,它定义了信息交换的格式,消息会交给下一层传输层来传输。** 我们把应用层交互的数据单元称为报文。 **协议:** 1. **HTTP:超文本传输协议** 2. **SMTP:简单邮件传输(发送)协议** 3. **POP3/IMAP:邮件接收的协议** 4. **FTP:文件传输协议 ** 5. **Telnet:远程登陆协议 ** 6. **SSH:安全的网络传输协议** #### 传输层 **传输层的主要任务就是负责向两台终端设备进程之间的通信提供通用的数据传输服务。** 应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。 **运输层主要使用以下两种协议:** 1. **传输控制协议 TCP**(Transmisson Control Protocol)**–**提供 **面向连接** 的,**可靠的** 数据传输服务。 2. **用户数据协议 UDP**(User Datagram Protocol)**–**提供 **无连接** 的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。 #### 网络层 **网络层负责为分组交换网上的不同主机提供通信服务。** 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报,简称数据报。 **⚠️注意 :****不要把运输层的“用户数据报 UDP”和网络层的“IP 数据报”弄混**。 **网络层的还有一个任务就是选择合适的路由,使源主机运输层所传下来的分组,能通过网络层中的路由器找到目的主机。** **这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称。** **互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Internet Prococol)和许多路由选择协议,因此互联网的网络层也叫做 ****网际层** 或 **IP 层**。 **网络层常见协议** : * **IP:网际协议** :网际协议 IP 是TCP/IP协议中最重要的协议之一,也是网络层最重要的协议之一,IP协议的作用包括寻址规约、定义数据包的格式等等,是网络层信息传输的主力协议。目前IP协议主要分为两种,一种是过去的IPv4,另一种是较新的IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。 * **ARP 协议** :ARP协议,全称地址解析协议(Address Resolution Protocol),它解决的是网络层地址和链路层地址之间的转换问题。因为一个IP数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但IP地址属于逻辑地址,而MAC地址才是物理地址,ARP协议解决了IP地址转MAC地址的一些问题。 * **NAT:网络地址转换协议** :NAT协议(Network Address Translation)的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,LAN)内,各主机使用的是同一个LAN下的IP地址,但在该LAN以外,在广域网(WAN)中,需要一个统一的IP地址来标识该LAN在整个Internet上的位置。 #### 网络接口层 **我们可以把网络接口层看作是数据链路层和物理层的合体。** 1. **数据链路层(data link layer)通常简称为链路层( 两台主机之间的数据传输,总是在一段一段的链路上传送的)。****数据链路层的作用是将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。** 2. **物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异** ### 网络分层的原因 **各层之间相互独立**:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)**。这个和我们对开发时系统进行分层是一个道理。** **提高了整体灵活性** :每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。**这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。** **大问题化小** : 分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 **这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。** ### TCP 与 UDP对比图 | | **TCP** | **UDP** | | ---------------------------- | -------------------- | ---------------- | | **是否面向连接** | **是** | **否** | | **是否可靠** | **是** | **否** | | **是否有状态** | **是** | **否** | | **传输效率** | **较慢** | **较快** | | **传输形式** | **字节流** | **数据报文段** | | **首部开销** | **20 ~ 60 bytes** | **8 bytes** | | **是否提供广播或多播服务** | **否** | **是** | ### 什么时候选择 TCP,什么时候选 UDP? * **UDP 一般用于即时通信**,比如: 语音、 视频 、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。 * **TCP 用于对传输准确性要求特别高的场景**,比如文件传输、发送和接收邮件、远程登录等等。 #### HTTP 基于 TCP 还是 UDP? **HTTP 3.0 之前是基于 TCP 协议的,而 HTTP3.0 将弃用 TCP,改用 ****基于 UDP 的 QUIC 协议** 。此变化主要为了解决 HTTP/2 中存在的队头阻塞问题。由于 HTTP/2 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。 #### 使用 TCP 的协议有哪些?使用 UDP 的协议有哪些? **运行于 TCP 协议之上的协议** : 1. **HTTP 协议** :超文本传输协议(HTTP,HyperText Transfer Protocol)主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。 2. **HTTPS 协议** :更安全的超文本传输协议(HTTPS,Hypertext Transfer Protocol Secure),身披 SSL 外衣的 HTTP 协议 3. **FTP 协议**:文件传输协议 FTP(File Transfer Protocol),提供文件传输服务,**基于 TCP** 实现可靠的传输。使用 FTP 传输文件的好处是可以屏蔽操作系统和文件存储方式。 4. **SMTP 协议**:简单邮件传输协议(SMTP,Simple Mail Transfer Protocol)的缩写,**基于 TCP 协议**,用来发送电子邮件。注意 ⚠️:接受邮件的协议不是 SMTP 而是 POP3 协议。 5. **POP3/IMAP 协议**: POP3 和 IMAP 两者都是负责邮件接收的协议。 6. **Telnet 协议**:远程登陆协议,通过一个终端登陆到其他服务器。被一种称为 SSH 的非常安全的协议所取代。 7. **SSH 协议** : SSH( Secure Shell)是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH 建立在可靠的传输协议 TCP 之上。 **运行于 UDP 协议之上的协议** : 1. **DHCP 协议**:动态主机配置协议,动态配置 IP 地址 2. **DNS** : **域名系统(DNS,Domain Name System)将人类可读的域名 (例如,[www.baidu.com](www.baidu.com)) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。** 我们可以将其理解为专为互联网设计的电话薄。实际上 DNS 同时支持 UDP 和 TCP 协议。 ### TCP 三次握手和四次挥手 #### 三次握手 **百度:** **一次握手**:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 **SYN_SEND** 状态,等待服务器的确认; **二次握手**:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 **SYN_RECV** 状态 **三次握手**:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入**ESTABLISHED** 状态,完成TCP三次握手。 **GPT3:** 1. **客户端向服务器发送SYN包,其中SYN标志位被设置为1,表示客户端请求建立连接。** 2. **服务器收到客户端的SYN包后,向客户端发送ACK包,其中ACK标志位和SYN标志位都被设置为1,表示服务器收到了客户端的请求,并且同意建立连接。** 3. **客户端收到服务器的ACK包后,向服务器发送ACK包,其中ACK标志位被设置为1,表示客户端也同意建立连接。** ##### 为什么要三次握手? **三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** 1. **第一次握手** :Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常 2. **第二次握手** :Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常 3. **第三次握手** :Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 **三次握手就能确认双方收发功能都正常,缺一不可。** ##### 第2次握手传回了ACK,为什么还要传回SYN? **服务端传回发送端所发送的 ACK 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。** > **SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。** #### 四次挥手 **百度:** 1. **第一次挥手** :客户端发送一个 FIN(SEQ=X) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 **FIN-WAIT-1** 状态。 2. **第二次挥手** :服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (SEQ=X+1)标志的数据包->客户端 。然后,此时服务端进入**CLOSE-WAIT**状态,客户端进入**FIN-WAIT-2**状态。 3. **第三次挥手** :服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包->客户端请求关闭连接,然后,服务端进入**LAST-ACK**状态。 4. **第四次挥手** :客户端发送 ACK (SEQ=y+1)标志的数据包->服务端并且进入**TIME-WAIT**状态,服务端在收到 ACK (SEQ=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 **2MSL** 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。 **GPT3:** 1. **客户端向服务器发送FIN包,其中FIN标志位被设置为1,表示客户端请求关闭连接。** 2. **服务器收到客户端的FIN包后,向客户端发送ACK包,其中ACK标志位被设置为1,表示服务器已经收到了客户端的请求,但是还没有准备好关闭连接。** 3. **当服务器准备好关闭连接时,向客户端发送FIN包,其中FIN标志位被设置为1,表示服务器请求关闭连接。** 4. **客户端收到服务器的FIN包后,向服务器发送ACK包,其中ACK标志位被设置为1,表示客户端已经收到了服务器的请求,同时也准备好关闭连接。** ##### 为什么要四次挥手? **TCP是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。** **举个例子:A 和 B 打电话,通话即将结束后。** 1. **第一次挥手** : A 说“我没啥要说的了” 2. **第二次挥手** :B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话 3. **第三次挥手** :于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了” 4. **第四次挥手** :A 回答“知道了”,这样通话才算结束。 ##### 为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手? **因为服务器收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务器到客户端的数据传送。** ##### 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样? **客户端没有收到 ACK 确认,会重新发送 FIN 请求。** ##### 为什么第四次挥手客户端需要等待 2*MSL(报文段最长寿命)时间后才进入 CLOSED 状态? **第四次挥手时,客户端发送给服务器的 ACK 有可能丢失,如果服务端因为某些原因而没有收到 ACK 的话,服务端就会重发 FIN,如果客户端在 2*MSL 的时间内收到了 FIN,就会重新发送 ACK 并再次等待 2MSL,防止 Server 没有收到 ACK 而不断重发 FIN。** > **MSL(Maximum Segment Lifetime)** : 一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。 ### [TCP 如何保证传输的可靠性](https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html) 1. **基于数据块传输** :应用数据被分割成 TCP 认为最适合发送的数据块,再传输给网络层,数据块被称为报文段或段。 2. **对失序数据包重新排序以及去重**:TCP 为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。 3. **校验和** : TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 4. **超时重传** : 当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为[已丢失](https://zh.wikipedia.org/wiki/%E4%B8%A2%E5%8C%85) 5. **流量控制** : TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议(TCP 利用滑动窗口实现流量控制)。 6. **拥塞控制** : 当网络拥塞时,减少数据的发送。 ### [HTTP 和 HTTPS 有什么区别](https://javaguide.cn/cs-basics/network/http&https.html) **端口号** :HTTP 默认是 80,HTTPS 默认是 443。 **URL 前缀** :HTTP 的 URL 前缀是 `http://`,HTTPS 的 URL 前缀是 `https://`。 **安全性和资源消耗** : HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。 ### HTTP常用状态码(Status Codes) | **2xx:成功** | **3xx:重定向** | **4xx:客户端错误** | **5xx:服务器错误** | | --------------- | -------------------- | ---------------------- | --------------------- | | **200 成功** | **301 永久重定向** | **400 错误请求** | **500 服务器错误** | | **201 创建** | **304 资源未修改** | **401 未授权** | **502 网关错误** | | | | **403 禁止访问** | **504 网关超时** | | | | **404 未找到** | | | | | **405 请求方法不对** | | ### 常用的HTTP请求头 1. **Accept** **指定客户端接受的数据类型,可以使用逗号分隔多种类型。例如:Accept: text/html, application/xhtml+xml, application/xml;q=0.9, ***/*;q=0.8。 2. **User-Agent** **指定客户端的浏览器和操作系统信息,用于服务器识别客户端。例如:User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36。** 3. **Referer** **指定客户端当前请求的来源URL,用于服务器识别请求的来源。例如:Referer: **[https://www.google.com](https://www.google.com)。 4. **Accept-Encoding** **指定客户端可以接受的压缩格式,例如gzip、deflate等。例如:Accept-Encoding: gzip, deflate。** 5. **Connection** **指定客户端与服务器之间连接的类型,例如keep-alive、close等。例如:Connection: keep-alive。** 6. **Cookie** **指定客户端的Cookie信息,用于服务器识别客户端的身份和状态。例如:Cookie: SESSIONID=1234567890。** 7. **Authorization** **用于客户端向服务器发送身份认证信息,例如Basic、Bearer等。例如:Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l。** 8. **Content-Type** **指定请求体的数据类型,例如application/json、application/x-www-form-urlencoded等。例如:Content-Type: application/json。** 9. **Content-Length** **指定请求体的长度,单位为字节。例如:Content-Length: 1234。** ### HTTP长连接和短连接 **HTTP短连接:浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。HTTP1.0默认使用的是短连接。** **HTTP长连接:指的是复用TCP连接。多个HTTP请求可以复用同一个TCP连接,这就节省了TCP连接建立和断开的消耗。** **HTTP/1.1起,默认使用长连接。要使用长连接,客户端和服务器的HTTP首部的Connection都要设置为keep-alive,才能支持长连接。** ### HTTP缓存 **HTTP缓存是指在客户端(例如浏览器)和服务器之间缓存HTTP请求和响应,以提高性能和减少网络带宽的消耗。客户端可以通过缓存来避免发送重复的请求,而服务器可以利用缓存来减少处理请求的负担和减少响应时间。** **HTTP缓存主要分为两种:强缓存和协商缓存。** 1. **强缓存** **强缓存指的是客户端直接从缓存中读取资源,不需要向服务器发送请求。强缓存可以通过设置响应头实现,常用的响应头有:** * **Expires:指定过期时间,是一个GMT格式的时间字符串,例如Expires: Fri, 31 Dec 2021 23:59:59 GMT。** * **Cache-Control:指定缓存的相关属性,例如max-age、no-cache、no-store等。例如Cache-Control: max-age=3600。** 2. **协商缓存** **协商缓存指的是客户端向服务器发送请求,服务器根据请求头信息和资源的缓存策略决定是否返回缓存的资源,而不是直接返回资源。协商缓存可以通过设置请求头和响应头实现,常用的请求头有:** * **If-Modified-Since:指定上次请求返回的资源的最后修改时间,服务器根据修改时间判断是否返回缓存的资源。例如If-Modified-Since: Fri, 01 Jan 2021 00:00:00 GMT。** * **If-None-Match:指定上次请求返回的资源的Etag值,服务器根据Etag值判断是否返回缓存的资源。例如If-None-Match: **“**etag-value**”**。** **常用的响应头有:** * **Last-Modified:指定资源的最后修改时间,客户端可以通过该值判断资源是否需要更新。例如Last-Modified: Fri, 01 Jan 2021 00:00:00 GMT。** * **Etag:指定资源的唯一标识符,客户端可以通过该值判断资源是否需要更新。例如Etag: **“**etag-value**”**。** * **Cache-Control:同强缓存。** **综上所述,HTTP缓存是一种重要的性能优化技术,可以提高应用的性能和用户体验。需要根据具体的应用场景和需求选择合适的缓存策略,并设置对应的请求头和响应头信息。** ### HTTP协议 1. **HTTP/0.9** **HTTP/0.9是最早的HTTP协议版本,它只支持GET方法,并且没有请求头和响应头。该版本主要用于传输HTML文件。** 2. **HTTP/1.0** **HTTP/1.0是第一个正式的HTTP协议版本,支持多种请求方法和状态码,并且引入了请求头和响应头,可以传输不同类型的文件。HTTP/1.0是一种非持久化协议,每次请求和响应都需要重新建立连接。** 3. **HTTP/1.1** **HTTP/1.1是当前最广泛使用的HTTP协议版本,支持持久化连接、分块传输编码、管道化请求等新特性。HTTP/1.1引入了Host头字段,可以支持在一台服务器上托管多个网站。** 4. **HTTP/2** **HTTP/2是HTTP协议的下一代版本,它在HTTP/1.1的基础上进一步优化了性能,支持多路复用、二进制协议、头部压缩等新特性。HTTP/2可以显著提高网页的加载速度和性能。** 5. **HTTP/3** **HTTP/3是HTTP协议的下一代版本,是在HTTP/2基础上进行优化的新协议。使用基于UDP协议的QUIC(Quick UDP Internet Connections)协议作为传输层协议,这意味着HTTP/3在网络拥塞和高丢包率的情况下有更好的表现,可以提供更快的连接速度和更好的性能。头部压缩、多路复用、可靠性和安全性** ### [HTTP 1.0 和 HTTP 1.1 有什么区别](https://javaguide.cn/cs-basics/network/http1.0&http1.1.html) **连接方式** : HTTP 1.0 为短连接,HTTP 1.1 支持长连接。 **状态响应码** : HTTP/1.1中新加入了大量的状态码,光是错误响应状态码就新增了24种。比如说,`100 (Continue)`——在请求大资源前的预热请求,`206 (Partial Content)`——范围请求的标识码,`409 (Conflict)`——请求与当前资源的规定冲突,`410 (Gone)`——资源已被永久转移,而且没有任何已知的转发地址。 **缓存处理** : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。 **带宽优化及网络连接的使用** :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 **Host头处理** : HTTP/1.1在请求头中加入了`Host`字段。 ### HTTP 1.1 和 HTTP 2.0 有什么区别 **HTTP/2.0是HTTP协议的下一代版本,相较于HTTP/1.1,在以下几个方面有较大的改进:** 1. **多路复用** **HTTP/1.1使用管道化请求(pipelining)技术可以同时发送多个请求,但由于客户端和服务器需要按顺序处理请求,容易产生“队头阻塞”(head-of-line blocking)的问题。HTTP/2.0采用了多路复用(multiplexing)技术,可以在一个连接上同时传输多个请求和响应,有效减少了网络延迟和提高了性能。** 2. **二进制协议** **HTTP/1.1使用文本协议传输数据,请求和响应数据以纯文本形式传输,占用大量带宽和资源。HTTP/2.0采用了二进制协议(binary protocol),可以更快速、更有效地传输数据。** 3. **头部压缩** **HTTP/2.0采用了HPACK头部压缩算法,对请求和响应头部进行压缩,可以减少网络传输的数据量,降低了带宽的消耗,提高了性能。** 4. **服务器推送** **HTTP/2.0支持服务器推送(server push)功能,可以在服务器未经请求就把响应数据推送给客户端,提高了性能和用户体验。** 5. **优先级** **HTTP/2.0可以设置请求的优先级,可以让客户端和服务器更好地控制请求的处理顺序,提高了性能和用户体验。** **综上所述,HTTP/2.0相较于HTTP/1.1在性能、效率、安全性等方面都有较大的改进和提升,可以更好地适应现代互联网应用的需求。由于HTTP/2.0采用了新的技术和协议,需要浏览器和服务器的支持才能发挥其优势。** ### HTTP 2.0 和 HTTP 3.0 有什么区别 **HTTP/3是HTTP协议的下一代版本,是在HTTP/2基础上进行优化的新协议。相较于HTTP/2,HTTP/3在以下几个方面有所更新:** 1. **传输层协议** **HTTP/2使用的是TLS层上的二进制协议(Binary protocol),而HTTP/3则使用基于UDP协议的QUIC(Quick UDP Internet Connections)协议作为传输层协议,这意味着HTTP/3在网络拥塞和高丢包率的情况下有更好的表现,可以提供更快的连接速度和更好的性能。** 1. **头部压缩** **HTTP/2采用了HPACK压缩算法对头部进行压缩,以减少网络传输的数据量,但在实际使用中仍然存在一些问题,例如对延迟的影响。HTTP/3采用了一种新的头部压缩算法QPACK,可以在保证压缩效果的同时减少延迟和性能损失。** 1. **多路复用** **HTTP/2和HTTP/3都支持多路复用,可以在单个连接上同时传输多个请求和响应,提高了网络的利用率和效率。不同的是,HTTP/3使用QUIC协议作为传输层协议,可以更好地支持多路复用,减少了网络拥塞和延迟。** 1. **可靠性和安全性** **HTTP/3使用基于UDP协议的QUIC协议作为传输层协议,可以提供更好的可靠性和安全性,例如零RTT握手、快速连接恢复等特性。此外,HTTP/3还支持TLS1.3,可以提供更强的安全性保障。** **总之,HTTP/3相较于HTTP/2在传输层协议、头部压缩、多路复用、可靠性和安全性等方面都有所更新和改进,可以提供更好的性能和用户体验。但由于HTTP/3是相对比较新的协议,目前在浏览器和服务器的支持上还比较有限。** ### HTTP 是不保存状态的协议, 如何保存用户状态? **HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。** **在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。** **Cookie 被禁用怎么办?** **最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。** ### URI 和 URL 的区别是什么? * **URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。** * **URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。** **URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。** ### [ARP](https://javaguide.cn/cs-basics/network/arp.html) **ARP 协议,全称 ****地址解析协议(Address Resolution Protocol)**,它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 **ARP解决了同一个局域网上的主机和路由器IP和MAC地址的解析。** * **每台主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址的对应关系。** * **当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP列表中是否存在该 IP地址对应的MAC地址,如果有,就直接将数据包发送到这个MAC地址;如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的MAC地址。此ARP请求数据包里包括源主机的IP地址、硬件地址、以及目的主机的IP地址。** * **网络中所有的主机收到这个ARP请求后,会检查数据包中的目的IP是否和自己的IP地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,如果ARP表中已经存在该IP的信息,则将其覆盖,然后给源主机发送一个 ARP响应数据包,告诉对方自己是它需要查找的MAC地址。** * **源主机收到这个ARP响应数据包后,将得到的目的主机的IP地址和MAC地址添加到自己的ARP列表中,并利用此信息开始数据的传输。** * **如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。** ### ICMP协议 **ICMP,Internet Control Message Protocol ,Internet控制消息协议。** * **ICMP协议是一种面向无连接的协议,用于传输出错报告控制信息。** * **它是一个非常重要的协议,它对于网络安全具有极其重要的意义。它属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。** * **当遇到IP数据无法访问目标、IP路由器无法按当前的传输速率转发数据包等情况时,会自动发送ICMP消息。** **比如我们日常使用得比较多的ping,就是基于ICMP的。** ### 什么是 Mac 地址? **MAC 地址的全称是 ****媒体访问控制地址(Media Access Control Address)**。如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。 **可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。** > **还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。** **MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。** **MAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。** **最后,记住,****MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。** ## [操作系统基础](https://javaguide.cn/cs-basics/operating-system/operating-system-basic-questions-01.html) ### CPU调度单位 **CPU调度是指操作系统中对CPU进行分配和调度的过程,用于管理系统中各个进程或线程对CPU的竞争。CPU调度的单位主要包括以下几种:** 1. **进程:操作系统中资源分配的基本单位,由一组相关的线程组成,包括代码、数据、堆栈、打开的文件等资源。** 2. **线程:进程中的执行单元,共享进程的资源,包括CPU状态、内存等。** 3. **作业:一组相互关联的任务,通常由多个进程组成,需要占用多个资源,包括CPU、内存、I/O等。** 4. **任务:一组需要执行的动作,可能包含多个进程、线程或作业,可以并行执行。** **不同的操作系统和调度算法对CPU调度的单位有不同的处理方式,需要根据实际需求进行选择和使用。** ### CPU调度算法 **操作系统中常见的CPU调度算法有以下几种:** 1. **先来先服务调度(FCFS,First-Come, First-Served):按照进程到达的先后顺序进行调度,优先级相同的进程采用先到先服务的方式。** 2. **短作业优先调度(SJF,Shortest Job First):按照进程执行的时间长度进行调度,优先调度执行时间短的进程。** 3. **优先级调度(Priority Scheduling):按照进程的优先级进行调度,优先级高的进程优先被调度。** 4. **时间片轮转调度(RR,Round Robin):将所有就绪进程按照到达的先后顺序放入就绪队列,每个进程分配一个时间片进行执行,如果在时间片结束之前未执行完,则放回就绪队列,等待下一次调度。** 5. **最高响应比优先调度(HRRN,Highest Response Ratio Next):按照进程的等待时间和执行时间的比值来确定优先级,优先调度响应比最高的进程。** 6. **多级反馈队列调度(MFQS,Multi-Level Feedback Queue Scheduling):将进程按照优先级分成多个队列,每个队列的时间片大小不同,优先级高的队列时间片小,优先调度高优先级队列中的进程,如果一个进程在一个队列中等待了一定时间仍未执行完,则将其移到下一个队列中,直到完成执行。** **不同的调度算法有不同的优缺点,需要根据实际应用场景进行选择和使用。** ### 重入锁&公平锁&非公平锁 **可重入锁是一种支持同一个线程多次获取锁的锁,它可以有效地避免死锁问题。可重入锁的实现可以通过内部计数器来实现,每当一个线程获取锁时,计数器加1,每当一个线程释放锁时,计数器减1,当计数器为0时表示锁已经完全释放。常见的可重入锁实现包括ReentrantLock和synchronized关键字。** **公平锁和非公平锁是针对锁的获取顺序而言的。公平锁会按照请求的顺序,按照先来先得的原则,让等待时间最长的线程先获得锁,从而保证所有线程的公平性。而非公平锁则不考虑请求的顺序,直接让某个线程获得锁,这样可能会导致某些线程一直无法获得锁,从而影响程序的性能。** **可重入锁可以实现公平锁和非公平锁,实现方式如下:** 1. **公平锁:在可重入锁实现中,公平锁的实现通常需要一个FIFO队列,当线程请求锁时,先将其加入到队列尾部,当锁释放时,再从队列头部取出等待时间最长的线程进行锁的分配。** 2. **非公平锁:在可重入锁实现中,非公平锁的实现相对简单,可以直接让某个线程获得锁,而无需考虑等待队列的顺序。** **总的来说,可重入锁是一种非常重要的锁机制,它可以避免死锁问题,提高程序的性能。公平锁和非公平锁则是可重入锁的一种扩展实现,开发人员需要根据实际需求来选择合适的锁类型,从而达到最优的程序性能。** ### 同步锁 **同步锁(Synchronization Lock),也称为互斥锁(Mutex Lock),是一种线程同步机制,用于防止多个线程同时访问共享资源。同步锁可以保证在同一时刻只有一个线程可以访问共享资源,从而避免了线程之间的竞争和冲突。** **同步锁的基本原理是通过获取和释放锁来控制线程的访问。当一个线程需要访问共享资源时,它首先需要获取同步锁,如果同步锁已经被其他线程持有,则该线程需要等待直到同步锁被释放。当该线程访问完共享资源后,需要释放同步锁,从而允许其他线程获取同步锁并访问共享资源。** **在Java中,同步锁可以使用synchronized关键字来实现。例如,可以使用synchronized关键字来保护一个方法或一个代码块,从而实现线程的同步访问。在使用synchronized关键字时,需要注意以下几点:** 1. **同步锁只能在同一进程内的不同线程之间使用,不能用于不同进程之间的线程通信。** 2. **同步锁只能保护一个代码块或一个方法,不能保护多个方法之间的访问。** 3. **同步锁可以降低程序的性能,因为它会导致线程的阻塞和唤醒,从而增加线程上下文切换的开销。** **总之,同步锁是一种线程同步机制,用于防止多个线程同时访问共享资源。同步锁的基本原理是通过获取和释放锁来控制线程的访问。在Java中,同步锁可以使用synchronized关键字来实现。** ### 进程和线程的区别 **进程和线程都是操作系统中的概念,但它们在功能和实现方式上有所不同。** **进程是操作系统中资源分配的基本单位。每个进程都有自己的独立内存空间、代码和数据段,并且可以被操作系统调度和管理。每个进程都有一个唯一的进程标识符(PID),可以通过它来唯一标识一个进程。** **线程是进程中的一个执行单元,每个线程都共享相同的代码和数据段,但它们有自己的栈空间和程序计数器。线程可以被看作是进程中的一个轻量级的进程,因为它们共享进程的资源,如内存空间、文件句柄等,所以创建和销毁线程的开销比创建和销毁进程的开销要小。** **下面是进程和线程之间的一些关键区别:** 1. **资源管理:每个进程都有自己的独立内存空间和系统资源,而线程共享进程的资源,如内存空间、文件句柄等。** 2. **调度:操作系统在调度进程时,会考虑到进程的优先级和其他因素,而在调度线程时,会考虑到线程的优先级和进程的优先级。** 3. **并发性:由于每个线程都共享进程的资源,因此线程可以实现更高效的并发性,而进程之间的通信开销较大,因此在并发环境中效率相对较低。** 4. **稳定性:如果一个进程崩溃,可能会影响到其他进程,但是如果一个线程崩溃,只会影响到自己所在的进程。** 5. **创建和销毁:创建和销毁线程的开销比创建和销毁进程的开销要小。** **总的来说,线程在处理并发和共享资源方面比进程更有效率,但是进程在稳定性和资源隔离方面比线程更可靠。因此,在设计应用程序时,需要根据具体需求选择合适的进程或线程模型。** ## 数据结构 ### 数组 vs 链表 * **数组支持随机访问,而链表不支持。** * **数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。** * **数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去,这个操作是比较耗时的!** ### 数组 #### [时间复杂度](https://blog.csdn.net/eg1107/article/details/128154432) **数组(Array)** 是一种很常见的数据结构。它由相同类型的元素(element)组成,并且是使用一块连续的内存来存储。 **我们直接可以利用元素的索引(index)可以计算出该元素对应的存储地址。** **数组的特点是:****提供随机访问** 并且容量有限。 ### 链表 #### 简介 **链表(LinkedList)** 虽然是一种线性表,但是并不会按线性的顺序存储数据,使用的不是连续的内存空间来存储数据。 **链表的插入和删除操作的复杂度为 O(1) ,只需要知道目标位置元素的上一个元素即可。但是,在查找一个节点或者访问特定位置的节点的时候复杂度为 O(n) 。** **使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但链表不会节省空间,相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。除此之外,链表不具有数组随机读取的优点。** #### [分类](https://javaguide.cn/cs-basics/data-structure/linear-data-structure.html#_2-2-%E9%93%BE%E8%A1%A8%E5%88%86%E7%B1%BB) **常见链表分类:** 1. **单链表** 2. **双向链表** 3. **循环链表** 4. **双向循环链表** #### 应用场景 * **如果需要支持随机访问的话,链表没办法做到。** * **如果需要存储的数据元素的个数不确定,并且需要经常添加和删除数据的话,使用链表比较合适。** * **如果需要存储的数据元素的个数确定,并且不需要经常添加和删除数据的话,使用数组比较合适。** ### 栈 #### 简介 **栈** (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。** **栈常用一维数组或链表来实现,用数组实现的栈叫作 ****顺序栈** ,用链表实现的栈叫作 **链式栈** 。 #### [应用场景](https://javaguide.cn/cs-basics/data-structure/linear-data-structure.html#_3-2-%E6%A0%88%E7%9A%84%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF) **当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 ****后进先出(LIFO, Last In First Out)** 的特性时,我们就可以使用栈这个数据结构。 ### 队列 #### 简介 **队列** 是 **先进先出( FIFO,First In, First Out)** 的线性表。在具体应用中通常用链表或者数组来实现,用数组实现的队列叫作 **顺序队列** ,用链表实现的队列叫作 **链式队列** 。**队列只允许在后端(rear)进行插入操作也就是 入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue** **队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。** #### [分类](https://javaguide.cn/cs-basics/data-structure/linear-data-structure.html#_4-2-%E9%98%9F%E5%88%97%E5%88%86%E7%B1%BB) **单队列&&循环队列** #### 应用场景 **当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。** * **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。 * **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。 * **Linux 内核进程队列(按优先级排队)** * **现实生活中的派对,播放器上的播放列表;** * **消息队列** ### 图 #### 图的基本概念 ##### 顶点 **图中的数据元素,我们称之为顶点,图至少有一个顶点(非空有穷集合)** **对应到好友关系图,每一个用户就代表一个顶点。** ##### 边 **顶点之间的关系用边表示。** **对应到好友关系图,两个用户是好友的话,那两者之间就存在一条边。** ##### 度 **度表示一个顶点包含多少条边,在有向图中,还分为出度和入度,出度表示从该顶点出去的边的条数,入度表示进入该顶点的边的条数。** **对应到好友关系图,度就代表了某个人的好友数量。** ##### 无向图和有向图 **边表示的是顶点之间的关系,有的关系是双向的,比如同学关系,A是B的同学,那么B也肯定是A的同学,那么在表示A和B的关系时,就不用关注方向,用不带箭头的边表示,这样的图就是无向图。** **有的关系是有方向的,比如父子关系,师生关系,微博的关注关系,A是B的爸爸,但B肯定不是A的爸爸,A关注B,B不一定关注A。在这种情况下,我们就用带箭头的边表示二者的关系,这样的图就是有向图。** ##### 无权图和带权图 **对于一个关系,如果我们只关心关系的有无,而不关心关系有多强,那么就可以用无权图表示二者的关系。** **对于一个关系,如果我们既关心关系的有无,也关心关系的强度,比如描述地图上两个城市的关系,需要用到距离,那么就用带权图来表示,带权图中的每一条边一个数值表示权值,代表关系的强度。** #### [图的存储](https://javaguide.cn/cs-basics/data-structure/graph.html#%E5%9B%BE%E7%9A%84%E5%AD%98%E5%82%A8) **邻接矩阵存储、 邻接表存储** #### [图的搜索](https://javaguide.cn/cs-basics/data-structure/graph.html#%E5%9B%BE%E7%9A%84%E6%90%9C%E7%B4%A2) **广度优先搜索、 深度优先搜索** ### [堆](https://javaguide.cn/cs-basics/data-structure/heap.html) ### 树 **树是一种非线性的数据结构,它由节点和边组成,每个节点包含一个值和若干指向其他节点的指针。树的一个重要特点是它是有层次结构的,每个节点都有一个父节点和若干子节点,节点之间的关系是一对多的关系。** **树中的常用概念:** * **节点** :树中的每个元素都可以统称为节点。 * **根节点** :顶层节点或者说没有父节点的节点。上图中 A 节点就是根节点。 * **父节点** :若一个节点含有子节点,则这个节点称为其子节点的父节点。上图中的 B 节点是 D 节点、E 节点的父节点。 * **子节点** :一个节点含有的子树的根节点称为该节点的子节点。上图中 D 节点、E 节点是 B 节点的子节点。 * **兄弟节点** :具有相同父节点的节点互称为兄弟节点。上图中 D 节点、E 节点的共同父节点是 B 节点,故 D 和 E 为兄弟节点。 * **叶子节点** :没有子节点的节点。上图中的 D、F、H、I 都是叶子节点。 * **节点的高度** :该节点到叶子节点的最长路径所包含的边数。 * **节点的深度** :根节点到该节点的路径所包含的边数 * **节点的层数** :节点的深度+1。 * **树的高度** :根节点的高度。 #### [二叉树的分类](https://javaguide.cn/cs-basics/data-structure/tree.html#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%88%86%E7%B1%BB) **满二叉树、 完全二叉树、 平衡二叉树** ##### 二叉树 **是一种每个节点最多只有两个子节点的树结构,它可以是空树,也可以只有一个根节点。二叉树的遍历方式包括前序遍历、中序遍历和后序遍历。其中前序遍历是先访问根节点,再访问左子树和右子树;中序遍历是先访问左子树,再访问根节点和右子树;后序遍历是先访问左子树和右子树,再访问根节点。** ##### 二叉搜索树 **是一种特殊的二叉树,它的每个节点的左子树中所有节点的值都小于该节点的值,右子树中所有节点的值都大于该节点的值。这样的性质使得二叉搜索树可以进行高效的查找、插入和删除操作。** ##### 平衡树 **是一种保持平衡的二叉搜索树,它通过旋转节点、左旋或右旋等操作来使树保持平衡。常见的平衡树包括红黑树、AVL树等。** ##### B树 **是一种多路搜索树,它的每个节点可以包含多个关键字和指针,从而可以存储更多的数据。B树被广泛用于文件系统、数据库等场景中。** #### [二叉树的存储](https://javaguide.cn/cs-basics/data-structure/tree.html#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%AD%98%E5%82%A8) **链式存储、 顺序存储** #### [二叉树的遍历](https://javaguide.cn/cs-basics/data-structure/tree.html#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%81%8D%E5%8E%86) **先序遍历、 中序遍历、 后序遍历** ### 红黑树 **红黑树特点** : 1. **每个节点非红即黑;** 2. **根节点总是黑色的;** 3. **每个叶子节点都是黑色的空节点(NIL节点);** 4. **如果节点是红色的,则它的子节点必须是黑色的(反之不一定);** 5. **从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。** **红黑树的应用** :TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。 ### [布隆过滤器](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html) ## [Linux基础](https://javaguide.cn/cs-basics/operating-system/linux-intro.html) ## [Shell基础](https://javaguide.cn/cs-basics/operating-system/shell-intro.html) ## 算法 ### 查找算法 1. **线性查找(Linear Search):也称为顺序查找,从数据结构的起始位置开始逐个比较查找目标,直到找到目标或者遍历完整个数据结构。** 2. **二分查找(Binary Search):只适用于有序的数据结构,每次将查找范围缩小一半,直到找到目标或者查找范围为空。** 3. **插值查找(Interpolation Search):只适用于有序的数据结构,通过目标值在数据结构中的位置来预测下一次查找的位置,从而加快查找速度。** 4. **斐波那契查找(Fibonacci Search):只适用于有序的数据结构,通过斐波那契数列来确定查找范围的长度和位置,从而加快查找速度。** 5. **树表查找(Tree Search):通过建立二叉查找树、平衡树、B树等数据结构来进行查找。** 6. **哈希表查找(Hash Table Search):通过哈希函数将关键字映射到哈希表中的某个位置,从而快速查找目标。** **不同的查找算法在不同的数据结构和数据规模下有着不同的性能表现,需要根据实际需求进行选择和使用。** ### 遍历一个树形结构 #### 前序遍历 **前序遍历先访问根节点,然后递归遍历左子树和右子树。** **递归实现:** ``` public void preOrderTraversal(TreeNode root) { if (root == null) { return; } System.out.println(root.val); // 访问当前节点 preOrderTraversal(root.left); // 遍历左子树 preOrderTraversal(root.right); // 遍历右子树 } ``` **迭代实现:** ``` public List<Integer> preOrderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Stack<TreeNode> stack = new Stack<>(); if (root == null) { return res; } stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); res.add(node.val); // 访问当前节点 if (node.right != null) { stack.push(node.right); } if (node.left != null) { stack.push(node.left); } } return res; } ``` #### 中序遍历 **中序遍历先遍历左子树,然后访问根节点,最后遍历右子树。** **递归实现:** ``` public void inOrderTraversal(TreeNode root) { if (root == null) { return; } inOrderTraversal(root.left); // 遍历左子树 System.out.println(root.val); // 访问当前节点 inOrderTraversal(root.right); // 遍历右子树 } ``` **迭代实现:** ``` public List<Integer> inOrderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Stack<TreeNode> stack = new Stack<>(); TreeNode cur = root; while (cur != null || !stack.isEmpty()) { while (cur != null) { stack.push(cur); cur = cur.left; } TreeNode node = stack.pop(); res.add(node.val); // 访问当前节点 cur = node.right; } return res; } ``` #### 后序遍历 **后序遍历先遍历左子树,然后遍历右子树,最后访问根节点。** **迭代实现:** ``` public List<Integer> postOrderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Stack<TreeNode> stack = new Stack<>(); TreeNode cur = root; TreeNode pre = null; while (cur != null || !stack.isEmpty()) { while (cur != null) { stack.push(cur); cur = cur.left; } TreeNode node = stack.peek(); if (node.right == null || node.right == pre) { stack.pop(); res.add(node.val); // 访问当前节点 pre = node; } else { cur = node.right; } } return res; } ``` **递归实现:** ``` public void postOrderTraversal(TreeNode root) { if (root == null) { return; } postOrderTraversal(root.left); // 遍历左子树 postOrderTraversal(root.right); // 遍历右子树 System.out.println(root.val); // 访问当前节点 } ``` ### 排序算法 **常见的排序算法包括以下几种:** 1. **冒泡排序(Bubble Sort):比较相邻两个元素,如果顺序不对则交换,每次将最大的元素冒泡到最后。** 2. **插入排序(Insertion Sort):将数组分为已排序和未排序两部分,每次将未排序的元素插入到已排序的合适位置。** 3. **选择排序(Selection Sort):每次从未排序的元素中选择最小的元素,放到已排序的末尾。** 4. **快速排序(Quick Sort):选定一个基准元素,将数组分为小于基准元素和大于基准元素的两部分,递归地对两部分进行排序。** 5. **归并排序(Merge Sort):将数组分为两半,递归地对两半进行排序,然后将两个有序数组归并为一个有序数组。** 6. **堆排序(Heap Sort):将数组构建成一个最大堆,然后每次将最大的元素取出,放到已排序的末尾,再重新调整堆。** 7. **希尔排序(Shell Sort):将数组分成若干个子序列,对每个子序列进行插入排序,然后缩小间隔,再进行排序,直到间隔为1。** 8. **计数排序(Counting Sort):统计每个元素出现的次数,然后根据元素的值进行排序。** 9. **桶排序(Bucket Sort):将元素划分为若干个桶,对每个桶进行排序,然后将所有桶的元素按顺序合并为一个有序序列。** 10. **基数排序(Radix Sort):按照元素的位数进行排序,先按个位排序,再按十位排序,以此类推。** **不同的排序算法有不同的时间复杂度和适用场景,需要根据实际情况进行选择和使用。** ### 快速排序 **快速排序(Quick Sort)是一种基于分治思想的排序算法。是一种高效的排序算法,基本思想是选择一个基准数,将待排序数组划分为两个子数组,分别进行递归排序。具体实现可以参考以下Java代码:** ``` public static void quickSort(int[] arr, int start, int end) { if (start < end) { int i = start, j = end, pivot = arr[start]; while (i < j) { while (i < j && arr[j] >= pivot) { j--; } if (i < j) { arr[i++] = arr[j]; } while (i < j && arr[i] < pivot) { i++; } if (i < j) { arr[j--] = arr[i]; } } arr[i] = pivot; quickSort(arr, start, i - 1); quickSort(arr, i + 1, end); } } ``` 1. **选择一个基准元素,将数组分为两个子数组,其中一个子数组中的所有元素都比基准元素小,另一个子数组中的所有元素都比基准元素大。** 2. **对子数组进行递归排序,直到子数组长度为1。** 3. **将排好序的子数组合并起来,得到最终的有序数组。** ### 动态规划 **是一种算法设计技术,用于解决多阶段决策问题。它的基本思想是将大问题分解为小问题,通过递推求解,最终得到整个问题的最优解。动态规划常常用于求解最优解、最短路径等问题,其主要应用场景包括动态规划、最短路问题、背包问题、编辑距离等。在具体实现时,需要定义状态转移方程,并使用备忘录技术避免重复计算。以下是动态规划的经典问题——背包问题的Java代码实现:** ``` public static int knapsack(int W, int[] wt, int[] val, int n) { int[][] dp = new int[n + 1][W + 1]; for (int i = 0; i <= n; i++) { for (int w = 0; w <= W; w++) { if (i == 0 || w == 0) { dp[i][w] = 0; } else if (wt[i - 1] <= w) { dp[i][w] = Math.max(val[i - 1] + dp[i - 1][w - wt[i - 1]], dp[i - 1][w]); } else { dp[i][w] = dp[i - 1][w]; } } } return dp[n][W]; } ``` ### 删除链表中倒数第n个节点 **删除链表中倒数第n个节点是一道经典的链表问题,可以使用双指针来解决。具体思路如下:** 1. **定义两个指针p和q,初始时都指向链表的头结点。** 2. **让指针q先向后移动n个位置。** 3. **然后同时让p和q向后移动,直到q指向链表的尾节点。** 4. **此时p指向的节点就是需要删除的节点,将其从链表中删除即可。** ``` public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode p = dummy, q = dummy; for (int i = 0; i < n; i++) { q = q.next; } while (q.next != null) { p = p.next; q = q.next; } p.next = p.next.next; return dummy.next; } ``` ### 字符串全排列 **字符串全排列是一道比较经典的算法问题,可以使用回溯法来解决。具体思路如下:** 1. **将字符串转换为字符数组,并定义一个空的字符串用于存放排列结果。** 2. **使用回溯法进行排列,对于每个字符,依次和后面的字符进行交换,并递归进行排列。** 3. **当排列完成后,将结果添加到结果集中,并进行回溯。** ``` public List<String> permutation(String s) { List<String> res = new ArrayList<>(); char[] arr = s.toCharArray(); backtrack(arr, 0, res); return res; } private void backtrack(char[] arr, int i, List<String> res) { if (i == arr.length - 1) { res.add(new String(arr)); return; } Set<Character> set = new HashSet<>(); for (int j = i; j < arr.length; j++) { if (!set.contains(arr[j])) { set.add(arr[j]); swap(arr, i, j); backtrack(arr, i + 1, res); swap(arr, i, j); } } } private void swap(char[] arr, int i, int j) { char temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ``` ### [常见的字符串算法总结](https://javaguide.cn/cs-basics/algorithms/string-algorithm-problems.html) ### [常见的链表算法总结](https://javaguide.cn/cs-basics/algorithms/linkedlist-algorithm-problems.html) ### [十大经典排序算法总结](https://javaguide.cn/cs-basics/algorithms/10-classical-sorting-algorithms.html) # 数据库【前端中概率】 ## 数据库基础知识总结 ### 什么是数据库事务调度? **数据库事务调度是指将多个事务的操作序列合理地调度和执行,以确保数据库的一致性、隔离性和持久性。** **事务是数据库中执行的一组操作,事务必须满足ACID属性:原子性、一致性、隔离性和持久性。事务调度的主要目标是确保事务的ACID属性,同时最大程度地提高数据库的并发性和性能。** **在数据库事务调度中,主要有两种调度方式:基于并发控制的调度和基于锁的调度。基于并发控制的调度是通过并发控制算法来实现事务之间的隔离和并发,比如多版本并发控制(MVCC)和时间戳并发控制(TCC)。基于锁的调度是通过对数据对象进行加锁和解锁来保证事务之间的隔离和并发,比如共享锁和排他锁。** **在事务调度中,还有一个重要的概念是事务的调度图。事务的调度图是一个有向图,其中每个节点表示一个事务,每条边表示两个事务之间的依赖关系。通过事务的调度图,可以确定事务的执行顺序,以确保事务之间的依赖关系得到正确的处理。** **在实际应用中,为了提高数据库的性能和并发性,通常会采用多种调度方式和算法的组合。因此,在设计数据库事务调度时,需要根据具体的应用场景和需求来选择合适的调度方式和算法,以达到最佳的性能和可靠性。** ### 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员? * **数据库** : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。 * **数据库管理系统** : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。 * **数据库系统** : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。 * **数据库管理员** : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。 ### 什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性? **元组** : 元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。 **码** :码就是能唯一标识实体的属性,对应表中的列。 **候选码** : 若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。 **主码** : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。 **外码** : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。 **主属性** : 候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。 **非主属性:** 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。 ### 什么是 ER 图? **ER 图** 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。 **ER 图由下面 3 个要素组成:** * **实体** :通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。 * **属性** :即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。 * **联系** :即实体与实体之间的关系,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。 ### 事务了解吗? **事务(Transaction)是数据库中的一个概念,指一组操作被视为一个不可分割的工作单元,并且要么全部成功,要么全部失败。事务有四个基本属性,即ACID(原子性、一致性、隔离性、持久性)。** 1. **原子性(Atomicity):指一个事务中的所有操作要么全部成功,要么全部失败,不会出现部分成功和部分失败的情况。** 2. **一致性(Consistency):指事务开始前和结束后,数据库的状态必须是一致的。即在事务执行过程中,如果数据发生了修改,那么修改后的数据必须满足预设的约束条件,否则事务会被回滚。** 3. **隔离性(Isolation):指每个事务的操作是互相隔离的,彼此之间互不干扰。每个事务必须认为它是唯一在执行的事务,即并发执行的多个事务之间不能相互干扰。** 4. **持久性(Durability):指当事务完成后,其所做的修改将永久保存在数据库中,并不会因为系统故障而丢失。** ### 数据库范式了解吗? **数据库范式有 3 种:** * **1NF(第一范式):属性不可再分。** * **2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。** * **3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。** ### 1NF(第一范式) **属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。****1NF 是所有关系型数据库的最基本要求** ,也就是说关系型数据库中创建的表一定满足第一范式。 ### 2NF(第二范式) **2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如下图所示,展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。** ### 3NF(第三范式) **3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,****基本**上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。 ### 主键和外键有什么区别? * **主键(主码)** :主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。 * **外键(外码)** :外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。 ### drop、delete 与 truncate 区别? ### 用法不同 * `drop`(丢弃数据): `drop table 表名` ,直接将表都删除掉,在删除表的时候使用。 * `truncate` (清空数据) : `truncate table 表名` ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。 * `delete`(删除数据) : `delete from 表名 where 列名=值`,删除某一行的数据,如果不加 `where` 子句和`truncate table 表名`作用类似。 `truncate` 和不带 where``子句的`delete`、以及`drop`都会删除表内的数据,但是 **`truncate`和`delete`只删除数据不删除表的结构(定义),执行`drop`语句,此表的结构也会删除,也就是执行`drop` 之后对应的表不复存在。** #### 数据库设计通常分为哪几步? 1. **需求分析** : 分析用户的需求,包括数据、功能和性能需求。 2. **概念结构设计** : 主要采用 E-R 模型进行设计,包括画 E-R 图。 3. **逻辑结构设计** : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。 4. **物理结构设计** : 主要是为所设计的数据库选择合适的存储结构和存取路径。 5. **数据库实施** : 包括编程、测试和试运行 6. **数据库的运行和维护** : 系统的运行与数据库的日常维护。 ## NoSQL基础知识总结 ### NoSQL 是什么? **NoSQL(Not Only SQL 的缩写)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。** **一个常见的误解是 NoSQL 数据库或非关系型数据库不能很好地存储关系型数据。NoSQL 数据库可以存储关系型数据—它们与关系型数据库的存储方式不同。** **NoSQL 数据库代表:HBase 、Cassandra、MongoDB、Redis。** ### NoSQL 数据库有什么优势? **NoSQL 数据库非常适合许多现代应用程序,例如移动、Web 和游戏等应用程序,它们需要灵活、可扩展、高性能和功能强大的数据库以提供卓越的用户体验。** * **灵活性:** NoSQL 数据库通常提供灵活的架构,以实现更快速、更多的迭代开发。灵活的数据模型使 NoSQL 数据库成为半结构化和非结构化数据的理想之选。 * **可扩展性:** NoSQL 数据库通常被设计为通过使用分布式硬件集群来横向扩展,而不是通过添加昂贵和强大的服务器来纵向扩展。 * **高性能:** NoSQL 数据库针对特定的数据模型和访问模式进行了优化,这与尝试使用关系数据库完成类似功能相比可实现更高的性能。 * **强大的功能:** NoSQL 数据库提供功能强大的 API 和数据类型,专门针对其各自的数据模型而构建。 ### NoSQL 数据库有哪些类型? **NoSQL 数据库主要可以分为下面四种类型:** * **键值** :键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制 value 字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。 * **文档** :文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。 * **图形** :图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。 * **宽列** :宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。 ### SQL 和 NoSQL 有什么区别? | **SQL 数据库** | **NoSQL 数据库** | | | ------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **数据存储模型** | **结构化存储,具有固定行和列的表格** | **非结构化存储。文档:JSON 文档,键值:键值对,宽列:包含行和动态列的表,图:节点和边** | | **发展历程** | **开发于 1970 年代,重点是减少数据重复** | **开发于 2000 年代后期,重点是提升可扩展性,减少大规模数据的存储成本** | | **例子** | **Oracle、MySQL、Microsoft SQL Server 、PostgreSQL** | **文档:MongoDB、CouchDB,键值:Redis 、DynamoDB,宽列:Cassandra 、 HBase,图表:Neo4j 、 Amazon Neptune、Giraph** | | **ACID 属性** | **提供原子性、一致性、隔离性和持久性 (ACID) 属性** | **通常不支持 ACID 事务,为了可扩展、高性能进行了权衡,少部分支持比如 MongoDB 。不过,MongoDB 对 ACID 事务 的支持和 MySQL 还是有所区别的。** | | **性能** | **性能通常取决于磁盘子系统。要获得最佳性能,通常需要优化查询、索引和表结构。** | **性能通常由底层硬件集群大小、网络延迟以及调用应用程序来决定。** | | **扩展** | **垂直(使用性能更强大的服务器进行扩展)、读写分离、分库分表** | **横向(增加服务器的方式横向扩展,通常是基于分片机制)** | | **用途** | **普通企业级的项目的数据存储** | **用途广泛比如图数据库支持分析和遍历连接数据之间的关系、键值数据库可以处理大量数据扩展和极高的状态变化** | | **查询语法** | **结构化查询语言 (SQL)** | **数据访问语法可能因数据库而异** | ## [字符集详解](https://javaguide.cn/database/character-set.html) ## [SQL基础语句](https://javaguide.cn/database/sql/sql-questions-01.html) ## [MySQL](https://javaguide.cn/database/mysql/mysql-questions-01.html) ## Redis ### [上](https://javaguide.cn/database/redis/redis-questions-01.html) ### [下](https://javaguide.cn/database/redis/redis-questions-02.html) ## MongoDB ### [上](https://javaguide.cn/database/mongodb/mongodb-questions-01.html) ### [下](https://javaguide.cn/database/mongodb/mongodb-questions-02.html) # 常用Java框架【前端极低概率】 ## MyBatis【前端极低概率】 [网站](https://javaguide.cn/system-design/framework/mybatis/mybatis-interview.html) ## Spring【前端极低概率】 [网站](https://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html) # 开发工具【前端中概率】 ## Maven ### Maven 介绍 [Maven](https://github.com/apache/maven)官方文档是这样介绍的 Maven 的: > **Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project**’**s build, reporting and documentation from a central piece of information.** > > **Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。** **什么是 POM?** 每一个 Maven 工程都有一个 `pom.xml` 文件,位于根目录中,包含项目构建生命周期的详细信息。通过 `pom.xml` 文件,我们可以定义项目的坐标、项目依赖、项目信息、插件信息等等配置。 **对于开发者来说,Maven 的主要作用主要有 3 个:** 1. **项目构建** :提供标准的、跨平台的自动化项目构建方式。 2. **依赖管理** :方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突问题。 3. **统一开发结构** :提供标准的、统一的项目结构。 ### Maven 坐标元素 **项目中依赖的第三方库以及插件可统称为构件。每一个构件都可以使用 Maven 坐标唯一标识,坐标元素包括:** * **goupId**(必须): 定义了当前 Maven 项目隶属的组织或公司。groupId 一般分为多段,通常情况下,第一段为域,第二段为公司名称。域又分为 org、com、cn 等,其中 org 为非营利组织,com 为商业组织,cn 表示中国。以 apache 开源社区的 tomcat 项目为例,这个项目的 groupId 是 org.apache,它的域是 org(因为 tomcat 是非营利项目),公司名称是 apache,artifactId 是 tomcat。 * **artifactId**(必须):定义了当前 Maven 项目的名称,项目的唯一的标识符,对应项目根目录的名称。 * **version**(必须): 定义了 Maven 项目当前所处版本。 * **packaging**(可选):定义了 Maven 项目的打包方式(比如 jar,war**…**),默认使用 jar。 * **classifier**(可选):常用于区分从同一 POM 构建的具有不同内容的构件,可以是任意的字符串,附加在版本号之后。 ### Maven 依赖 #### 依赖配置 **配置说明** : * **dependencies : 一个 pom.xml 文件中只能存在一个这样的标签,是用来管理依赖的总标签。** * **dependency:包含在 dependencies 标签中,可以有多个,每一个表示项目的一个依赖。** * **groupId,artifactId,version(必要):依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven 根据坐标才能找到需要的依赖。我们在上面解释过这些元素的具体意思,这里就不重复提了。** * **type(可选):依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值是 jar。** * **scope(可选):依赖的范围,默认值是 compile。** * **optional(可选): 标记依赖是否可选** * **exclusions(可选):用来排除传递性依赖,例如 jar 包冲突** #### 依赖范围 **classpath** 用于指定 `.class` 文件存放的位置,类加载器会从该路径中加载所需的 `.class` 文件到内存中。 **Maven 在编译、执行测试、实际运行有着三套不同的 classpath:** * **编译 classpath** :编译主代码有效 * **测试 classpath** :编译、运行测试代码有效 * **运行 classpath** :项目运行时有效 **Maven 的依赖范围如下:** * **compile**:编译依赖范围(默认),使用此依赖范围对于编译、测试、运行三种都有效,即在编译、测试和运行的时候都要使用该依赖 Jar 包。 * **test**:测试依赖范围,从字面意思就可以知道此依赖范围只能用于测试,而在编译和运行项目时无法使用此类依赖,典型的是 JUnit,它只用于编译测试代码和运行测试代码的时候才需要。 * **provided** :此依赖范围,对于编译和测试有效,而对运行时无效。比如 `servlet-api.jar` 在 Tomcat 中已经提供了,我们只需要的是编译期提供而已。 * **runtime**:运行时依赖范围,对于测试和运行有效,但是在编译主代码时无效,典型的就是 JDBC 驱动实现。 * **system**:系统依赖范围,使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径,不依赖 Maven 仓库解析,所以可能会造成建构的不可移植。 #### Maven 仓库 **Maven 仓库分为:** * **本地仓库** :运行 Maven 的计算机上的一个目录,它缓存远程下载的构件并包含尚未发布的临时构件。`settings.xml` 文件中可以看到 Maven 的本地仓库路径配置,默认本地仓库路径是在 `${user.home}/.m2/repository`。 * **远程仓库** :官方或者其他组织维护的 Maven 仓库。 **Maven 远程仓库可以分为:** * **中央仓库** :这个仓库是由 Maven 社区来维护的,里面存放了绝大多数开源软件的包,并且是作为 Maven 的默认配置,不需要开发者额外配置。另外为了方便查询,还提供了一个[查询地址](https://search.maven.org/) * **开发者可以通过这个地址更快的搜索需要构件的坐标。** * **私服** :私服是一种特殊的远程 Maven 仓库,它是架设在局域网内的仓库服务,私服一般被配置为互联网远程仓库的镜像,供局域网内的 Maven 用户使用。 * **其他的公共仓库** :有一些公共仓库是未来加速访问(比如阿里云 Maven 镜像仓库)或者部分构件不存在于中央仓库中。 **Maven 依赖包寻找顺序:** 1. **先去本地仓库找寻,有的话,直接使用。** 2. **本地仓库没有找到的话,会去远程仓库找寻,下载包到本地仓库。** 3. **远程仓库没有找到的话,会报错。** #### Maven 生命周期 **Maven 的生命周期就是为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。** **Maven 定义了 3 个生命周期**`META-INF/plexus/components.xml`: * `default` 生命周期 * `clean`生命周期 **clean 生命周期的目的是清理项目,共包含 3 个阶段:** 1. **pre-clean** 2. **clean** 3. **post-clean** * `site`生命周期 **site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段:** 1. **pre-site** 2. **site** 3. **post-site** 4. **site-deploy** **这些生命周期是相互独立的,每个生命周期包含多个阶段(phase)。并且,这些阶段是有序的,也就是说,后面的阶段依赖于前面的阶段。当执行某个阶段的时候,会先执行它前面的阶段。** ## Gradle ### Gradle 介绍 **Gradle 官方文档是这样介绍的 Gradle 的:** > **Gradle is an open-source **[build automation](https://en.wikipedia.org/wiki/Build_automation) > **tool flexible enough to build almost any type of software. Gradle makes few assumptions about what you’re trying to build or how to build it. This makes Gradle particularly flexible.** > > **Gradle 是一个开源的构建自动化工具,它足够灵活,可以构建几乎任何类型的软件。Gradle 对你要构建什么或者如何构建它做了很少的假设。这使得 Gradle 特别灵活。** **简单来说,Gradle 就是一个运行在 JVM 上的自动化的项目构建工具,用来帮助我们自动构建项目。** **对于开发者来说,Gradle 的主要作用主要有 3 个:** 1. **项目构建** :提供标准的、跨平台的自动化项目构建方式。 2. **依赖管理** :方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突问题。 3. **统一开发结构** :提供标准的、统一的项目结构。 **Gradle 构建脚本是使用 Groovy 或 Kotlin 语言编写的,表达能力非常强,也足够灵活。** ### Groovy 介绍 **Gradle 是运行在 JVM 上的一个程序,它可以使用 Groovy 来编写构建脚本。** **Groovy 是运行在 JVM 上的脚本语言,是基于 Java 扩展的动态语言,它的语法和 Java 非常的相似,可以使用 Java 的类库。Groovy 可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了 Java 、Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。** **我们可以用学习 Java 的方式去学习 Groovy ,学习成本相对来说还是比较低的,即使开发过程中忘记 Groovy 语法,也可以用 Java 语法继续编码。** **基于 JVM 的语言有很多种比如 Groovy,Kotlin,Java,Scala,他们最终都会编译生成 Java 字节码文件并在 JVM 上运行。** ### Gradle 优势 **Gradle 是新一代的构建系统,具有高效和灵活等诸多优势,广泛用于 Java 开发。不仅 Android 将其作为官方构建系统, 越来越多的 Java 项目比如 Spring Boot 也慢慢迁移到 Gradle。** * **在灵活性上,Gradle 支持基于 Groovy 语言编写脚本,侧重于构建过程的灵活性,适合于构建复杂度较高的项目,可以完成非常复杂的构建。** * **在粒度性上,Gradle 构建的粒度细化到了每一个 task 之中。并且它所有的 Task 源码都是开源的,在我们掌握了这一整套打包流程后,我们就可以通过去修改它的 Task 去动态改变其执行流程。** * **在扩展性上,Gradle 支持插件机制,所以我们可以复用这些插件,就如同复用库一样简单方便。** ### Gradle 插件 **Gradle 提供的是一套核心的构建机制,而 Gradle 插件则是运行在这套机制上的一些具体构建逻辑,其本质上和 **`.gradle` 文件是相同。你可以将 Gradle 插件看作是封装了一系列 Task 并执行的工具。 **Gradle 插件主要分为两类:** * **脚本插件: 脚本插件就是一个普通的脚本文件,它可以被导入都其他构建脚本中。** * **二进制插件 / 对象插件:在一个单独的插件模块中定义,其他模块通过 Plugin ID 应用插件。因为这种方式发布和复用更加友好,我们一般接触到的 Gradle 插件都是指二进制插件的形式。** **虽然 Gradle 插件与 .gradle 文件本质上没有区别,**`.gradle` 文件也能实现 Gradle 插件类似的功能。但是,Gradle 插件使用了独立模块封装构建逻辑,无论是从开发开始使用来看,Gradle 插件的整体体验都更友好。 * **逻辑复用:** 将相同的逻辑提供给多个相似项目复用,减少重复维护类似逻辑开销。当然 .gradle 文件也能做到逻辑复用,但 Gradle 插件的封装性更好; * **组件发布:** 可以将插件发布到 Maven 仓库进行管理,其他项目可以使用插件 ID 依赖。当然 .gradle 文件也可以放到一个远程路径被其他项目引用; * **构建配置:** Gradle 插件可以声明插件扩展来暴露可配置的属性,提供定制化能力。当然 .gradle 文件也可以做到,但实现会麻烦些。 #### Gradle 构建生命周期 **Gradle 构建的生命周期有三个阶段:****初始化阶段,配置阶段**和**运行阶段**。 ## Git ### 版本控制 **版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 除了项目源代码,你可以对任何类型的文件进行版本控制。** ### Git 简史 **Git 采用的是直接记录快照的方式,而非差异比较。我后面会详细介绍这两种方式的差别。** **大部分版本控制系统(CVS、Subversion、Perforce、Bazaar 等等)都是以文件变更列表的方式存储信息,这类系统****将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。** **具体原理如下图所示,理解起来其实很简单,每当我们提交更新一个文件之后,系统都会记录这个文件做了哪些更新,以增量符号 Δ(Delta)表示。** ### Git 的三种状态 **Git 有三种状态,你的文件可能处于其中之一:** 1. **已提交(committed)**:数据已经安全的保存在本地数据库中。 2. **已修改(modified)**:已修改表示修改了文件,但还没保存到数据库中。 3. **已暂存(staged)**:表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 **由此引入 Git 项目的三个工作区域的概念:****Git 仓库(.git directory)**、**工作目录(Working Directory)** 以及 **暂存区域(Staging Area)** 。 **基本的 Git 工作流程如下:** 1. **在工作目录中修改文件。** 2. **暂存文件,将文件的快照放入暂存区域。** 3. **提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。** ### [Git操作](https://javaguide.cn/tools/git/git-intro.html#%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%9A%84-git-%E6%8F%90%E4%BA%A4%E6%B6%88%E6%81%AF) ## NPM | **选项** | **说明** | **示例** | | ---------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | **search** | **在存储库中查找模块包** | **npm search express** | | **install** | **使用在存储库或本地位置上的一个package.json文件来安装包** | **npm install** | | **install -g** | **在全局可访问的位置安装一个包** | **npm install express -g** | | **uninstall** | **卸载一个模块** | **npm uninstall express** | | **remove** | **删除一个模块** | | | **pack** | **把在一个package.json文件中定义的模块封装成.tgz文件** | **npm pack** | | **view** | **显示模块的详细信息** | **npm view express** | | **publish** | **把在一个package.json文件中定义的模块发布到注册表** | **npm publish** | | **unpublish** | **取消发布您已发布到注册表的一个模块(在某些情况下,还需使用**–**force 选项)** | **npm unpublish myModule** | | **owner** | **允许您在存储库中添加、删除包和列出包的所有者** | **npm add****myModule**npm rm****myModule**npm ls myModule** | | **whoami** | **根据指定注册表模块)打印用户名** | **npm whoami** | | **adduser** | **将用户信息添加到当前的开发环境** | **npm adduser** **login** | | **logout** | **将用户信息从当前的开发环境中清除** | **npm logout** | | **init** | **初始化Node包的信息,会创建package.json文件** | **npm init** | | **login** | **等同于adduser** | **npm login** | # 系统设计 ## RestFul API【前端中概率】 ### API定义 **API(Application Programming Interface)** 翻译过来是应用程序编程接口的意思。 ### RESTful API定义 **RESTful API** 经常也被叫做 **REST API**,它是基于 REST 构建的 API。 **RESTful API 可以让你看到 URL+Http Method 就知道这个 URL 是干什么的,让你看到了 HTTP 状态码(status code)就知道请求结果如何。** ### REST定义 **REST** 是 `REpresentational State Transfer` 的缩写。这个词组的翻译过来就是“**表现层状态转化**”。 **这样理解起来甚是晦涩,实际上 REST 的全称是 ****Resource Representational State Transfer** ,直白地翻译过来就是 **“资源”在网络传输中以某种“表现形式”进行“状态转移”** 。如果还是不能继续理解,请继续往下看,相信下面的讲解一定能让你理解到底啥是 REST 。 **我们分别对上面涉及到的概念进行解读,以便加深理解,实际上你不需要搞懂下面这些概念,也能看懂我下一部分要介绍到的内容。不过,为了更好地能跟别人扯扯 “RESTful API”我建议你还是要好好理解一下!** * **资源(Resource)** :我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。比如我们的班级 classes 是代表一个集合形式的资源,而特定的 class 代表单个个体资源。每一种资源都有特定的 URI(统一资源标识符)与之对应,如果我们需要获取这个资源,访问这个 URI 就可以了,比如获取特定的班级:`/class/12`。另外,资源也可以包含子资源,比如 `/classes/classId/teachers`:列出某个指定班级的所有老师的信息 * **表现形式(Representational)**:**“**资源**”**是一种信息实体,它可以有多种外在表现形式。我们把**“**资源**”**具体呈现出来的形式比如 `json`,`xml`,`image`,`txt` 等等叫做它的**“**表现层/表现形式**”**。 * **状态转移(State Transfer)** :大家第一眼看到这个词语一定会很懵逼?内心 BB:这尼玛是啥啊? 大白话来说 REST 中的状态转移更多地描述的服务器端资源的状态,比如你通过增删改查(通过 HTTP 动词实现)引起资源状态的改变。ps:互联网通信协议 HTTP 协议,是一个无状态协议,所有的资源状态都保存在服务器端。 **总结** 1. **每一个 URI 代表一种资源;** 2. **客户端和服务器之间,传递这种资源的某种表现形式比如 **`json`,`xml`,`image`,`txt` 等等; 3. **客户端通过特定的 HTTP 动词,对服务器端资源进行操作,实现**“**表现层状态转化**”**。** ### RESTful API 规范【一面问过】 #### 请求方法 * `GET`:请求从服务器获取特定资源。举个例子:`GET /classes`(获取所有班级) * `POST` :在服务器上创建一个新的资源。举个例子:`POST /classes`(创建班级) * `PUT` :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:`PUT /classes/12`(更新编号为 12 的班级) * `DELETE` :从服务器删除特定的资源。举个例子:`DELETE /classes/12`(删除编号为 12 的班级) * `PATCH` :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。 #### 请求路径(接口命名) 1. **网址中不能有动词,只能有名词,API 中的名词也应该使用复数。** 因为 REST 中的资源往往和数据库中的表对应,而数据库中的表都是同种记录的**“**集合**”**(collection)。如果 API 调用并不涉及资源(如计算,翻译等操作)的话,可以用动词。比如:`GET /calculate?param1=11¶m2=33` 。 2. **不用大写字母,建议用中杠 - 不用下杠 _** 。比如邀请码写成 `invitation-code`而不是 invitation_code 。 3. **善用版本化 API**。当我们的 API 发生了重大改变而不兼容前期版本的时候,我们可以通过 URL 来实现版本化,比如 `http://api.example.com/v1`、`http://apiv1.example.com` 。版本不必非要是数字,只是数字用的最多,日期、季节都可以作为版本标识符,项目团队达成共识就可。 4. **接口尽量使用名词,避免使用动词。** RESTful API 操作(HTTP Method)的是资源(名词)而不是动作(动词)。 > **Eg:** > > ``` > GET /classes:列出所有班级 > POST /classes:新建一个班级 > GET /classes/{classId}:获取某个指定班级的信息 > PUT /classes/{classId}:更新某个指定班级的信息(一般倾向整体更新) > PATCH /classes/{classId}:更新某个指定班级的信息(一般倾向部分更新) > DELETE /classes/{classId}:删除某个班级 > GET /classes/{classId}/teachers:列出某个指定班级的所有老师的信息 > GET /classes/{classId}/students:列出某个指定班级的所有学生的信息 > DELETE /classes/{classId}/teachers/{ID}:删除某个指定班级下的指定的老师的信息 > ``` #### 过滤信息(Filtering) **如果我们在查询的时候需要添加特定条件的话,建议使用 url 参数的形式。** ``` GET /classes?state=active&name=guidegege//查询 state 状态为 active 并且 name 为 guidegege 的班级 GET /classes?page=1&size=10 //指定第1页,每页10个数据-分页查询 ``` ### RESTful 的极致 HATEOAS【很小概率问】 > **RESTful 的极致是 hateoas ,但是这个基本不会在实际项目中用到。** **上面是 RESTful API 最基本的东西,也是我们平时开发过程中最容易实践到的。实际上,RESTful API 最好做到 Hypermedia,即返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。** **比如,当用户向 **`api.example.com` 的根目录发出请求,会得到这样一个返回结果 ``` { "link": { "rel": "collection https://www.example.com/classes", "href": "https://api.example.com/classes", "title": "List of classes", "type": "application/vnd.yourformat+json" } } ``` **上面代码表示,文档中有一个 **`link` 属性,用户读取这个属性就知道下一步该调用什么 API 了。`rel` 表示这个 API 与当前网址的关系(collection 关系,并给出该 collection 的网址),`href` 表示 API 的路径,title 表示 API 的标题,`type` 表示返回类型 `Hypermedia API` 的设计被称为[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS) ## 代码命名指南【**专业基础(中概率)**】 ### 为什么需要重视命名? **因为 ****好的命名即是注释,别人一看到你的命名就知道你的变量、方法或者类是做什么的!** **简单来说就是 ****别人根据你的命名就能知道你的代码要表达的意思** (不过,前提这个人也要有基本的英语知识,对于一些编程中常见的单词比较熟悉)。 ### 常见命名规则以及适用场景 #### 驼峰命名法(CamelCase) **驼峰命名法应该我们最常见的一个,这种命名方式使用大小写混合的格式来区别各个单词,并且单词之间不使用空格隔开或者连接字符连接的命名方式** #### 大驼峰命名法(UpperCamelCase) **类名需要使用大驼峰命名法(UpperCamelCase)** **Eg:ServiceDiscovery、ServiceInstance、LruCacheFactory** #### 小驼峰命名法(lowerCamelCase) **方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。** **Eg:** ``` getUserInfo() createCustomThreadPool() setNameFormat(String nameFormat) Uservice userService; ``` #### 蛇形命名法(snake_case) **测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)** **Eg:** ``` @Test void should_get_200_status_code_when_request_is_valid() { ...... } ``` #### 串式命名法(kebab-case) **在串式命名法中,各个单词之间通过连接符“-”连接,比如**`dubbo-registry`。 **建议项目文件夹名称使用串式命名法(kebab-case),比如 dubbo 项目的各个模块的命名是下面这样的。** ### 常见命名规范 #### Java 语言基本命名规范 **1、类名需要使用大驼峰命名法(UpperCamelCase)风格。方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。** **2、测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)**,比如`should_get_200_status_code_when_request_is_valid`、`CLIENT_CONNECT_SERVER_FAILURE`。并且,**测试方法名称要求全部小写,常量以及枚举名称需要全部大写。** **3、项目文件夹名称使用串式命名法(kebab-case),比如`dubbo-registry`。** **4、包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 **“**.**”** 分隔符连接,并且各个单词必须为单数。** **正例: **`org.apache.dubbo.common.threadlocal` **5、抽象类命名使用 Abstract 开头**。 **6、异常类命名使用 Exception 结尾。** **7、测试类命名以它要测试的类的名称开始,以 Test 结尾。** **POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。** **如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。** #### 命名易读性规范 **1、为了能让命名更加易懂和易读,尽量不要缩写/简写单词,除非这些单词已经被公认可以被这样缩写/简写。比如 `CustomThreadFactory` 不可以被写成 ~~`CustomTF` 。** **2、命名不像函数一样要尽量追求短,可读性强的名字优先于简短的名字,虽然可读性强的名字会比较长一点。** 这个对应我们上面说的第 1 点。 **3、避免无意义的命名,你起的每一个名字都要能表明意思。** **正例:**`UserService userService;``int userCount`; **反例: **`UserService service``int count` **4、避免命名过长(50 个字符以内最好),过长的命名难以阅读并且丑陋。** **5、不要使用拼音,更不要使用中文。** 不过像 alibaba 、wuhan、taobao 这种国际通用名词可以当做英文来看待。 ## 代码重构指南【(低概率)】 ### 何谓重构? **重构就是利用设计模式(如组合模式、策略模式、责任链模式)、软件设计原则(如 SOLID 原则、YAGNI 原则、KISS 原则)和重构手段(如封装、继承、构建测试体系)来让代码更容易理解,更易于修改。** **软件设计原则指导着我们组织和规范代码,同时,重构也是为了能够尽量设计出尽量满足软件设计原则的软件。** **正确重构的核心在于 ****步子一定要小,每一步的重构都不会影响软件的正常运行,可以随时停止重构。** ### 为什么要重构? **让代码更容易理解** : 通过添加注释、命名规范、逻辑优化等手段可以让我们的代码更容易被理解; **避免代码腐化** :通过重构干掉坏味道代码; **加深对代码的理解** :重构代码的过程会加深你对某部分代码的理解; **发现潜在 bug** :是这样的,很多潜在的 bug ,都是我们在重构的过程中发现的; ### 何时进行重构? **重构在是开发过程中随时可以进行的,见机行事即可,并不需要单独分配一两天的时间专门用来重构。** * **提交代码之前** * **开发一个新功能之后&之前** * **Code Review 之后** * **捡垃圾式重构** * **阅读理解代码的时候** ### 重构有哪些注意事项? * **单元测试是重构的保护网** **单元测试可以为重构提供信心,降低重构的成本。我们要像重视生产代码那样,重视单元测试。** **另外,多提一句:持续集成也要依赖单元测试,当持续集成服务自动构建新代码之后,会自动运行单元测试来发现代码错误。** **怎样才能算单元测试呢?** 网上的定义很多,很抽象,很容易把人给看迷糊了。我觉得对于单元测试的定义主要取决于你的项目,一个函数甚至是一个类都可以看作是一个单元。就比如说我们写了一个计算个人股票收益率的方法,我们为了验证它的正确性专门为它写了一个单元测试。再比如说我们代码有一个类专门负责数据脱敏,我们为了验证脱敏是否符合预期专门为这个类写了一个单元测试。 **单元测试也是需要重构或者修改的。**[《代码整洁之道:敏捷软件开发手册》](https://book.douban.com/subject/4199741/) * **不要为了重构而重构** **重构一定是要为项目带来价值的!** 某些情况下我们不应该进行重构: * **学习了某个设计模式/工程实践之后,不顾项目实际情况,刻意使用在项目上(避免货物崇拜编程);** * **项目进展比较急的时候,重构项目调用的某个 API 的底层代码(重构之后对项目调用这个 API 并没有带来什么价值);** * **重写比重构更容易更省事;** ## 单元测试【专业基础(中低概率)】 #### 何谓单元测试? **维基百科是这样介绍单元测试的:** > **在计算机编程中,单元测试(Unit Testing)是针对程序模块(软件设计的最小单位)进行的正确性检验测试工作。** > > **程序单元是应用的 ****最小可测试部件** 。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 **由于每个单元有独立的逻辑,在做单元测试时,为了隔离外部依赖,确保这些依赖不影响验证逻辑,我们经常会用到 Fake、Stub 与 Mock 。** #### 为什么需要单元测试? 1. **为重构保驾护航** 2. **提高代码质量减少 ** 3. **bug快速定位 bug ** 4. **持续集成依赖单元测试** ## 安全【(中低概率)】 ### 认证授权基础概念 1. **Authentication(认证)** 是验证您的身份的凭据(例如用户名/用户 ID 和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。 2. **Authorization(授权)** 发生在 **Authentication(认证)** 之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。 3. **RBAC 模型** RBAC 即基于角色的权限访问控制(Role-Based Access Control)。这是一种通过角色关联权限,角色同时又关联用户的授权的方式。 **简单地说:一个用户可以拥有若干角色,每一个角色又可以被分配若干权限,这样就构造成“用户-角色-权限” 的授权模型。在这种模型中,用户与角色、角色与权限之间构成了多对多的关系** **在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。** 4. **`Cookie` 存放在客户端,一般用来保存用户信息**。 5. **`Session` 的主要作用就是通过服务端记录用户的状态。** `Cookie` 数据保存在客户端(浏览器端),`Session` 数据保存在服务器端。相对来说 `Session` 安全性更高。如果使用 `Cookie` 的一些敏感信息不要写入 `Cookie` 中,最好能将 `Cookie` 信息加密然后使用到的时候再去服务器端解密。 6. **如果没有 Cookie 的话 Session 还能用吗?** **一般是通过 **`Cookie` 来保存 `SessionID` ,假如你使用了 `Cookie` 保存 `SessionID` 的方案的话, 如果客户端禁用了 `Cookie`,那么 `Session` 就无法正常工作。 **但是,并不是没有 **`Cookie` 之后就不能用 `Session` 了,比如你可以将 `SessionID` 放在请求的 `url` 里面`https://javaguide.cn/?Session_id=xxx` 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 `SessionID` 进行一次加密之后再传入后端。 7. **为什么 Cookie 无法防止 CSRF 攻击,而 Token 可以?** **上面也提到过,进行 **`Session` 认证的时候,我们一般使用 `Cookie` 来存储 `SessionId`,当我们登陆后后端生成一个 `SessionId` 放在 Cookie 中返回给客户端,服务端通过 Redis 或者其他存储工具记录保存着这个 `SessionId`,客户端登录以后每次请求都会带上这个 `SessionId`,服务端通过这个 `SessionId` 来标示你这个人。如果别人通过 `Cookie` 拿到了 `SessionId` 后就可以代替你的身份访问系统了。 `Session` 认证中 `Cookie` 中的 `SessionId` 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。 > **不论是 `Cookie` 还是 `Token` 都无法避免 **跨站脚本攻击(Cross Site Scripting)XSS。 8. **如何防止CSRF攻击?** [美团官方答案](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) 9. **什么是 OAuth 2.0?** **OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0 是对 OAuth 1.0 的完全重新设计,OAuth 2.0 更快,更容易实现,OAuth 1.0 已经被废弃。** **实际上它就是一种授权机制,它的最终目的是为第三方应用颁发一个有时效性的令牌 Token,使得第三方应用能够通过该令牌获取相关的资源。** **OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。** **另外,现在 OAuth 2.0 也常见于支付场景(微信支付、支付宝支付)和开发平台(微信开放平台、阿里开放平台等等)。** 10. **什么是 SSO?** **SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。** **SSO 有什么好处?** * **用户角度** :用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。 * **系统管理员角度** : 管理员只需维护好一个统一的账号中心就可以了,方便。 * **新系统开发角度:** 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。 ### JWT 基础概念【(低概率)】 #### 什么是 JWT? **JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。** **JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。** **可以看出,****JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则** 。 **并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。** #### JWT 由哪些部分组成? **JWT 本质上就是一组字串,通过(**`.`)切分成三个为 Base64 编码的部分: * **Header** : 描述 JWT 的元数据,定义了生成签名的算法以及 `Token` 的类型。 * **Payload** : 用来存放实际需要传递的数据 * **Signature(签名)** :服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。 **JWT 通常是这样的:**`xxxxx.yyyyy.zzzzz`。 #### 如何基于 JWT 进行身份验证? **在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。** 1. **用户向服务器发送用户名、密码以及验证码用于登陆系统。** 2. **如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。** 3. **用户以后每次向后端发请求都在 Header 中带上这个 JWT 。** 4. **服务端检查 JWT 并从中获取用户相关信息。** **两点建议**: 1. **建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。** 2. **请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 **`Authorization` 字段中(`Authorization: Bearer Token`)。 #### 如何防止 JWT 被篡改? **密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。** #### 如何加强 JWT 的安全性? **使用安全系数高的加密算法。** **使用成熟的开源库,没必要造轮子。** **JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。** **一定不要将隐私信息存放在 Payload 当中。** **密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。** **Payload 要加入 **`exp` (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。 ## Java 定时任务【(极低概率)】 [网站](https://javaguide.cn/system-design/schedule-task.html) ## 前端&移动端实时消息推送【(低概率)】 ### 短轮询 **轮询(polling)** 应该是实现消息推送方案中最简单的一种,这里我们暂且将轮询分为短轮询和长轮询。 **短轮询很好理解,指定的时间间隔,由浏览器向服务器发出 HTTP 请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。** **一个简单的 JS 定时器就可以搞定,每秒钟请求一次未读消息数接口,返回的数据展示即可。** ``` setInterval(() => { // 方法请求 messageCount().then((res) => { if (res.code === 200) { this.messageCount = res.data } }) }, 1000); ``` **效果还是可以的,短轮询实现固然简单,缺点也是显而易见,由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。** ### 长轮询 **长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。长轮询在中间件中应用的很广泛,比如 Nacos 和 Apollo 配置中心,消息队列 Kafka、RocketMQ 中都有用到长轮询。** **长轮询其实原理跟轮询差不多,都是采用轮询的方式。不过,如果服务端的数据没有发生变更,会 一直 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询。** ### iframe 流 **iframe 流就是在页面中插入一个隐藏的**`<iframe>`标签,通过在`src`中请求消息数量 API 接口,由此在服务端和客户端之间创建一条长连接,服务端持续向`iframe`传输数据。 **传输的数据通常是 HTML、或是内嵌的JavaScript 脚本,来达到实时更新页面的效果。** **iframe 流的服务器开销很大,而且IE、Chrome等浏览器一直会处于 loading 状态,图标会不停旋转,简直是强迫症杀手。****iframe 流非常不友好,强烈不推荐。** ### [SSE](https://javaguide.cn/system-design/web-real-time-message-push.html#sse-%E6%88%91%E7%9A%84%E6%96%B9%E5%BC%8F) **服务器发送事件(Server-Sent Events),简称 SSE。这是一种服务器端到客户端(浏览器)的单向消息推送。** **SSE 基于 HTTP 协议的,我们知道一般意义上的 HTTP 协议是无法做到服务端主动向客户端推送消息的,但 SSE 是个例外,它变换了一种思路。** **SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:** * **SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。** * **SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。** * **SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。** * **SSE 默认支持断线重连;WebSocket 则需要自己实现。** * **SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。** **SSE 与 WebSocket 该如何选择?** > **技术并没有好坏之分,只有哪个更合适** **SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。** **但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SEE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。** ### Websocket【常用】 **Websocket 应该是大家都比较熟悉的一种实现消息推送的方式,上边我们在讲 SSE 的时候也和 Websocket 进行过比较。** **是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。** ### MQTT **MQTT (Message Queue Telemetry Transport)是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。** **该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。** **TCP 协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于 TCP/IP 协议上,也就是说只要支持 TCP/IP 协议栈的地方,都可以使用 MQTT 协议。** **为什么要用 MQTT 协议?** **MQTT 协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP 协议呢?** * **首先 HTTP 协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合 IOT 应用程序。** * **HTTP 是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。** * **通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP 要实现这样的功能不但很困难,而且成本极高。** ### 总结 | | **介绍** | **优点** | **缺点** | | --------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------- | | **短轮询** | **客户端定时向服务端发送请求,服务端直接返回响应数据(即使没有数据更新)** | **简单、易理解、易实现** | **实时性太差,无效请求太多,频繁建立连接太耗费资源** | | **长轮询** | **与短轮询不同是,长轮询接收到客户端请求之后等到有数据更新才返回请求** | **减少了无效请求** | **挂起请求会导致资源浪费** | | **iframe 流** | **服务端和客户端之间创建一条长连接,服务端持续向**`iframe`传输数据。 | **简单、易理解、易实现** | **维护一个长连接会增加开销,效果太差(图标会不停旋转)** | | **SSE** | **一种服务器端到客户端(浏览器)的单向消息推送。** | **简单、易实现,功能丰富** | **不支持双向通信** | | **WebSocket** | **除了最初建立连接时用 HTTP 协议,其他时候都是直接基于 TCP 协议进行通信的,可以实现客户端和服务端的全双工通信。** | **性能高、开销小** | **对开发人员要求更高,实现相对复杂一些** | | **MQTT** | **基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息。** | **成熟稳定,轻量级** | **对开发人员要求更高,实现相对复杂一些** | # 高性能【只提一嘴,问的话再说】 ## CDN(内容分发网络) ### 什么是 CDN ? **CDN** 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 **内容分发网络** 。 **我们可以将内容分发网络拆开来看:** * **内容 :指的是静态资源比如图片、视频、文档、JS、CSS、HTML。** * **分发网络 :指的是将这些静态资源分发到位于多个不同的地理位置机房中的服务器上,这样,就可以实现静态资源的就近访问比如北京的用户直接访问北京机房的数据。** **所以,简单来说,****CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。** ### 总结 * **CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。** * **基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。** * **GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个 CDN 节点之间相互协作,最常用的是基于 DNS 的 GSLB。CDN 会通过 GSLB 找到最合适的 CDN 节点。** * **为了防止静态资源被盗用,我们可以利用 ****Referer 防盗链** + **时间戳防盗链** 。 ## 读写分离和分库分表详解【前端小概率问】 ### 读写分离 #### 什么是读写分离? **读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。** 这样的话,就能够小幅提升写性能,大幅提升读性能。 **一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。** #### 读写分离会带来什么问题?如何解决? **读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 ****主从同步延迟** 。 > ****无特别好的方法,需根据实际业务分析** **1.强制将读请求路由到主库处理。** **既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。** **2.延迟读取。** **还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。** **不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。** #### 如何实现读写分离? 1. **部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。** 2. **保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的****主从复制**。 3. **系统将写请求交给主数据库处理,读请求交给从数据库处理。** **1.代理方式** **我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。** **提供类似功能的中间件有 ****MySQL Router**(官方)、**Atlas**(基于 MySQL Proxy)、**Maxscale**、**MyCat**。 **2.组件方式** **在这种方式中,我们可以通过引入第三方组件来帮助我们读写请求。** **这也是我比较推荐的一种方式。这种方式目前在各种互联网公司中用的最多的,相关的实际的案例也非常多。如果你要采用这种方式的话,推荐使用 **`sharding-jdbc` ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。 #### 主从复制原理是什么? **MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。** 1. **主库将数据库中数据的变化写入到 binlog** 2. **从库连接主库** 3. **从库会创建一个 I/O 线程向主库请求更新的 binlog** 4. **主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收** 5. **从库的 I/O 线程将接收的 binlog 写入到 relay log 中。** 6. **从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。** **MySQL 主从复制是依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog 。** ### 分库分表 #### 什么是分库? **分库** 就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。 **垂直分库** 就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。 **举个例子:说你将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。** **水平分库** 是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。 **举个例子:订单表数据量太大,你对订单表进行了水平切分(水平分表),然后将切分后的 2 张订单表分别放在两个不同的数据库。** #### 什么是分表? **分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 **垂直分表** 是对数据表列的拆分,把一张列比较多的表拆分为多张表。 **举个例子:我们可以将用户信息表中的一些列单独抽出来作为一个表。** **水平分表** 是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。 **举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。** **水平拆分只能解决单表数据量大的问题,为了提升性能,我们通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现。** #### 什么情况下需要分库分表? * **单表的数据达到千万级别以上,数据库读写速度比较缓慢。** * **数据库中的数据占用的空间越来越大,备份时间越来越长。** * **应用的并发量太大。** #### 常见的分片算法有哪些? * **哈希分片** :求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。 * **范围分片** :按照特性的范围区间(比如时间区间、ID区间)来分配数据,比如 将 `id` 为 `1~299999` 的记录分到第一个库, `300000~599999` 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 * **地理位置分片** :很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。 * **融合算法** :灵活组合多种分片算法,比如将哈希分片和范围分片组合。 #### 分库分表会带来什么问题呢? * **join 操作** : 同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。 * **事务问题** :同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。 * **分布式 id** :分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。 #### 分库分表后,数据怎么迁移呢? **比较简单同时也是非常常用的方案就是****停机迁移**,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。 **如果你不想停机迁移数据的话,也可以考虑****双写方案**。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的: * **我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。** * **在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。** * **重复上一步的操作,直到老库和新库的数据一致为止。** **想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。** #### 总结 **读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。** **读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。** **分库** 就是将数据库中的数据分散到不同的数据库上。**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 **引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。** **ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。** # 高可用【只提一嘴,问的话再说】 ## 高可用系统设计指南 ### 什么是高可用?可用性的判断标准是啥? **高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。** **一般情况下,我们使用多少个 9 来评判一个系统的可用性,比如 99.9999% 就是代表该系统在所有的运行时间中只有 0.0001% 的时间是不可用的,这样的系统就是非常非常高可用的了!当然,也会有系统如果可用性不太好的话,可能连 9 都上不了。** **除此之外,系统的可用性还可以用某功能的失败次数与总的请求次数之比来衡量,比如对网站请求 1000 次,其中有 10 次请求失败,那么可用性就是 99%。** ### 哪些情况会导致系统不可用? 1. **黑客攻击;** 2. **硬件故障,比如服务器坏掉。** 3. **并发量/用户请求量激增导致整个服务宕掉或者部分服务不可用。** 4. **代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。** 5. **网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。** 6. **自然灾害或者人为破坏。** ### 有哪些提高系统可用性的方法? 1. **注重代码质量,测试严格把关** 2. **使用集群,减少单点故障 ** 3. **限流** 4. **超时和重试机制设置 ** 5. **熔断机制 ** 6. **异步调用 ** 7. **使用缓存** ## 冗余设计详解 **高可用集群(High Availability Cluster,简称 HA Cluster)、同城灾备、异地灾备、同城多活和异地多活是冗余思想在高可用系统设计中最典型的应用。** * **高可用集群** : 同一份服务部署两份或者多份,当正在使用的服务突然挂掉的话,可以切换到另外一台服务,从而保证服务的高可用。 * **同城灾备** :一整个集群可以部署在同一个机房,而同城灾备中相同服务部署在同一个城市的不同机房中。并且,备用服务不处理请求。这样可以避免机房出现意外情况比如停电、火灾。 * **异地灾备** :类似于同城灾备,不同的是,相同服务部署在异地(通常距离较远,甚至是在不同的城市或者国家)的不同机房中 * **同城多活** :类似于同城灾备,但备用服务可以处理请求,这样可以充分利用系统资源,提高系统的并发。 * **异地多活** : 将服务部署在异地的不同机房中,并且,它们可以同时对外提供服务。 **高可用集群单纯是服务的冗余,并没有强调地域。同城灾备、异地灾备、同城多活和异地多活实现了地域上的冗余。** **同城和异地的主要区别在于机房之间的距离。异地通常距离较远,甚至是在不同的城市或者国家。** **和传统的灾备设计相比,同城多活和异地多活最明显的改变在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。** **光做好冗余还不够,必须要配合上 ****故障转移** 才可以! 所谓故障转移,简单来说就是实现不可用服务快速且自动地切换到可用服务,整个过程不需要人为干涉。 ## 服务限流详解 ### 常见限流算法 #### 1. 固定窗口计数器算法 **固定窗口其实就是时间窗口。****固定窗口计数器算法** 规定了我们单位时间处理的请求数量。 **假如我们规定系统中某个接口 1 分钟只能访问 33 次的话,使用固定窗口计数器算法的实现思路如下:** * **给定一个变量 **`counter` 来记录当前接口处理的请求数量,初始值为 0(代表接口当前 1 分钟内还未处理请求)。 * **1 分钟之内每处理一个请求之后就将 **`counter+1` ,当 `counter=33` 之后(也就是说在这 1 分钟内接口已经被访问 33 次的话),后续的请求就会被全部拒绝。 * **等到 1 分钟结束后,将 **`counter` 重置 0,重新开始计数。 **这种限流算法无法保证限流速率,因而无法保证突然激增的流量。** **就比如说我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。** #### 2. 滑动窗口计数器算法 **滑动窗口计数器算法** 算的上是固定窗口计数器算法的升级版。 **滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:****它把时间以一定比例分片** 。 **例如我们的接口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理 不大于 **`60(请求数)/60(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 **很显然, ****当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。** #### 3. 漏桶算法 **我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。** **如果想要实现这个算法的话也很简单,准备一个队列用来保存请求,然后我们定期从队列中拿请求来执行就好了(和消息队列削峰/限流的思想是一样的)。** #### 4. 令牌桶算法 **令牌桶算法也比较简单。和漏桶算法算法一样,我们的主角还是桶(这限流算法和桶过不去啊)。不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。** ### 单机限流怎么做?(尽量不提) **单机限流针对的是单体架构应用。** **单机限流可以直接使用 Google Guava 自带的限流工具类 **`RateLimiter` 。 `RateLimiter` 基于令牌桶算法,可以应对突发流量。 > **Guava 地址:**[https://github.com/google/guava](https://github.com/google/guava) **除了最基本的令牌桶算法(平滑突发限流)实现之外,Guava 的**`RateLimiter`还提供了 **平滑预热限流** 的算法实现。 **平滑突发限流就是按照指定的速率放令牌到桶里,而平滑预热限流会有一段预热时间,预热时间之内,速率会逐渐提升到配置的速率。** **另外,****Bucket4j** 是一个非常不错的基于令牌/漏桶算法的限流库。 --- > **Bucket4j 地址:**[https://github.com/vladimir-bukhtoyarov/bucket4j](https://github.com/vladimir-bukhtoyarov/bucket4j) **相对于,Guava 的限流工具类来说,Bucket4j 提供的限流功能更加全面。不仅支持单机限流和分布式限流,还可以集成监控,搭配 Prometheus 和 Grafana 使用。** **不过,毕竟 Guava 也只是一个功能全面的工具类库,其提供的开箱即用的限流功能在很多单机场景下还是比较实用的。** **Spring Cloud Gateway 中自带的单机限流的早期版本就是基于 Bucket4j 实现的。后来,替换成了 ****Resilience4j**。 **Resilience4j 是一个轻量级的容错组件,其灵感来自于 Hystrix。自**[Netflix 宣布不再积极开发 Hystrix](https://github.com/Netflix/Hystrix/commit/a7df971cbaddd8c5e976b3cc5f14013fe6ad00e6) --- > **Resilience4j 地址: **[https://github.com/resilience4j/resilience4j](https://github.com/resilience4j/resilience4j) **一般情况下,为了保证系统的高可用,项目的限流和熔断都是要一起做的。** **Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重试等保障系统高可用开箱即用的功能。并且,Resilience4j 的生态也更好,很多网关都使用 Resilience4j 来做限流熔断的。** **因此,在绝大部分场景下 Resilience4j 或许会是更好的选择。如果是一些比较简单的限流场景的话,Guava 或者 Bucket4j 也是不错的选择。** ### 分布式限流怎么做?(尽量不提) **分布式限流针对的分布式/微服务应用架构应用,在这种架构下,单机限流就不适用了,因为会存在多种服务,并且一种服务也可能会被部署多份。** **分布式限流常见的方案:** * **借助中间件架限流** :可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。 * **网关层限流** :比较常用的一种方案,直接在网关层把限流给安排上了。不过,通常网关层限流通常也需要借助到中间件/框架。就比如 Spring Cloud Gateway 的分布式限流实现`RedisRateLimiter`就是基于 Redis+Lua 来实现的,再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。 **如果你要基于 Redis 来手动实现限流逻辑的话,建议配合 Lua 脚本来做。** **为什么建议 Redis+Lua 的方式?** 主要有两点原因: * **减少了网络开销** :我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 * **原子性** :一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 **我这里就不放具体的限流脚本代码了,网上也有很多现成的优秀的限流脚本供你参考,就比如 Apache 网关项目 ShenYu 的 RateLimiter 限流插件就基于 Redis + Lua 实现了令牌桶算法/并发令牌桶算法、漏桶算法、滑动窗口算法。** > **ShenYu 地址: **[https://github.com/apache/incubator-shenyu](https://github.com/apache/incubator-shenyu) ## 超时&重试详解 **由于网络问题、系统或者服务内部的 Bug、服务器宕机、操作系统崩溃等问题的不确定性,我们的系统或者服务永远不可能保证时刻都是可用的状态。** **为了最大限度的减小系统或者服务出现故障之后带来的影响,我们需要用到的 ****超时(Timeout)** 和 **重试(Retry)** 机制。 **想要把超时和重试机制讲清楚其实很简单,因为它俩本身就不是什么高深的概念。** **虽然超时和重试机制的思想很简单,但是它俩是真的非常实用。你平时接触到的绝大部分涉及到远程调用的系统或者服务都会应用超时和重试机制。尤其是对于微服务系统来说,正确设置超时和重试非常重要。单体服务通常只涉及数据库、缓存、第三方 API、中间件等的网络调用,而微服务系统内部各个服务之间还存在着网络调用。** ### 超时机制 #### 什么是超时机制? **超时机制说的是当一个请求超过指定的时间(比如 1s)还没有被处理的话,这个请求就会直接被取消并抛出指定的异常或者错误(比如 **`504 Gateway Timeout`)。 **我们平时接触到的超时可以简单分为下面 2 种:** * **连接超时(ConnectTimeout)** :客户端与服务端建立连接的最长等待时间。 * **读取超时(ReadTimeout)** :客户端和服务端已经建立连接,客户端等待服务端处理完请求的最长时间。实际项目中,我们关注比较多的还是读取超时。 **一些连接池客户端框架中可能还会有获取连接超时和空闲连接清理超时。** **如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。** **这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。** **我之前在实际项目就遇到过类似的问题,整个网站无法正常处理请求,服务器负载直接快被拉满。后面发现原因是项目超时设置错误加上客户端请求处理异常,导致服务端连接数直接接近 40w+,这么多堆积的连接直接把系统干趴了。** #### 超时时间应该如何设置? **超时到底设置多长时间是一个难题!超时值设置太高或者太低都有风险。如果设置太高的话,会降低超时机制的有效性,比如你设置超时为 10s 的话,那设置超时就没啥意义了,系统依然可能会出现大量慢请求堆积的问题。如果设置太低的话,就可能会导致在系统或者服务在某些处理请求速度变慢的情况下(比如请求突然增多),大量请求重试(超时通常会结合重试)继续加重系统或者服务的压力,进而导致整个系统或者服务被拖垮的问题。** **通常情况下,我们建议读取超时设置为 ****1500ms** ,这是一个比较普适的值。如果你的系统或者服务对于延迟比较敏感的话,那读取超时值可以适当在 **1500ms** 的基础上进行缩短。反之,读取超时值也可以在 **1500ms** 的基础上进行加长,不过,尽量还是不要超过 **1500ms** 。连接超时可以适当设置长一些,建议在 **1000ms ~ 5000ms** 之内。 **没有银弹!超时值具体该设置多大,还是要根据实际项目的需求和情况慢慢调整优化得到。** **更上一层,参考**[美团的Java线程池参数动态配置](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html) ### 重试机制 #### 什么是重试机制? **重试机制一般配合超时机制一起使用,指的是多次发送相同的请求来避免瞬态故障和偶然性故障。** **瞬态故障可以简单理解为某一瞬间系统偶然出现的故障,并不会持久。偶然性故障可以理解为哪些在某些情况下偶尔出现的故障,频率通常较低。** **重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。** #### 重试的次数如何设置? **重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。** **重试的次数通常建议设为 3 次。并且,我们通常还会设置重试的间隔,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。** #### 重试幂等 **超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。** **什么情况下会出现一个请求被多次执行呢?客户端等待服务端完成请求完成超时但此时服务端已经执行了请求,只是由于短暂的网络波动导致响应在发送给客户端的过程中延迟了。** **举个例子:用户支付购买某个课程,结果用户支付的请求由于重试的问题导致用户购买同一门课程支付了两次。对于这种情况,我们在执行用户购买课程的请求的时候需要判断一下用户是否已经购买过。这样的话,就不会因为重试的问题导致重复购买了。** 最后修改:2023 年 03 月 30 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,请随意赞赏,谢谢
此处评论已关闭