-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 263 KB
/
index.json
1
[{"categories":["复习","校招","Android"],"content":"引言 先放一张 Google 官方提供的经典分层架构图 Android 软件堆栈\" Android 软件堆栈 总的来说,Android 的系统体系结构分为 5 层,自顶向下依次是: 系统应用层 Java API 框架层 (Framework) 原生 C/C++ 库和 Android Runtime(即系统 Native 运行库和 Android 运行时环境) 硬件抽象层 (HAL) Linux 内核层 其中每一层都包含大量的子模块或子系统。 参考链接: 平台架构 | Android 开发者 | Android Developers Android 操作系统架构开篇 Android 系统架构 · 笔试面试知识整理 掌握 Android 系统架构,看这一篇就够了! ","date":"2021-12-10","objectID":"/post/e553bd11/:0:0","tags":["校招","秋招","学习","复习","Android","系统"],"title":"Android 系统架构","uri":"/post/e553bd11/"},{"categories":["复习","校招","Android"],"content":"简介 ANR 全称 Application Not Responding,即应用程序无响应。 主要原因(为什么会发生 ANR?) 主线程(UI 线程)在规定时间内没有处理完相应的工作,就会出现 ANR。 ","date":"2021-12-10","objectID":"/post/d1aa87c3/:0:0","tags":["校招","秋招","学习","复习","Android","ANR"],"title":"Android ANR 原因、定位和避免","uri":"/post/d1aa87c3/"},{"categories":["复习","校招","Android"],"content":"ANR 场景 ","date":"2021-12-10","objectID":"/post/d1aa87c3/:1:0","tags":["校招","秋招","学习","复习","Android","ANR"],"title":"Android ANR 原因、定位和避免","uri":"/post/d1aa87c3/"},{"categories":["复习","校招","Android"],"content":"ANR 超时阈值表 类型 描述 前台 后台 Service 服务 20s 200s Broadcast 广播 10s 60s ContentProvider 内容提供者 10s – InputEventDispatching (常见) 输入事件分发(包括按键和触摸事件) 5s – 以上四个条件的执行时间达到阈值,均可造成 ANR 的发生。 ","date":"2021-12-10","objectID":"/post/d1aa87c3/:1:1","tags":["校招","秋招","学习","复习","Android","ANR"],"title":"Android ANR 原因、定位和避免","uri":"/post/d1aa87c3/"},{"categories":["复习","校招","Android"],"content":"Android 中哪些操作是在主线程呢? Activity 的所有生命周期回调都是在主线程执行的。 Service 默认在主线程执行。 BroadcastReceiver 的 onReceive 回调是在主线程执行的。 没有使用子线程的 looper 的 Handler 的 handleMessage.post(Runnable) 是在主线程执行的。 AsyncTask 的回调中除了 doInBackground,其他都是在主线程执行的。 定位 ANR ","date":"2021-12-10","objectID":"/post/d1aa87c3/:1:2","tags":["校招","秋招","学习","复习","Android","ANR"],"title":"Android ANR 原因、定位和避免","uri":"/post/d1aa87c3/"},{"categories":["复习","校招","Android"],"content":"方法一:log 可以在 log 中搜索ANR in或am_anr,该行会包含 ANR 的时间、进程以及何种 ANR 等信息。 ","date":"2021-12-10","objectID":"/post/d1aa87c3/:2:0","tags":["校招","秋招","学习","复习","Android","ANR"],"title":"Android ANR 原因、定位和避免","uri":"/post/d1aa87c3/"},{"categories":["复习","校招","Android"],"content":"方法二:traces.txt 使用adb命令 adb pull /data/anr/traces.txt traces.txt 会保存到当前所在的目录下,从 trace.txt 文件查看调用 stack。 如何解决/避免 ANR? 使用 AsyncTask 处理耗时 I/O 操作 使用 Thread 或者 HandlerThread 提高优先级。(HandlerThread???) 使用 Handler 来处理工作线程的耗时任务。 Activity 的 onCreate() 和 onResume() 回调中尽量避免耗时的操作。 事实上,onPause() 中也应尽量避免耗时,可以放到 onStop() 中处理,这样可以让新的 Activity 尽快显示出来并切换到前台。 【因为栈顶的 Activity 需要先 onPause 后(消失于前台),新的 Activity 才能启动。】 造成 ANR 的更多原因及解决办法 以上只是由于简单的主线程耗时造成的 ANR,实际上还有其他原因: 原因 解决办法 主线程阻塞或主线程数据读取(死锁/线程锁) 避免死锁的出现,使用子线程来处理耗时操作或阻塞任务。尽量避免在主线程 Query Provider,不要滥用 SharePreferences( SP 本章不展开讲) CPU 满负荷,I/O 阻塞 文件读写或数据库操作放在子线程异步操作 内存不足 Manifest 中 设置 android:largeHeap=“true”,增大 App 分配的内存。但是不建议使用此法,从根本上防止内存泄漏,优化内存使用才是正道。 各大组件 ANR 各大组件生命周期中也应避免耗时操作,具体参考上面的超时阈值。 参考链接: 深入理解Android ANR(面试及实战) 彻底理解安卓应用无响应机制 Android ANR:原理分析及解决办法 Android异常与性能优化相关面试问题-ANR异常面试问题详解 99%面试必问题目:讲一下ANR吧! 妥妥的去面试之Android基础(一) - K码农 看完这篇 Android ANR 分析,就可以和面试官装逼了! 2021Android 高级面试题及答案,Android 篇 ","date":"2021-12-10","objectID":"/post/d1aa87c3/:3:0","tags":["校招","秋招","学习","复习","Android","ANR"],"title":"Android ANR 原因、定位和避免","uri":"/post/d1aa87c3/"},{"categories":["复习","校招","Android","Java"],"content":"对问题的疑惑 其实这个问题问的很奇怪,看到这个问题的瞬间,都会想到问的是: new Thread(runnable).start() 和 runnable.run() 的区别? 可是 runnable.run() 明明就只是单纯地执行一个对象里面的方法, thread.start()才是线程从新建到就绪的方法,此时线程等待CPU调度,得到资源才会去执行run() 这种问题很呆,所以我觉得应该不是问这个(=。=) 实现 Runnable 和继承 Thread 的区别? 我觉得这样问才是问题真正的问法,那我们看一下三段代码: // 继承 Thread 1 class MyThread extends Thread { public void run(){ // TODO Auto-generated method stub } } public static void main(String[] args) { // TODO Auto-generated method stub new MyThread().start(); new MyThread().start(); } // 继承 Thread 2 class MyThread extends Thread { public void run(){ // TODO Auto-generated method stub } } public static void main(String[] args) { // TODO Auto-generated method stub MyThread mt = new MyThread(); new MyThread(mt).start(); new MyThread(mt).start(); } // 实现 Runnable class MyRunnable implements Runnable { public void run(){ // TODO Auto-generated method stub } } public static void main(String[] args) { // TODO Auto-generated method stub MyRunnable mr = new MyRunnable(); new Thread(mr).start(); new Thread(mr).start(); } 实际上在没有成员变量的情况下,三者的运行结果并无任何区别。 若是有成员变量,后两种写法会因为成员变量所在的对象只被实例化了一次而导致结果有所区别。当然,还有多线程必须要注意同步加锁的问题。这些与问题无关的这里就不展开说了。 看到网上很多都在说: 代码可以被多个线程复用,增加程序的健壮性; 我想说的是, 难道继承就不能复用代码了吗? (3 段代码的两个线程都是执行同一个run()方法) 适合多个相同的程序代码的线程去处理同一个资源(即线程资源共享); 我想说的是, 难道继承就不能共享了吗? (第 2 段代码) 可以避免 Java 中的单继承的限制; 我想说的是……这点没的说,确实可以避免~ 简单分析 Thread 源码 嗯,本章只是很简单的看一下 Thread 的头部,不深入分析~ public class Thread implements Runnable { // ... private volatile String name; private int priority; private Thread threadQ; // ... } 可见,Thread 也实现了 Runnable 接口,只是提供了更多的可用方法和成员而已。 结论 综上所述,Thread 和 Runnable 的实质是继承关系,没有可比性。 事实是,Thread 和 Runnable 没有本质的区别,区别只是在用法上需不需要多继承以及是否有执行复杂线程操作的需求。 参考链接: 彻底理解Runnable和Thread的区别 被自己蠢哭了的 Runnable.run() 方法 ","date":"2021-12-09","objectID":"/post/6ee1cab0/:0:0","tags":["校招","秋招","学习","复习","Android","Java","线程","Thread","Runnable"],"title":"Thread 和 Runnable 的区别","uri":"/post/6ee1cab0/"},{"categories":["Hugo"],"content":"前面 Hexo 迁移到 Hugo 记录 中提到过怎么在 Hugo 加入 Service Worker,本文基于前面的文章源码进行修改。 要支持缓存更新提示刷新其实很简单,在博客根目录/layouts/_default/baseof.html中加入以下代码即可,这里用了 Noty 的消息提示插件库: ... \u003cscript src=\"https://cdn.jsdelivr.net/npm/[email protected]/lib/noty.min.js\" type=\"text/javascript\"\u003e\u003c/script\u003e \u003clink href=\"https://cdn.jsdelivr.net/npm/[email protected]/lib/noty.css\" rel=\"stylesheet\" type=\"text/css\" /\u003e \u003clink href=\"https://cdn.jsdelivr.net/npm/[email protected]/lib/themes/metroui.css\" rel=\"stylesheet\" type=\"text/css\" /\u003e \u003cscript\u003e // 可以这么注册 Service Worker if ('serviceWorker' in navigator) { // 监听 Service Worker 状态,有更新即提示 navigator.serviceWorker.addEventListener('controllerchange', function (ev) { try { console.log(\"Cache Update!\"); new Noty({ type: \"info\", theme: \"metroui\", closeWith: ['click'], callbacks: { onClick: () =\u003e { window.location.reload(); } }, timeout: \"5000\", text: \"\u003cstyle\u003e#tips:link{color: #2a2a2a} #tips:visited{color: #4b0173} #tips:hover{color: #f5f5ff} #tips:active{color: #cccccc}\u003c/style\u003e\u003cdiv\u003e\u003cb\u003e检测到站点需要更新\u003c/b\u003e\u003c/div\u003e\u003ca id='tips' href='#' onclick='window.location.reload();'\u003e点击刷新页面\u003c/a\u003e 更新站点\", }).show(); }catch (e) { } }); ... } \u003c/script\u003e ... 效果就不放了,自己测试一下就知道了,随便改一下precache中任意一个文件的 revision (MD5) 或者改一下Service Worker的 suffix (version) 都可以触发。 唯一美中不足的地方就是首次进入博客会提示刷新,因为首次进入博客缓存必然会有变动…… ","date":"2020-08-25","objectID":"/post/17740ab6/:0:0","tags":["Hugo","教程","个人经验"],"title":"为 Service Worker 添加缓存更新刷新提示","uri":"/post/17740ab6/"},{"categories":["Hugo"],"content":" 参考: Hugo 篇四:添加友链卡片 shortcodes jerryc127/hexo-theme-butterfly - GitHub 本文借鉴了 hexo-theme-butterfly 的样式以 Hugo LoveIt 主题为基础进行 Hugo 的友链界面开(chao)发(xi)。 废话不多说,直入正题~ 代码 虽然可以直接改themes/LoveIt的内容,但是我用了git submodule的方式下载的主题,若要修改则必须建立自己的仓库拥有改过后的主题内容,否则即使提交了也没法 CI 部署修改后的主题内容(而且会报错);同时,为了方便日后更新主题,我个人还是建议在assets目录下更改。 注意:后面说的assets和layouts目录都不是themes/LoveIt/*下的,而是博客根目录/* layouts/shortcodes/下面新建friend.html文件: {{ if .IsNamedParams }} {{- $src := .Get \"logo\" -}} {{- $small := .Get \"logo_small\" | default $src -}} {{- $large := .Get \"logo_large\" | default $src -}} \u003cdiv class=\"friend-div\"\u003e \u003ca target=\"_blank\" href={{ .Get \"url\" | safeURL }} title={{ .Get \"name\" }} \u003e \u003cimg class=\"lazyload\" src=\"/svg/loading.min.svg\" data-src={{ $src | safeURL }} alt={{ .Get \"name\" }} data-sizes=\"auto\" data-srcset=\"{{ $small | safeURL }}, {{ $src | safeURL }} 1.5x, {{ $large | safeURL }} 2x\" /\u003e \u003cspan class=\"friend-name\"\u003e{{ .Get \"name\" }}\u003c/span\u003e \u003cspan class=\"friend-info\"\u003e{{ .Get \"word\" }}\u003c/span\u003e \u003c/a\u003e \u003c/div\u003e {{ end }} assets/css/_partial/_single/下面新建_friend.scss文件: #article-container { word-wrap: break-word; overflow-wrap: break-word } #article-container a { color: #49b1f5 } #article-container a:hover { text-decoration: underline } #article-container img { margin: 0 auto .8rem } .flink#article-container .friend-list-div \u003e .friend-div a .friend-info, .flink#article-container .friend-list-div \u003e .friend-div a .friend-name { overflow: hidden; -o-text-overflow: ellipsis; text-overflow: ellipsis; white-space: nowrap } .flink#article-container .friend-list-div { overflow: auto; padding: 10px 10px 0; text-align: center; } .flink#article-container .friend-list-div \u003e .friend-div { position: relative; float: left; overflow: hidden; margin: 15px 7px; width: calc(100% / 3 - 15px); height: 90px; border-radius: 8px; line-height: 17px; -webkit-transform: translateZ(0) } @media screen and (max-width: 1100px) { .flink#article-container .friend-list-div \u003e .friend-div { width: calc(50% - 15px) !important } @media screen and (max-width: 600px) { .flink#article-container .friend-list-div \u003e .friend-div { width: calc(100% - 15px) !important } } } .flink#article-container .friend-list-div \u003e .friend-div:hover { background: rgba(87, 142, 224, 0.15); } .flink#article-container .friend-list-div \u003e .friend-div:hover img { -webkit-transform: rotate(360deg); -moz-transform: rotate(360deg); -o-transform: rotate(360deg); -ms-transform: rotate(360deg); transform: rotate(360deg) } .flink#article-container .friend-list-div \u003e .friend-div:before { position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: -1; background: var(--text-bg-hover); content: ''; -webkit-transition: -webkit-transform .3s ease-out; -moz-transition: -moz-transform .3s ease-out; -o-transition: -o-transform .3s ease-out; -ms-transition: -ms-transform .3s ease-out; transition: transform .3s ease-out; -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0) } .flink#article-container .friend-list-div \u003e .friend-div:hover:before, .flink#article-container .friend-list-div \u003e .friend-div:focus:before, .flink#article-container .friend-list-div \u003e .friend-div:active:before { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1) } .flink#article-container .friend-list-div \u003e .friend-div a { color: var(--font-color); text-decoration: none } .flink#article-container .friend-list-div \u003e .friend-div a img{ float: left; margin: 15px 10px; width: 60px; height: 60px; border-radius: 35px; -webkit-transition: all .3s; -moz-transition: all .3s; -o-transition: all .3s; -ms-transition: all .3s; transition: all .3s } .flink#article-container .friend-list-div \u003e .friend-div a .friend-name { display: block; padding: 16px 10px 0 0; height: 40px; font-weight: 700; font-size: 20px } .flink#article-container .friend-list-div \u003e .friend-div a .friend-info { display: block; padding: 1px 10px 1px 0; height: 50px; font-size: 13px } ","date":"2020-08-24","objectID":"/post/9e9c31ab/:0:0","tags":["Hugo","教程","个人经验"],"title":"Hugo 添加友情链接页面","uri":"/post/9e9c31ab/"},{"categories":null,"content":"那些人,那些事","date":"2020-08-21","objectID":"/friends/","tags":null,"title":"友情链接","uri":"/friends/"},{"categories":null,"content":"MakaL-0- MakaLoo’s Blog Shaka Rover Shaka Rover’s Blog 北故 北故博客 Bakumon 食梦兽博客 ","date":"2020-08-21","objectID":"/friends/:0:0","tags":null,"title":"友情链接","uri":"/friends/"},{"categories":null,"content":"如果你想和我交换友链 如果你想与我交换友链,请在下方评论。 我仅接受个人博客申请友链。 我的友链信息如下: 博客名: Reborn’s Blog 地址: https://reb.mallotec.com 头像: https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/android-chrome-192x192.png 描述: 一壶酒,一蓑衣,渔歌唱晚,了此一生 ","date":"2020-08-21","objectID":"/friends/:1:0","tags":null,"title":"友情链接","uri":"/friends/"},{"categories":["Hugo"],"content":" 参考: 把博客从 Hexo 迁移到 Hugo Hugo 永久链接 HUGO 为什么要迁移到Hugo? 因为 Hexo 太慢啦~ Node的依赖也乱七八糟~ 顺便想要简洁主题找不到合适自己的,要么太过简洁,想要的功能都没有;要么太过臃肿,阅读界面太乱;太难了~ 迁移需要注意的地方 ","date":"2020-08-19","objectID":"/post/0e6db571/:0:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"关于 Front-matter ","date":"2020-08-19","objectID":"/post/0e6db571/:1:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"时间格式 date Hexo 中时间格式与 Hugo 格式不太相同,Hugo 对时间格式的要求比较严格,Hexo 中YYYY-MM-DD HH:MM:SS格式的时间在 Hugo 中无法被正确识别。 Hugo 中使用的时间格式如下: YYYY-MM-DD HH:MM:SS +08:00 最后的+08:00代表中国的时区是 GMT+8。 另外,Hexo 中 Front Matter 代表博客更新时间的updated选项,在 Hugo 中是lastmod。 由于写的文章太多,我直接用正则表达式搜索替换: ([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))\\s([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]) ","date":"2020-08-19","objectID":"/post/0e6db571/:1:1","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"关于迁移旧链接 迁移博客过程中最重要的莫过于保证文章的 URL 不变,不然,这将会非常不利于 SEO。好不容易有一篇文章出现在 Google 相关搜索结果的前排,却因为 URL 的变化导致原链接 404 从而导致该文章从 Google 的索引中移除,这一定会是非常令人沮丧的😶。下面我们就来说说怎么解决这个问题~ Hugo 默认使用 “Pretty URLs”(漂亮URL), 而 Hexo 使用 “Ugly URLs”(丑陋URL) ├─content # 网站源代码目录 | ├─_index.md # URL转换为:https://example.com/ | ├─posts # 一个名为posts的章节文件夹 | | ├─_index.md # URL转换为:https://example.com/posts/ | | ├─first.md # URL转换为:https://example.com/posts/first/ | | └─topic # 章节内的子页面内容目录(包含md文件和图片) | | | ├─second.md # URL转换为:https://example.com/posts/topic/second/ | | | └─123.jpg # second.md文件引用的本地图片 “_index.md” 和 “index.md” 文件在 Hugo 中具有特殊含义(用于组织页面)。在 “Pretty URLs” 渲染下,与实际路径一致。 若文件名不为 “_index.md” 或 “index.md” 时,“Pretty URLs” 将 md 文件渲染为目录。 会导致后果:md 文件引用本地图片时,渲染路径比实际路径多了一个与文件名同名的目录名,导致引用图片失败。 而 “Ugly URLs” 则会直接渲染为 xxx.html,这会导致迁移后 Hexo 版本的链接会直接失效。若有搜索引擎收录了 Hexo 版本的链接,则会指向 404 页面。 这是我不能接受的,于是我翻了翻 Hugo 文档,终于找到了解决方法: 假设您在content/posts/my-awesome-blog-post.md中创建了一条新内容。内容是您以前的帖子的修订版,位于content/posts/my-original-url.md。您可以在新的my-awesome-blog-post.md的开头创建别名字段,在其中可以添加以前的路径。以下示例说明如何在 YAML 中创建此字段。(机翻勿吐槽🙈️……) content/posts/my-awesome-post.md aliases:- /posts/my-original-url/- /2010/01/01/even-earlier-url.html 当你输入aliases中任意一个路径时,他就会跳转到my-awesome-post.md所在的页面啦~ 给 Hugo 添加新功能 ","date":"2020-08-19","objectID":"/post/0e6db571/:2:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"永久链接 之前使用 Hexo 的时候,用的是hexo-abbrlink插件来处理永久链接。迁移到 Hugo 之后,发现没有这种插件,顿时就慌了,那可怎么办呀~ 搜索了一下发现还是有大佬利用 Hugo 自建函数实现了这个功能。 ","date":"2020-08-19","objectID":"/post/0e6db571/:3:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"永久链接生成方案 大佬的永久链接生成方案是直接对时间 + 文章名生成字符串做一下 md5 然后取任意 4-12 位。想了一下,这样的 hash 冲撞概率还是挺小的,我觉得可以! 那么接下来说说怎么把这个方案应用到 Hugo 中…… Hugo 在永久链接中支持一个参数:slug。简单来说,我们可以针对每一篇文章指定一个 slug,然后在 config.toml 中配置permalinks包含slug参数,就可以生成唯一的永久链接。我们的目的就是对每篇文章自动生成一个 slug。 修改archetypes/default.md添加如下一行: ---#...slug:{{substr (md5 (printf \"%s%s\" .Date (replace .TranslationBaseName \"-\" \" \" | title))) 4 8 }}#...--- 这样在每次使用hugo new的时候就会自动填写一个永久链接了。 之后修改config.toml添加如下行: [permalinks] posts = \"/post/:slug\" ","date":"2020-08-19","objectID":"/post/0e6db571/:3:1","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"DisqusJS 我现在用的是 LoveIt 主题,官方集成了 Disqus 评论模块,却没有集成 DisqusJS,于是决定自己动手~ 以 LoveIt 主题为例: 拷贝themes/LoveIt/layouts/partials/comment.html到博客根目录/layouts/partials/comment.html 在id=\"comments\"的 div 块里加入以下代码: \u003cdiv id=\"comments\"\u003e ... {{- /* DisqusJS Comment System */ -}} {{- $disqusjs := $comment.disqusjs | default dict -}} {{- if $disqusjs.enable -}} \u003c!-- jsDelivr --\u003e {{- $source := $cdn.disqusjsCSS | default \"lib/disqusjs/disqusjs.css\" -}} \u003clink rel=\"stylesheet\" href=\"{{- $source -}}\"\u003e {{- $source := $cdn.disqusjsJS | default \"lib/disqusjs/disqus.js\" -}} \u003c!-- jsDelivr --\u003e \u003cscript src=\"{{- $source -}}\"\u003e\u003c/script\u003e \u003cdiv id=\"disqus_thread\" class=\"comment\"\u003e\u003c/div\u003e \u003cscript\u003e var dsqjs = new DisqusJS({ shortname: {{- $disqusjs.shortname -}}, siteName: {{- $disqusjs.siteName -}}, api: {{- $disqusjs.api -}}, apikey: {{- $disqusjs.apikey -}}, admin: {{- $disqusjs.admin -}}, adminLabel: {{- $disqusjs.adminLabel -}} }); \u003c/script\u003e {{- end -}} ... \u003c/div\u003e 拷贝themes/LoveIt/assets/data/cdn/jsdelivr.yml到博客根目录/assets/data/cdn/jsdelivr.yml 在libFiles结点里添加以下代码: libFiles:...disqusjsCSS:[email protected]/dist/disqusjs.cssdisqusjsJS:[email protected]/dist/disqus.js 修改config.toml添加如下行: [params.page.comment] enable = true ... # DisqusJS 评论系统设置 [params.page.comment.disqusjs] # Reborn 新增 enable = true # Disqus 的 shortname,用来在文章中启用 Disqus 评论系统 shortname = \"Your shortname\" siteName = 'Your siteName' apikey = 'Your apikey' api = 'https://disqus.skk.moe/disqus/' admin = 'Your admin' adminLabel = 'admin' ... ","date":"2020-08-19","objectID":"/post/0e6db571/:4:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"PWA/Service Worker 懒得写了,网上一堆解释,直接贴源码算了: 在static/目录下新建sw.js文件,然后加入以下代码: importScripts('https://cdn.jsdelivr.net/npm/workbox-cdn/workbox/workbox-sw.js'); if (workbox) { console.log(`Yay! Workbox is loaded 🎉`); workbox.core.setCacheNameDetails({ prefix: \"Your Username\" }); workbox.core.skipWaiting(); workbox.core.clientsClaim(); workbox.precaching.precacheAndRoute([ { \"url\": \"/index.html\", \"revision\": \"MD5 of your index.html\" }, { \"url\": \"/css/style.min.css\", \"revision\": \"MD5 of your style.css\" } ], {}); workbox.routing.registerRoute(/(?:\\/)$/, new workbox.strategies.StaleWhileRevalidate({ cacheName: \"html\", plugins: [ new workbox.expiration.ExpirationPlugin({ maxAgeSeconds: 60 * 60 * 24 * 7, // purgeOnQuotaError: !0 }) ] }), \"GET\"); workbox.routing.registerRoute( /\\.(?:js|css)$/, new workbox.strategies.StaleWhileRevalidate({ cacheName: 'static-resources' }) ) workbox.routing.registerRoute( /\\.(?:png|jpg|jpeg|gif|bmp|webp|svg|ico)$/, new workbox.strategies.CacheFirst({ cacheName: \"images\", plugins: [new workbox.expiration.ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 7 * 24 * 60 * 60, // purgeOnQuotaError: !0 })] }), \"GET\"); // Fonts workbox.routing.registerRoute( /\\.(?:eot|ttf|woff|woff2)$/, new workbox.strategies.CacheFirst({ cacheName: \"fonts\", plugins: [ new workbox.expiration.ExpirationPlugin({ maxEntries: 1000, maxAgeSeconds: 60 * 60 * 24 * 30 }), new workbox.cacheableResponse.CacheableResponsePlugin({ statuses: [0, 200] }) ] }) ); workbox.routing.registerRoute( /^https:\\/\\/fonts\\.googleapis\\.com/, new workbox.strategies.StaleWhileRevalidate({ cacheName: 'google-fonts-stylesheets' }) ); workbox.routing.registerRoute( /^https:\\/\\/fonts\\.gstatic\\.com/, new workbox.strategies.CacheFirst({ cacheName: 'google-fonts-webfonts', plugins: [ new workbox.cacheableResponse.CacheableResponsePlugin({ statuses: [0, 200] }), new workbox.expiration.ExpirationPlugin({ maxAgeSeconds: 60 * 60 * 24 * 365, maxEntries: 30 }) ] }) ); // external resources workbox.routing.registerRoute( /(^https:\\/\\/cdn\\.jsdelivr\\.net.*?(\\.js|\\.css))|(^https:\\/\\/cdnjs\\.cloudflare\\.com)/, new workbox.strategies.CacheFirst({ cacheName: \"external-resources\", plugins: [ new workbox.expiration.ExpirationPlugin({ maxEntries: 1000, maxAgeSeconds: 60 * 60 * 24 * 30 }), new workbox.cacheableResponse.CacheableResponsePlugin({ statuses: [0, 200] }) ] }) ); workbox.googleAnalytics.initialize({}); } else { console.log(`Boo! Workbox didn't load 😬`) } 拷贝themes/LoveIt/layouts/_default/baseof.html到博客根目录/layouts/_default/baseof.html 在\u003c/body\u003e后加入以下代码: \u003cscript\u003e // 可以这么注册 Service Worker if ('serviceWorker' in navigator) { // 为了保证首屏渲染性能,可以在页面 load 完之后注册 Service Worker window.onload = function () { navigator.serviceWorker .register('/sw.js', { scope: '/' }) .then(function(registration) { console.log('Service Worker Registered'); }); navigator.serviceWorker .ready .then(function(registration) { console.log('Service Worker Ready'); }); }; } \u003c/script\u003e ","date":"2020-08-19","objectID":"/post/0e6db571/:5:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"添加清理旧版本策略 修改sw.js文件: ... ... workbox.core.setCacheNameDetails({ prefix: \"Your name\", suffix: 'v2', // 修改这里就可以更新了 precache: 'precache', runtime: 'runtime' }); // 跳过等待期 workbox.core.skipWaiting(); // 一旦激活就开始控制任何现有客户机(通常是与skipWaiting配合使用) workbox.core.clientsClaim(); // 删除过期缓存 workbox.precaching.cleanupOutdatedCaches(); ... ... // 安装阶段跳过等待,直接进入 active self.addEventListener('install', function (event) { event.waitUntil(self.skipWaiting()); }); // Call Activate Event to remove old cache self.addEventListener('activate', function (event) { event.waitUntil( Promise.all([ // 更新客户端 self.clients.claim(), // 清理旧版本 caches.keys().then(function (cacheList) { return Promise.all( cacheList.map(function (cacheName) { if (/(v\\d+)/.test(cacheName) === false || workbox.core.cacheNames.suffix !== RegExp.$1) { return caches.delete(cacheName); } }) ); }) ]) ); }); 然后确保每个new workbox.strategies.xxx的cacheName都是如下代码: new workbox.strategies.xxx({ cacheName: \"your-cache-name-\" + workbox.core.cacheNames.suffix, }) ","date":"2020-08-19","objectID":"/post/0e6db571/:5:1","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"Github Action 自动部署 Github Action 是个好东西,一定要好好利用,我已经写好一个 Hugo 部署到 github page 的 github action 了,直接用就好了~ 在博客源码的 Github 仓库项目中Settings—Secrets设置好GH_TOKEN、GIT_NAME、GIT_EMAIL 新建.github/workflows/deploy.yml,示例: name:自动部署 Hugoon:push:branches:- masterjobs:build:runs-on:ubuntu-lateststrategy:matrix:node-version:[10.x]steps:- name:开始运行uses:actions/checkout@v1- name:缓存uses:actions/cache@v1id:cache-dependencieswith:path:node_moduleskey:${{runner.OS}}-${{hashFiles('**/package-lock.json')}}- name:部署准备run:|git clone https://${{secrets.GH_TOKEN}}@github.com/username/username.github.io.git .deploy_git- name:部署博客uses:RebornQ/[email protected]:github_token:${{secrets.GH_TOKEN}}username:${{secrets.GIT_NAME}}email:${{secrets.GIT_EMAIL}}repo_url:https://${{secrets.GH_TOKEN}}@github.com/username/username.github.io.git ","date":"2020-08-19","objectID":"/post/0e6db571/:6:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Hugo"],"content":"音乐 shortcode 支持本地歌词文件 太简单了,看 APlayer 文档就能找出来,直接贴代码: 拷贝themes/LoveIt/layouts/shortcodes/music.html到博客根目录/layouts/shortcodes/music.html 修改如下代码: ... {{- if .Get \"url\" -}} ... {{- $lrc := .Get \"lrc\" -}} {{- with dict \"Path\" $lrc \"Resources\" .Page.Resources | partial \"function/resource.html\" -}} {{- $lrc = .RelPermalink -}} {{- end -}} \u003cmeting-js ... lrc=\"{{ $lrc }}\" ... \u003e\u003c/meting-js\u003e ... ","date":"2020-08-19","objectID":"/post/0e6db571/:7:0","tags":["Hugo","教程","个人经验"],"title":"Hexo 迁移到 Hugo 记录","uri":"/post/0e6db571/"},{"categories":["Android"],"content":"背景 项目历经9个月的演化,终于从一开始为宝可梦的那样记账研究的语言切换而写的Demo进化成第三方库。 这是我的第一个开源第三方库,说起来还挺激动的(嗯…失眠了…)~ 使用文档 一个用 Kotlin 写的多语言切换的 Android 第三方库。 目前只支持英文/简中/繁中三种语言,需要添加其他的语言可以提交 issue 或者直接联系我。 现在我们支持任何一种语言,但是需要你自己定义你要支持的语言列表(Key 与 Value 间的关系),详情请看👉注意事项。 ","date":"2019-12-08","objectID":"/post/7afcff16/:0:0","tags":["Android","第三方开源库"],"title":"里包恩第一款开源第三方库 plugin-locale 面世!!","uri":"/post/7afcff16/"},{"categories":["Android"],"content":"引入依赖 (可选)项目的 build.gradle 中加入: 当 JCenter 无法链接的时候可以尝试使用 allprojects { repositories { // ... maven { url \"https://dl.bintray.com/rebornq/maven/\" } } } app.gradle 中加入: implementation 'com.mallotec.reb:plugin-locale:{last-version}' 注意:{last-version}要替换为最新版本号,最新版本链接:https://bintray.com/rebornq/maven/plugin-locale/_latestVersion ","date":"2019-12-08","objectID":"/post/7afcff16/:1:0","tags":["Android","第三方开源库"],"title":"里包恩第一款开源第三方库 plugin-locale 面世!!","uri":"/post/7afcff16/"},{"categories":["Android"],"content":"只需三步即可食用 在 Application 中初始化 LocalePlugin.init(this) 或 LocalePlugin.init(this, { 刷新界面的方式 }) 其中{ 刷新界面的方式 }有三种: LocaleConstant.RESTART_TO_LAUNCHER_ACTIVITY: 清空栈中所有Activity并重启到LauncherActivity LocaleConstant.RECREATE_CURRENT_ACTIVITY: 重新创建当前Activity,默认是这种方式,可不填写。此方式可能会因为内存低无法注销广播接收器而导致内存泄漏,解决方法请查看👉常见问题或👉Wiki LocaleConstant.CUSTOM_WAY_TO_UPDATE_INTERFACE: 自定义刷新界面, 如果选这种方式的朋友请务必查看👉更多用法或👉Wiki 定义好支持的语言列表对应关系,详情请看👉注意事项,如: private fun getLocale(which : String): Locale { return when (which) { \"0\" -\u003e Locale.ROOT // 跟随系统 \"1\" -\u003e Locale.ENGLISH \"2\" -\u003e Locale.SIMPLIFIED_CHINESE \"3\" -\u003e Locale.TRADITIONAL_CHINESE else -\u003e Locale.SIMPLIFIED_CHINESE } } 一句代码调用切换语言: // 应用切换的语言 LocaleHelper.getInstance() .language(getLocale(which.toString())) .apply(this) 若{ 刷新界面的方式 }选择了第一种LocaleConstant.RESTART_TO_LAUNCHER_ACTIVITY,请使用下面的方式: // 应用切换的语言 val intent = Intent(this, TargetActivity::class.java) LocaleHelper.getInstance() .language(getLocale(which.toString())) .apply(this, intent) 若{ 刷新界面的方式 }选择了第三种LocaleConstant.CUSTOM_WAY_TO_UPDATE_INTERFACE,请使用下面的方式: 上面有写,查看👉更多用法或👉Wiki 注意:这里的this必须是当前Activity的Context;which是所选的语言标记,详情请看下方注意事项的对应关系 Demo 效果图: MultiLanguageDemo-NoRestartToLaunche\" MultiLanguageDemo-NoRestartToLaunche 更多请查看本项目的 Demo 常见问题 ","date":"2019-12-08","objectID":"/post/7afcff16/:2:0","tags":["Android","第三方开源库"],"title":"里包恩第一款开源第三方库 plugin-locale 面世!!","uri":"/post/7afcff16/"},{"categories":["Android"],"content":"切换语言失效原因及解决方法 AndroidX AppCompat 库 1.1.0-alpha03 以上版本导致的 Locale 被一个新的 Configuration 对象覆盖掉 仅写出解决方法,本demo经测试无法复现问题 在Activity中加入以下代码即可: // 防止 Locale 被一个新的 Configuration 对象覆盖掉(AndroidX AppCompat 库 1.1.0-alpha03 以上版本) override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) { overrideConfiguration?.setLocale(LocaleHelper.getInstance().getSetLocale()) super.applyOverrideConfiguration(overrideConfiguration) } ","date":"2019-12-08","objectID":"/post/7afcff16/:3:0","tags":["Android","第三方开源库"],"title":"里包恩第一款开源第三方库 plugin-locale 面世!!","uri":"/post/7afcff16/"},{"categories":["Android"],"content":"内存泄漏原因及解决方法 Activity 对象被回收时还没来得及执行 onDestroy() 方法导致没注销对应的广播接收器引发的内存泄漏 这种情况常见于内存低的时候Activity被强制回收,不走onDestroy()方法导致的。 遇到这种情况的朋友请自行判断内存低即将回收Activity的地方(onTrimMemory()?), 然后手动调用以下代码注销广播接收器: BroadcastReceiverManager.unregisterBroadcastReceiver(activity) 写在最后 欢迎大家 Star、Fork 和提 Issue 提 PR 呀~ 项目地址:https://github.com/RebornQ/Plugin-Locale-Kotlin Thanks 以下不分排名先后 Thanks @MichaelJokAr. 感谢 @MichaelJokAr 的教程——Android国际化(多语言)实现,支持8.0 Thanks @Bakumon. 感谢 @宝可梦 的指点 Thanks @JessYan. 感谢 @JessYan 的教程——我一行代码都不写实现Toolbar!你却还在封装BaseActivity? Thanks @Yaroslav Berezanskyi. 感谢 @Yaroslav Berezanskyi 的教程——How to change the language on Android at runtime and don’t go mad ","date":"2019-12-08","objectID":"/post/7afcff16/:4:0","tags":["Android","第三方开源库"],"title":"里包恩第一款开源第三方库 plugin-locale 面世!!","uri":"/post/7afcff16/"},{"categories":["复习","校招","数据库"],"content":"数据库 ","date":"2019-08-12","objectID":"/post/4d2729e2/:0:0","tags":["校招","秋招","学习","复习","数据库"],"title":"8. 数据库 校招复习(持续更新~)","uri":"/post/4d2729e2/"},{"categories":["复习","校招","数据库"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/4d2729e2/:1:0","tags":["校招","秋招","学习","复习","数据库"],"title":"8. 数据库 校招复习(持续更新~)","uri":"/post/4d2729e2/"},{"categories":["复习","校招","数据库"],"content":"三大范式(2020卓动春招面试真题) ","date":"2019-08-12","objectID":"/post/4d2729e2/:1:1","tags":["校招","秋招","学习","复习","数据库"],"title":"8. 数据库 校招复习(持续更新~)","uri":"/post/4d2729e2/"},{"categories":["复习","校招","设计模式"],"content":"设计模式 ","date":"2019-08-12","objectID":"/post/b05a5a01/:0:0","tags":["校招","秋招","学习","复习","设计模式"],"title":"7. 设计模式 校招复习(持续更新~)","uri":"/post/b05a5a01/"},{"categories":["复习","校招","设计模式"],"content":"建造者模式(Builder Pattern) 建造者模式(Builder Pattern)- 最易懂的设计模式解析 ","date":"2019-08-12","objectID":"/post/b05a5a01/:1:0","tags":["校招","秋招","学习","复习","设计模式"],"title":"7. 设计模式 校招复习(持续更新~)","uri":"/post/b05a5a01/"},{"categories":["复习","校招","设计模式"],"content":"适配器模式(Adapter Pattern) 适配器模式(Adapter Pattern)- 最易懂的设计模式解析 ","date":"2019-08-12","objectID":"/post/b05a5a01/:2:0","tags":["校招","秋招","学习","复习","设计模式"],"title":"7. 设计模式 校招复习(持续更新~)","uri":"/post/b05a5a01/"},{"categories":["复习","校招","Android"],"content":"SharePreference Android中最简单的数据存储方式:SharedPreferences——食梦兽 Android:这是一份全面 \u0026 详细的SharePreferences学习指南 ","date":"2019-08-12","objectID":"/post/7afd20c5/:0:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"简介 定义:一种数据存储方式 本质:以键值对的形式存储在 xml 中 特点:轻量级 应用场景:轻量级存储(如 应用中的配置、参数属性 等) 默认存储位置:/data/data/\u003cPackageName\u003e/shared_prefs ","date":"2019-08-12","objectID":"/post/7afd20c5/:1:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"实现原理 ","date":"2019-08-12","objectID":"/post/7afd20c5/:2:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/7afd20c5/:3:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"SharePreference 支持多进程吗?不支持要如何实现?讲讲实现原理?(2020网易校招笔试原题) Android 常用架构模式 面试总结 | 记一次Android 面试 ","date":"2019-08-12","objectID":"/post/7afd20c5/:3:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"MVP ","date":"2019-08-12","objectID":"/post/7afd20c5/:4:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"优缺点及优化 ","date":"2019-08-12","objectID":"/post/7afd20c5/:4:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"MVVM 是让人耳目一新的 Jetpack MVVM 精讲啊! - 源码:https://github.com/KunMinX/Jetpack-MVVM-Best-Practice 如何构建Android MVVM 应用框架——美团技术团队 《安卓-深入浅出MVVM教程》应用篇-01Hello MVVM (快速入门) - 源码(已废弃):https://github.com/ittianyu/MVVM - 源码:https://github.com/ittianyu/relight 使用Android架构组件开发MVVM模式的应用 ","date":"2019-08-12","objectID":"/post/7afd20c5/:5:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"优缺点及优化 其他 ","date":"2019-08-12","objectID":"/post/7afd20c5/:5:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/7afd20c5/:6:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"Android双开原理(卓动2020秋招笔试真题) 「原创」Android 应用多开对抗实践 深度分析:揭秘一个手机登陆多个微信的原理,看完你也会了! Android 虚拟多开系列二——技术原理 Android应用双开实现 Android容器和虚拟化:从应用多开谈起 一行代码帮你检测Android多开软件 Android虚拟化引擎VirtualApp探究 Android虚拟化研究(PDF) 应用多开技术总结 多开技术方案 应用实例 修改APK 克隆大师 修改FrameWork(Android多用户机制) 小米应用分身、360奇酷手机等 通过虚拟化技术实现(Hook) 360分身大师、LBE平行空间、太极、VirtualApp等 以插件机制运行 DroidPlugin 修改APK 反编译APK,修改APK 包名、签名,将APK伪装成另外一个不同的APK,但对于一些有加密的APK,可能没办法实现。 修改FrameWork 对于有系统修改权限的厂商,可以修改 Framework 来实现双开的目的。 通过虚拟化技术实现(Hook) 虚拟 FrameWork 层、虚拟文件系统、模拟 Android 对组件的管理、虚拟应用进程管理等一整套虚拟技术,将APK复制一份到虚拟空间中运行。 以插件机制运行 可以在无需安装、修改的情况下运行APK文件,利用反射替换,动态代理,hook了系统的大部分与system—server进程通讯的函数,以此作为“欺上瞒下”的目的,欺骗系统“以为”只有一个apk在运行,瞒过插件让其“认为”自己已经安装。 系统级技术 多开技术方案 发行版App Android多用户功能 OEM系统自带的“应用分身”、“应用双开”和“Island/练妖壶”等各种 Android for work产品 chroot 暂无发现App 多用户功能: 多用户模式主要用到UserManager相关类,切换不同的用户,在不同用户下运行App,实现多开。 但这种简单粗暴的方法会将Android的服务都运行多一份,如果只应用于应用多开,资源实在浪费。 如 MX4-Pro 中的访客用户模式,缺点在于成本较高,需要一个支持多账户的手机才可以,而国内ROM一般都把这个功能从表面上“阉割”了。此外,双开时还需要频繁切换系统账户,且不能同时收消息,极为不便。更加遗憾的是除分身外,基本没有其他功能。 在 Android 5.0 在基于多用户功能添加了 Android for work 功能,可以在同一个桌面启动器下使用受限用户启动 APP,不再需要切换界面。同时将权限开发给了非系统应用。 chroot: 需要 root 权限,在本地挂载精简版系统镜像,然后远程进去。目前没有App实现,可能ARM服务器云手机中用到。 用户级技术 ","date":"2019-08-12","objectID":"/post/7afd20c5/:6:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇五・其他","uri":"/post/7afd20c5/"},{"categories":["复习","校招","Android"],"content":"View ","date":"2019-08-12","objectID":"/post/24eff910/:0:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇四・View","uri":"/post/24eff910/"},{"categories":["复习","校招","Android"],"content":"自定义 View 流程(原理) Android:一篇文章带你完全梳理自定义View工作流程! Android自定义View基础:ViewRoot、DecorView \u0026 Window的简介 (1)自定义View基础 - 最易懂的自定义View原理系列 Android自定义View绘制前的准备:DecorView创建 \u0026 显示 (2)自定义View Measure过程 - 最易懂的自定义View原理系列 (3)自定义View Layout过程 - 最易懂的自定义View原理系列 (4)自定义View Draw过程- 最易懂的自定义View原理系列 手把手教你写一个完整的自定义View ","date":"2019-08-12","objectID":"/post/24eff910/:1:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇四・View","uri":"/post/24eff910/"},{"categories":["复习","校招","Android"],"content":"自定义 View 的应用 Canvas类的最全面详解 - 自定义View应用系列 Path类的最全面详解 - 自定义View应用系列 ","date":"2019-08-12","objectID":"/post/24eff910/:2:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇四・View","uri":"/post/24eff910/"},{"categories":["复习","校招","Android"],"content":"Android四大组件 ","date":"2019-08-12","objectID":"/post/e52fe569/:0:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Activity(活动) 重学安卓:Activity 的快乐你不懂! ","date":"2019-08-12","objectID":"/post/e52fe569/:1:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"启动模式 Activity的四种启动模式应用场景 android launchmode(四种启动模式)应用场景及实例 Android的Activity的启动模式、任务栈以及使用场景? 我打赌你一定没搞明白的Activity启动模式 彻底明白Activity启动模式-SingleTop、SingleTask、SingleInstance具体使用场景 Android基础:最易懂的Activity启动模式详解 standard 默认启动模式,不会判断启动的 activity 在栈中是否存在,每次启动都会创建一个新的实例 singleTop 栈顶复用模式,会判断启动的 activity 是否位于栈顶。若是则直接使用栈顶实例(调用 onNewIntent()方法),若否则创建新实例位于栈顶 应用场景 适合从外界可能多次跳转到一个界面的情况。 消息推送 接收通知启动的内容显示页面。例如:某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的(这个例子解释好像有点问题???) 登录页面 登录成功跳转到主页,按下两次登录按钮,生成了两个主页。一些有启动延迟的页面(往往是动画,网络造成)也会有这样的情况。 singleTask 如果希望 activity 在整个应用程序中只存在一个实例,可以用 singleTask。 栈内单例模式,系统会判断启动的 activity 是否有实例存在栈中。如果存在则直接使用该实例,并将该 activity 之上的所有 activity 出栈;如果不存在则创建一个新的实例。 之前打开过的页面,打开之前的页面就ok,不再新建。 应用场景 适合作为程序模块逻辑入口,即程序入口点。 主页面(Home) 例如浏览器的主界面,不管从多少个应用启动浏览器,只会启动一次主界面,其余情况都走onNewIntent(),并且会清空主界面上面的其他页面。 假设用户在主页跳转到其他页面,运行多次操作后想返回到主页,假设不使用 singleTask 模式,在点击返回的过程中会多次看到主页,这明显就是设计不合理了。 singleInstance 堆内单例模式/单一实例模式,整个操作系统里只有一个内存实例存在。不同的应用去打开这个 activity 共享公用的同一个 activity。他会运行在自己单独、独立的任务栈里面,并且任务栈里只有他一个实例存在。 如果在启动这样的 activity 时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。 应用场景 适合需要与程序分离开的页面。 闹铃提醒,将闹铃提醒与闹铃设置分离 来电显示 呼叫来电界面 系统Launcher 注意事项 singleInstance 不要用于中间页面,如果用于中间页面,跳转会有问题(stackoverflow上有人说过)。 比如A -\u003e B(singleInstance) -\u003e C,完全退出后,再次启动,首先打开的还是B。 例:某个应用中用到了 google 地图,当退出该应用的时候,进入 google 地图,还是刚才的界面。 总结 LaunchMode 场景 standard mainfest 中没有配置就默认标准模式 singleTop 登录页面、消息推送 singleTask 程序模块逻辑入口:主页面、扫一扫页面 电商中:购物界面,确认订单界面,付款界面 singleInstance 闹铃提醒、来电显示、呼叫来电界面、系统Launcher ","date":"2019-08-12","objectID":"/post/e52fe569/:1:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"启动流程 ","date":"2019-08-12","objectID":"/post/e52fe569/:1:2","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"生命周期 Activity 生命周期\" Activity 生命周期 ","date":"2019-08-12","objectID":"/post/e52fe569/:1:3","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Service(服务) Android 服务两种启动方式的区别——食梦兽 Android 四大组件:一份全面 \u0026 简洁的 Service 知识讲解攻略 Android:Service生命周期 完全解析 Android:(本地、可通信的、前台、远程)Service使用全面介绍 Android多线程:这是一份全面 \u0026 详细的IntentService源码分析指南 Android:远程服务Service(含AIDL \u0026 IPC讲解) ","date":"2019-08-12","objectID":"/post/e52fe569/:2:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Start方式开启服务 生命周期 onCreate() –\u003e onStartCommand() –\u003e onDestroy() 说明:如果服务已经开启,不会重复的执行onCreate(),而是会调用onStartCommand() 特点 使用步骤 定义一个类继承Service 在Manifest中配置该Service 使用Context.startService(Intent)方法启动该服务 不再使用时,调用stopService(Intent)方法停止该服务 ","date":"2019-08-12","objectID":"/post/e52fe569/:2:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Bind方式开启服务 生命周期 onCreate() –\u003e onBind() –\u003e onUnbind() –\u003e onDestroy() 注意:绑定服务不回调用onStartCommand()方法。 使用步骤 定义一个类继承Service 在Manifest中配置该Service 使用Context.bindService(Intent, ServiceConnection, int)方法启动该服务 不再使用时,调用unbindService(ServiceConnection)停止该服务 ","date":"2019-08-12","objectID":"/post/e52fe569/:2:2","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"两种启动方式的区别 Start方式:服务一旦开启就跟调用者没有任何关系了 Bind方式:调用者挂了,服务也会挂 ","date":"2019-08-12","objectID":"/post/e52fe569/:2:3","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Broadcast Receiver(广播接收器) Android 两种注册、发送广播的区别——食梦兽 Android四大组件:BroadcastReceiver史上最全面解析 ","date":"2019-08-12","objectID":"/post/e52fe569/:3:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"注册广播 在 Android 中,如果想要接收到广播信息,必须自定义我们的广播接收器(新建类继承BroadcastReceiver,并重写其onReceive()方法,实现接收到特定广播所要做的事情)。 定义好广播接收器后,要想使用它接收广播,就要注册这个广播接收器。 注册广播接收器有两种方法:静态注册和动态注册,下面会详解~ 静态注册(在Manifest中) 直接在声明文件Manifest.xml的\u003capplication\u003e节点中配置广播接收器。 动态注册(在代码中) 使用Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)方法注册广播。 两种注册广播的方式有什么不同? 静态注册:常驻型广播,不受应用生命周期影响。 也就是说当应用程序关闭后,如果有信息广播来,程序依旧会被系统调用。 适用场景:需要时刻监听广播。 动态注册:非常驻型广播,跟随组件生命周期的变化。 即组件结束 = 广播结束,在组件结束前必须移除广播接收器。 适用场景:需要特定时刻监听广播。 特别注意 动态广播最好在Activity的onResume()注册、onPause()注销。 原因: 对于动态广播,有注册就必须要有注销,否则会导致内存泄漏。 重复注册、重复注销也不允许。 在onResume()注册、onPause()注销是因为onPause()在 App 死亡前一定会被执行,从而保证广播在 App 死亡前一定会被注销,从而防止内存泄露。 ","date":"2019-08-12","objectID":"/post/e52fe569/:3:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"发送广播 无序广播 所有的接收者都会接收事件,不可以被拦截,不可以被修改。 普通广播 系统广播 有序广播 按照优先级,一级一级的向下传递,接收者可以修改广播数据,也可以终止广播事件。 粘性广播 App应用内广播 ","date":"2019-08-12","objectID":"/post/e52fe569/:3:2","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"常见面试题 为什么动态广播接收器在 Activity 被回收前没有手动注销会导致内存泄漏? ","date":"2019-08-12","objectID":"/post/e52fe569/:3:3","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Content Provider(内容提供器) ","date":"2019-08-12","objectID":"/post/e52fe569/:4:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"Intent Android:关于 Intent的那些小事(介绍、使用方法等) 面试总结 | 记一次Android 面试 Android理解:显式和隐式Intent ","date":"2019-08-12","objectID":"/post/e52fe569/:5:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"常见面试题 Android 隐式和显式 Intent 的区别?(2020卓动笔试原题) 显示Intent:直接指定需要打开的Activity类,意图特别明确,所以是显示的。 设置这个类的方式可以是Class对象(如SecondActivity.class),也可以是包名+类名的字符串。 应用内部Activity跳转常用这个方式。 隐式Intent:隐式不明确指定启动哪个Activity,而是设置Action、Data、Category,让系统来筛选出合适的Activity。 筛选是根据Manifest中所有的\u003cintent-filter\u003e来筛选。 ","date":"2019-08-12","objectID":"/post/e52fe569/:5:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇三・四大组件","uri":"/post/e52fe569/"},{"categories":["复习","校招","Android"],"content":"事件分发 Android事件分发机制详解:史上最全面、最易懂 重学安卓:Activity 的快乐你不懂! 重学安卓:学习 View 事件分发,就像外地人上了黑车! ","date":"2019-08-12","objectID":"/post/c302b7c8/:0:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"工作原理 事实上,View 事件分发的本质是递归。 递流程的方向遵循以下流程: View 事件分发流程-递流程\" View 事件分发流程-递流程 归流程的方向遵循以下流程: View 事件分发流程-归流程\" View 事件分发流程-归流程 每次完整的事件分发流程,都包含自上而下的“递”,与自下而上的“归”2个流程。 每次完整的事件分发流程,都是针对一个事件(MotionEvent)完成的递归,而一个事件只对应着一个Action,例如:ACTION_DOWN。 一次用户触摸操作,我们称之为一个事件序列。一个事件序列会包含ACTION_DOWN、ACTION_MOVE…ACTION_MOVE、ACTION_UP等多个事件。 也即一个事件序列,包含从ACTION_DOWN到ACTION_UP的多次事件分发流程。 ","date":"2019-08-12","objectID":"/post/c302b7c8/:1:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"3个重要方法 事先分发包含 3 个重要方法: dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent ","date":"2019-08-12","objectID":"/post/c302b7c8/:1:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"大致流程 递流程 dispatchTouchEvent(ev) 在“递”的过程中,主要通过dispatchTouchEvent方法进行事件的向下分发。 因此“递”流程可以改进为: View 事件分发流程-递流程Advanced\" View 事件分发流程-递流程Advanced 该方法主要执行以下操作(分两种情况): 如果 child 是 ViewGroup,那么实际执行的就是 ViewGroup 重写的 dispatchTouchEvent 方法。该方法内可以判断,是否在当前层级拦截当前事件、或者继续把事件下发给下一级。 如果 child 是不再有 child 的 View 或 ViewGroup,那么实际执行的就是 View 类实现的 super.dispatchTouchEvent 方法。该方法内可以判断,如果 View.enabled == true 且实现了 onTouchListener 且 onTouch() 返回 true,那么不执行 onTouchEvent( ),并直接返回 true 表示事件已被消费,然后步入“归”流程;否则执行 onTouchEvent( )。 流程如下: View 事件分发流程-View#dispatchTouchEvent\" View 事件分发流程-View#dispatchTouchEvent 相关源码(仅贴出关键代码): public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null \u0026\u0026 (mViewFlags \u0026 ENABLED_MASK) == ENABLED \u0026\u0026 mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); } 从流程图或源码可以看出: 若手动复写在onTouch()中返回true(即将事件消费掉,将不会再执行onTouchEvent() 若一个控件不可点击(即View.enabled == false),那么给它注册的onTouch()事件将永远得不到执行。因为这是逻辑与的判断,而判断 View 是否可点击在执行onTouch()事件之前。 onTouchEvent(ev) onTouchEvent 方法的流程如下: View 事件分发流程-View#onTouchEvent\" View 事件分发流程-View#onTouchEvent 分析:在onTouchEvent()中,如果View.clickabled == true并且实现了onClickListener或onLongClickListener,就会执行onClick()或onLongClick()。 onInterceptTouchEvent(ev) 事实上,在“递”的流程中,ViewGroup 可以在当前层级通过设置onInterceptTouchEvent()方法返回true,来拦截事件的下发,然后直接步入“归”流程。 注意: 该拦截方法只存在于 ViewGroup,普通的 View 无该方法 如果某一层级的 ViewGroup 拦截了某个事件,那么后续的这一事件序列都会默认拦截,不再调用此方法。 即onInterceptTouchEvent()方法只走一次,一旦走过,就会留下记号(mFirstTouchTarget == null),那么下一次直接根据这个记号来判断拦不拦截。 相关源码(仅贴出关键代码): public boolean dispatchTouchEvent(MotionEvent ev) { ... if (disallowIntercept || !onInterceptTouchEvent(ev)) { ... } ... } 那么有同学就好奇了,disallowIntercept又是什么东西啊? 正所谓“上有政策,下有对策”。在ViewGroup可以拦截事件下发的同时,child也可以通过getParent.requestDisallowInterceptTouchEvent()方法来改变disallowIntercept的值,从而阻止上一级的下发拦截(即关闭拦截功能,使得原本被拦截的事件继续下发)。 说明:disallowIntercept = 是否禁用事件拦截功能(默认值 false) 归流程 总之,递流程走到没有 child 的层级,就意味着步入“归”流程。 如果该层级的super.dispatchTouchEvent(ev)没有返回true,那么将继续执行上一级的super.dispatchTouchEvent(ev),直到被某一级消费为止(也即返回true为止)。 这就是“归”流程。 总结 综上,整个大致流程如下图: View 事件分发流程-递归全流程\" View 事件分发流程-递归全流程 ","date":"2019-08-12","objectID":"/post/c302b7c8/:1:2","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"应用场景 ","date":"2019-08-12","objectID":"/post/c302b7c8/:2:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"滑动冲突解决 冲突场景 要点 外部拦截法 重写onInterceptTouchEvent(),根据冲突场景的规则来判断是否拦截。 内部拦截法 重写子 View 的dispatchTouchEvent() ,然后调用parent.requestDisallowInterceptTouchEvent(true)禁止父容器拦截事件,全部交给子 View 处理。 ","date":"2019-08-12","objectID":"/post/c302b7c8/:2:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"额外知识 ","date":"2019-08-12","objectID":"/post/c302b7c8/:3:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"onTouch()和onTouchEvent()的区别? 2个方法都是在View.dispatchTouchEvent()中调用,但onTouch() 优先于 onTouchEvent() 执行 若手动复写在onTouch()中返回true(即将事件消费掉,将不会再执行onTouchEvent() ","date":"2019-08-12","objectID":"/post/c302b7c8/:3:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇二・事件分发","uri":"/post/c302b7c8/"},{"categories":["复习","校招","Android"],"content":"Handler Android Handler:这是一份 全面、详细的Handler机制 学习攻略 Android Handler:图文解析 Handler通信机制 的工作原理 Android Handler:手把手带你深入分析 Handler机制源码 面试字节跳动Android研发岗,已拿到offer,这些知识点该放出来了 ","date":"2019-08-12","objectID":"/post/e7dd5054/:0:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"工作原理 ","date":"2019-08-12","objectID":"/post/e7dd5054/:1:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"概念 要了解原理首先我们要知道几个概念: Handler:处理器 定义1:主线程与子线程的通信媒介 定义2:线程消息的主要处理者 作用:负责将消息Message发送到消息队列MessageQueue以及处理循环器Looper分派过来的消息 MessageQueue:消息队列 定义:一种先进先出的数据结构 作用:用于存储Handler发过来的消息Message Looper:循环器 定义:消息队列MessageQueue与处理器Handler的通信媒介 作用:用于消息循环 消息获取:循环取出消息队列MessageQueue里的消息Message 消息分发:将取出的消息Message发送给对应的处理器Handler 备注: 每个线程只能拥有1个Looper 1个Looper可绑定多个线程的Handler(即多个线程可往1个Looper所拥有的MessageQueue中发送消息,提供了线程间通信的可能) ","date":"2019-08-12","objectID":"/post/e7dd5054/:1:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"工作流程 Handler工作流程\" Handler工作流程 注意: 主线程的Looper对象自动调用ActivityThread的静态方法main()中的Looper.prepareMainLooper()生成,同时会生成对应的MessageQueue对象;而子线程的Looper对象则需要手动通过Looper.prepare()创建 在子线程若不手动创建Looper对象,则无法生成Handler对象。 在创建Handler对象时,则通过构造方法自动关联当前线程的Looper对象 \u0026 对应的消息队列对象MessagerQueue,从而自动绑定了实现创建Handler对象操作的线程。 在Looper消息分发dispatchMessage(msg)时,会进行1次发送方式的判断(具体看源码): 若msg.callback属性不为空,则代表使用了post(Runnable r)发送消息,则回调Runnable对象里复写的run() 若msg.callback属性为空,则代表使用了sendMessage(Message msg)发送消息,则回调复写的handleMessage(msg) ","date":"2019-08-12","objectID":"/post/e7dd5054/:1:2","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"常见面试问题 ","date":"2019-08-12","objectID":"/post/e7dd5054/:2:0","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"为什么Looper不断循环去处理消息却不会导致主线程卡死(ANR)?(2020蘑菇街面试真题) 系统每 16ms 会发送一个刷新 UI 消息唤醒。 Looper.loop()的死循环里面有两行代码: Message msg = queue.next; msg.target.dispatchMessage(msg); 这两行代码的作用就是从 消息队列MessageQueue 中不断地取出消息,然后把消息分发出去,给 Handler 处理,所以不会造成ANR。 因为 Android 是由事件驱动的,Looper.loop()不断地接收事件、处理事件,每一个点击触摸甚至是 Activity 的生命周期都是运行在Looper.loop()的控制下,如果他停止了,应用也随之停止了。 只能是某一个消息或者说对消息的处理阻塞了Looper.loop(),而不是Looper.loop()阻塞了它。 ","date":"2019-08-12","objectID":"/post/e7dd5054/:2:1","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"消息队列MessasgeQueue是一个什么样的结构?(2020蘑菇街面试真题) 存储特性为先进先出的数据结构,也就是队列 ","date":"2019-08-12","objectID":"/post/e7dd5054/:2:2","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"Handler/线程、Looper和MessageQueue的关系?(2020网易校招笔试原题) Looper:负责从消息队列中拿消息,然后分发给线程中的 Handler 处理 MessageQueue:负责存储 Handler 发过来的消息 Handler:负责发消息到 MessageQueue 以及处理 Looper 分发过来的消息 ","date":"2019-08-12","objectID":"/post/e7dd5054/:2:3","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Android"],"content":"Handler的post消息如何被执行?(2020网易校招笔试原题) Handler工作流程-Handler.post\" Handler工作流程-Handler.post ","date":"2019-08-12","objectID":"/post/e7dd5054/:2:4","tags":["校招","秋招","学习","复习","Android"],"title":"6. Android 校招复习篇一・Handler","uri":"/post/e7dd5054/"},{"categories":["复习","校招","Java"],"content":"多线程 Android多线程:你必须要了解的多线程基础知识汇总 Android:关于多线程的总结知识都在这里了! HashMap Java:这是一份全面 \u0026 详细的HashMap 1.7源码分析指南 Java源码分析:关于 HashMap 1.8 的重大更新 Java源码分析:HashMap 1.8 相对于1.7 到底更新了什么? ","date":"2019-08-12","objectID":"/post/b3fd4b52/:0:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"实现原理 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:1:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"Hash冲突/碰撞解决方法 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:2:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:3:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"HashMap和ConcurrentHashMap区别?(2020蘑菇街面试真题) 线程安全? HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你! 漫画:什么是ConcurrentHashMap? ","date":"2019-08-12","objectID":"/post/b3fd4b52/:3:1","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"HashMap的Value(还是Key?)能不能为null?(2020蘑菇街面试真题) Key: 源码中的hash(Object key)函数里有一句 return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u003e\u003e\u003e 16); 可以看出,当key == null时,hashCode为0,而不是抛出异常,所以是key是可以为null的 Value: HashMap源码中并没有对value做限制,所以是value是可以为null的 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:3:2","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"ArrayMap 的优势? String 相关 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:3:3","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:4:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"String、StringBuffer 和 StringBuilder 的区别和应用场景 Java中String、StringBuffer、StringBuilder的区别以及使用场景总结 String,StringBuffer, StringBuilder 的区别是什么?String为什么是不可变的? 详解String、StringBuffer和StringBuilder的区别和应用场景 String、StringBuffer、StringBuilder的区别及应用场景 区别 是否可以改变? String是字符串常量,由String创建的字符内容是不可改变的。我们对字符串进行拼接或重新赋值,是在字符串池中创建了新的字符串,原来那个字符串的值并没有改变。 而StringBuffer和StringBuilder是字符串变量,由StringBuffer和StringBuilder创建的字符内容是可以改变的。而且在字符串拼接的情况下,不会产生临时的字符串。 StringBuffer是线程安全的,而StringBuilder是非线程安全。StringBuilder是从 JDK5 开始,为StringBuffer补充的一个单线程的等价类。我们应该在使用时优先考虑 StringBuilder,因为它支持StringBuffer的所有操作,但是因为它不执行同步,不会有线程安全带来的额外系统消耗,所以速度更快。 实际上StringBuilder和StringBuffer的方法是完全等价的,只是StringBuffer的方法加了sychronized描述。 场景 如果不常去改变String的值,不进行许多字符串拼接等操作,就比较适合使用String,因为String是不可变。 如果在一个单线程中,有许多字符串拼接操作,使用StringBuilder就可以满足了,并且它性能更好。 如果在多线程中,要考虑到线程安全问题,就只能用StringBuffer ","date":"2019-08-12","objectID":"/post/b3fd4b52/:4:1","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"字符串拼接后地址比较(卓动2020秋招笔试真题) 写出以下3个方法的返回值: public static boolean testV1() { String a = \"a1\"; String b = \"a\" + 1; return a == b; } public static boolean testV2() { String a = \"ab\"; String bb = \"b\"; String ab = \"a\" + bb; return a == ab; } public static boolean testV3() { String a = \"ab\"; String bb = \"b\"; String ab = \"a\" + \"b\"; return a == ab; } true false true List相关 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:4:2","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"ArrayList和LinkedList区别?(从实现原理、性能对比阐述) ","date":"2019-08-12","objectID":"/post/b3fd4b52/:4:3","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"List迭代时移除元素造成的异常(卓动2020秋招笔试真题) Java ConcurrentModificationException异常原因和解决方法 java.util.ConcurrentModificationException详解 java.util.ConcurrentModificationException异常详解 下面这段代码执行结果是什么?如有异常,请根据此段程序执行的意图给出相应的解决方案并说明原由。 public static void testV1() { ArrayList\u003cInteger\u003e ids = new ArrayList\u003c\u003e(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); for (Integer id : ids) { if (id == 5) { ids.remove(5); } System.out.println(id); } } foreach 循环背后的实现原理其实就是 Iterator,所以我们不妨把上面的代码转换一下以便后面分析: ArrayList\u003cInteger\u003e ids = new ArrayList\u003c\u003e(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); Iterator\u003cInteger\u003e iterator = ids.iterator(); while (iterator.hasNext()) { Integer id = iterator.next(); if (id == 5) { ids.remove(5); } System.out.println(id); } 执行结果: 0 1 2 3 4 5 Exception in thread \"main\" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at test.ListTest.testV1(ListTest.java:18) at test.ListTest.main(ListTest.java:12) 异常原由 当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。 从前面的异常中我们可以知道,报错原因是在java.util.ArrayList$Itr.checkForComodification()这个方法里。 我们先不忙看这个方法的源码,我们先根据程序的代码一步一步看 ArrayList 源码的实现。 查首先看 ArrayList 的iterator()方法的具体实现,看源码发现在 ArrayList 的源码中并没有iterator()这个方法,那么很显然这个方法应该是其父类或者实现的接口中的方法,我们在其父类 AbstractList 中找到了iterator()方法的具体实现: public Iterator\u003cE\u003e iterator() { return new Itr(); } 从这段代码可以看出返回的是一个指向 Itr 类型对象的引用,我们接着看 Itr 的具体实现,在 AbstractList 类中找到了 Itr 类的具体实现,它是 AbstractList 的一个成员内部类,下面这段代码是 Itr 类的所有实现: private class Itr implements Iterator\u003cE\u003e { int cursor = 0; int lastRet = -1; int expectedModCount = modCount; public boolean hasNext() { return cursor != size(); } public E next() { checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet \u003c 0) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet \u003c cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 这里不分析整个 Itr 类,针对本题,我们只需要知道以下几个关键信息: 执行next()方法的时候,会先调用一次checkForComodification()进行检查; 当modCount不等于expectedModCount的时候,就会抛出ConcurrentModificationException异常。 那么modCount和expectedModCount,又是什么呢?下面是科普时间~ 变量名 描述 modCount 当前集合修改次数;ArrayList 的父类 AbstarctList 中有一个成员变量modCount,每次对集合进行修改(增删元素)时都会modCount++。 expectedModCount 期望的集合修改次数;迭代 ArrayList 的 Iterator(即内部类Itr) 中有一个变量 expectedModCount,该变量会初始化和modCount相等。 那么我们现在来看一下什么时候modCount不等于expectedModCount。 既然这里是执行 ArrayList 的remove(),那我们不妨先去看看remove()方法做了什么: public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved \u003e 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } 通过 remove 方法删除元素首先对 modCount 进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将 size 进行减1操作,并将引用置为 null 以方便垃圾收集器进行回收工作。 modCount++就是此案元凶,我们来分析一下删除元素前后的各个成员变量的值: 删除元素前: 变量名 变量值 Iterator#expectedModCount 0 List#modCount 0 删除元素后: 变量名 变量值 Iterator#expectedModCount 0 List#modCount 1 而删除元素后,程序又执行了next()遍历下一个元素;而执行next()方法的时候,又会先调用一次checkForComodification()进行检查;在checkForComodification()方法中,当modCount不等于expectedModCount的时候,就会抛出ConcurrentModificationException异常。 笔/面试简述 说了那么多,要是笔试的时候写那么长不是早就到时间了?这里给一个简述给大家参考一下: 原由是当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。 在 ArrayList 迭代器的next()方法中,首先会调用一次checkForComodification()方法对 modCount 和 expectedModCount 进行检查。 modCount 是当前的集合修改次数, expectedModCount 是期望的集合修改次数,当 modCount 不等于 expectedModCount 的时候,checkForComodification()方法就会抛出ConcurrentModificationE","date":"2019-08-12","objectID":"/post/b3fd4b52/:4:4","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:5:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"用Java实现对一个文件夹内所有文件包括子文件夹的删除(卓动2020秋招笔试真题) 思路: 先判断节点存不存在 然后判断节点是不是文件夹,若是文件夹,则递归遍历该目录下的所有子结点(文件或目录) 递归后删除结点。 /** * 题目:用Java实现对一个文件夹内所有文件包括子文件夹的删除 * * @param dir 将要删除的文件目录 * @return 删除状态 */ public static boolean deleteAllFilesInDir(File dir) { if (!dir.exists()) { throw new RuntimeException(\"目录或文件不存在!\"); } if (dir.isDirectory()) { for (File child : Objects.requireNonNull(dir.listFiles())) { deleteAllFilesInDir(child); } } // 此时已经没有文件了,该目录可以删除 return dir.delete(); } JVM ","date":"2019-08-12","objectID":"/post/b3fd4b52/:5:1","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"GC算法(垃圾回收机制) JVM(2)–一文读懂垃圾回收 JVM:这是一份全面 \u0026 详细的 (GC)垃圾收集算法 讲解攻略 JVM:关于那些 常见的垃圾收集器 学习指南 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:6:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"内存泄漏和内存溢出 面试总结 | 记一次Android 面试 Android性能优化:关于 内存泄露 的知识都在这里了! ","date":"2019-08-12","objectID":"/post/b3fd4b52/:7:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"内存溢出(OOM) 内存溢出是指应用在申请内存的时候,没有足够的内存可以分配,导致Out Of Memory错误,也就是 OOM。 注意:OOM 会导致应用 Crash。 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:7:1","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"内存泄漏 内存泄漏是指在对象被垃圾回收和释放时,如果得不到及时的释放,就会一直占用内存,造成内存泄漏。 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:7:2","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"区别 看上面定义 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:7:3","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"什么时候会发生?举例场景 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:7:4","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"Java 类加载机制 (JVM)Java虚拟机:类加载的5个过程 (JVM)Java虚拟机:(双亲委派模型)类加载器全解析 (JVM)Java虚拟机:静态分派 \u0026 动态分派 原理解析 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:8:0","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","Java"],"content":"双亲委派模型以及意义 ","date":"2019-08-12","objectID":"/post/b3fd4b52/:8:1","tags":["校招","秋招","学习","复习","Java"],"title":"5. Java 校招复习(持续更新~)","uri":"/post/b3fd4b52/"},{"categories":["复习","校招","计算机网络"],"content":"TCP 这样理解,你也能在 30 秒内讲明白 TCP 三次握手 TCP协议的常见面试题 关于三次握手与四次挥手面试官想考我们什么?— 不看后悔系列 图解TCP协议中的三次握手和四次挥手 TCP三次握手四次挥手详解 通俗大白话来理解TCP协议的三次握手和四次分手 网络协议-TCP、UDP区别及TCP三次握手、四次挥手 TCP 协议简介——阮一峰的网络日志 TCP/IP协议栈与数据包封装 TCP/IP 知识点整理 TCP/IP协议详解 TCP/IP 数据包报文格式(IP包、TCP报头、UDP报头) ","date":"2019-08-12","objectID":"/post/f0a8d55a/:0:0","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"TCP 数据段结构 TCP 数据包结构\" TCP 数据包结构 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:1:0","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"序列号、确认号以及标志位含义 序列号 seq (Sequence Numbers):用来标识从 TCP 源端向目的端发送的字节流,发起方发送数据时对此进行标记。 确认号 Ack (Acknowledge Number):在接收端,用来通知发送端数据成功接收;其数值等于发送方的序列号 seq + 1(即接收方期望的下一个序列号); 一般用法为:接收到的上一次远端主机传来的 seq 然后 +1,再发送给远端主机,提示远端主机已经成功接收上一次所有数据。 标志位: ACK: 确认序列号有效,确认值 ACK 为 1 便是确认连接 SYN: 创建一个连接 FIN: 终结/关闭一个连接 一般 确认号(Acknowledgement Number) 用小写Ack表示,标志位的 确认值(Acknowledgement) 用大写ACK表示。 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:1:1","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:2:0","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"TCP 三次握手描述? 三分钟基础知识:用动画给面试官解释 TCP 三次握手过程 关于TCP协议中三次握手中的ACK和Ack number的区别 TCP三次握手中SYN,ACK,Seq三者的关系 TCP三次握手图解\" TCP三次握手图解 每个状态的含义: CLOSED —— 没有任何连接状态; LISTEN —— 侦听来自远方 TCP 端口的连接请求; SYN_SEND —— 在发送连接请求后等待匹配的连接请求; SYN_RCVD —— 在收到和发送一个连接请求后等待对连接请求的确认; ESTABLISHED —— 代表一个打开的连接,数据可以进行传输。 为什么 TCP 握手要三次,一、两次不行吗? 主要是为了防止已失效的连接请求报文段突然又传到了服务器端,因而产生错误。 三次握手其实就是一个双方都确认自己与对方接收与发送能力是否正常的过程。 第一次握手:客户端发送网络包,服务端接收到了。此时服务端就可以知道:客户端发送能力正常、服务端的接收能力正常。 第二次握手:服务端发包,客户端接收到了。此时客户端就可以知道:客户端自己的接收和发送能力正常、服务端的接收和发送能力正常。但是这个时候服务端并不能确定客户端的接收能力是否正常,因此我们还要进行第三次握手。 第三次握手:客户端发包,服务端接收到了。此时服务端就可以确认:客户端的接收和发送能力正常、服务端自己的接收和发送能力也正常。 因此,需要三次握手才能确认双方的接收与发送能力是否正常。 现假定一种异常情况:Client发出的第一个连接请求报文并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放后的某个时间才到达Server。 本来这是一个早已失效的报文段,但Server收到此失效的连接请求报文段后,就误以为是Client又发出一次新的连接请求,于是就向Client发出确认报文段,同意建立连接。 假定不采用第三次报文握手,那么只要Server发出确认报文,新的连接就建立了。 由于现在Client并没有发出建立连接的请求,因此不会理睬Server的确认,也不会向Server发送数据,但Server却以为新的连接已经建立了,并一直等待Client发来的数据。Server的很多资源就这样白白浪费了。 采用三次握手,可以防止上述现象的发生。例如在刚才的异常情况下,Client不会向Server的确认发出确认,Server由于收不到确认,就知道Client并没有要求建立连接,于是Server就不会再建立连接。 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:2:1","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"TCP 四次挥手描述? 三分钟基础知识:用动画给女朋友讲解 TCP 四次分手过程 TCP四次挥手图解\" TCP四次挥手图解 每个状态的含义: ESTABLISHED —— 代表一个打开的连接,数据可以进行传输。 FIN_WAIT_1 —— 等待远程 TCP 的连接中断请求,或先前的连接中断请求的确认; FIN_WAIT_2 —— 从远程 TCP 等待连接中断请求; CLOSE_WAIT —— 等待从本地用户发来的中断请求; LAST_ACK —— 等待原来发向 TCP 的连接中断请求的确认; TIME_WAIT —— 等待足够的时间以确保远程 TCP 接收到连接中断请求的确认; CLOSED —— 没有任何连接状态; 其他标识的含义: MSL —— 指一个片段在网络中最大的存活时间; 2MSL —— 一个发送和一个回复所需的最大时间,两倍的MSL(Maximum Segment Lifetime)。 为什么 TCP 握手是三次,挥手却要四次? 因为(握手时)当 Server 端收到 Client 端发来的SYN报文后,是可以直接发送SYN + ACK报文的(其中SYN报文是用来同步的,ACK报文是用来应答的)。 但是挥手(即关闭连接)时,当 Server 端收到 Client 端发来的FIN报文通知时,它仅仅表示对方没有数据发给 Server端 了,并不代表 Server 端已经把所有数据都全部发送给 Client 端。 所以 Server 很可能并不会立即关闭Socket,只能先回复一个ACK报文告诉 Client 端:“你发的FIN报文我收到了”。只有等我 Server 端所有报文都发完了,我才能发送FIN报文告诉 Client 我现在可以关闭连接了。 因此FIN和ACK不能一起发送,故需要四次挥手。 为什么 TCP 挥手 TIME_WAIT 状态要有 2 MSL 等待延迟? 为了保证发送最后一个 ACK 报文段能够到达服务端。 场景 对应这样一种情况: 假定网络是不可靠的,导致最后客户端发送ACK = 1给服务端的过程中丢失了,服务端没收到,服务端怎么认为的?“我已经发完数据了,怎么客户端没回应我?是不是中途丢失了?”。然后服务端再次发起断开连接的请求FIN = 1, ACK = 1,一个来回就是2MSL。 所以**TIME_WAIT状态就是用来重发可能丢失的 ACK 报文**。 一些说明 关于2MSL的说明: 客户端给服务端发送的ACK = 1丢失,服务端等待1MSL没收到,然后重新发送消息FIN = 1, ACK = 1需要1MSL。 如果客户端再次接收到服务端的消息,则重启2MSL计时器,发送确认请求。 客户端只需等待2MSL,如果没有再次收到服务端的消息,客户端就推断服务端已经接收到自己确认消息,然后结束 TCP 连接。 防止已失效的连接请求报文段出现在本连接中。 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:2:2","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"TCP 和 UDP 的区别? TCP面向连接,UDP是无连接的(即发送数据前不需要建立连接); TCP提供可靠传输,保证数据准确性,UDP可能会丢包;TCP保证数据顺序,UDP不保证; TCP是面向字节流,而UDP是面向报文的(流模式和数据包模式) 网络分层模型 你真的懂网络分层模型吗? 面试官最爱问你的,网络分层中每一层有哪些内容 网络7层协议,4层,5层?理清容易混淆的几个概念 OSI七层模型、五层模型和TCP/IP四层模型的关系如下: 网络分层模型关系图\" 网络分层模型关系图 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:2:3","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"三种模型分析 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:3:0","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"OSI 七层模型 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:3:1","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"网络的五层划分(五层模型) ","date":"2019-08-12","objectID":"/post/f0a8d55a/:3:2","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"TCP/IP 四层模型 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:3:3","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"网络分层模型常见面试题 ","date":"2019-08-12","objectID":"/post/f0a8d55a/:4:0","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","计算机网络"],"content":"ARP 协议属于哪一层? ","date":"2019-08-12","objectID":"/post/f0a8d55a/:4:1","tags":["校招","秋招","学习","复习","计算机网络"],"title":"4. 计算机网络 校招复习(持续更新~)","uri":"/post/f0a8d55a/"},{"categories":["复习","校招","操作系统"],"content":"生产者消费者算法 信号量机制 ","date":"2019-08-12","objectID":"/post/739f7ebe/:0:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"PV 操作 死锁 经典生活例子:独木桥 死锁知识总结 | 理想村 | 屠杭镝的博客 资源图与死锁定理的灵活运用 银行家算法与Java实现 死锁 银行家算法 『现代操作系统』死锁 操作系统原理-死锁 银行家算法 Java面试必问-死锁终极篇 ","date":"2019-08-12","objectID":"/post/739f7ebe/:1:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"死锁的概念 ","date":"2019-08-12","objectID":"/post/739f7ebe/:2:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"什么是死锁? ","date":"2019-08-12","objectID":"/post/739f7ebe/:2:1","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"产生死锁的必要条件 互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。 如独木桥就是一种独占资源,两方的人不能同时过桥。 不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。 如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。 占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是它在等待新资源时,仍继续占用已占有的资源。 以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源);但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的情况。 循环等待条件。存在一个进程等待序列{P1, P2, …, Pn},其中 P1 等待 P2 所占有的某一些资源,P2 等待 P3 所占有的某一些资源,…,而 Pn 等待 P1 所占有的某一资源,形成一个进程循环等待环。 就像前面的过独木桥问题,甲等待乙占有的桥面,而乙有等待甲占有的桥面,从而彼此循环等待。 ","date":"2019-08-12","objectID":"/post/739f7ebe/:2:2","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"死锁的预防 死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。 打破互斥条件。即允许进程同时访问某些资源。 有些资源是不允许被同时访问的,如打印机等等,这是由资源本身的属性所决定的。 所以,这种方法并无实用价值。 打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。 就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再申请;它所释放的资源可以分配给其他进程;这就相当于该进程占有的资源被隐蔽地抢占了。 这种预防死锁的方法实现起来困难,会降低系统性能。 打破占有且申请条件。可以实行资源预先分配策略。 资源预先分配策略: 即进程运行前一次性地向系统申请它所需要的全部资源。 打破循环等待条件。实行资源有序分配策略。 资源有序分配策略: 采用这种策略,即把资源事先分类编号,按号分配,使进程在申请、占用资源时不会形成环路。 所有进程对资源的请求必须严格按资源序号递增的顺序提出;进程占用了小号资源,才能申请大号资源,就不会形成环路,从而预防了死锁。 ","date":"2019-08-12","objectID":"/post/739f7ebe/:3:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"如何避免?(预防和避免不是一个玩意吗?) ","date":"2019-08-12","objectID":"/post/739f7ebe/:4:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"安全序列 ","date":"2019-08-12","objectID":"/post/739f7ebe/:4:1","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"银行家算法 ","date":"2019-08-12","objectID":"/post/739f7ebe/:4:2","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"死锁的检测与解除 Java面试宝典导读 第8章 第5节 处理调度与死锁 ","date":"2019-08-12","objectID":"/post/739f7ebe/:5:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"死锁的检测 ","date":"2019-08-12","objectID":"/post/739f7ebe/:5:1","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"死锁的恢复(解除) 一旦在死锁检测时发现了死锁,就要消除死锁,使系统从死锁状态中恢复过来。 资源剥夺法。 挂起某些死锁进程,并抢占它的资源,将这些资源分配给其它的死锁进程。 但应防止被挂起的进程长时间得不到资源时,而处于资源匮乏的状态。 撤销进程法。 强制撤销一个或一部分死锁进程并剥夺这些进程的资源。 撤销的原则可以按照进程优先级和撤销进程代价的高低进行。 进程退回法。 让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。 要求系统保持进程的历史信息,设置还原点。 ","date":"2019-08-12","objectID":"/post/739f7ebe/:5:2","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","操作系统"],"content":"举例场景? 4 个生活场景详解 BAT 面试中的死锁问题 Java 多线程实现死锁场景 突发奇想-怎么写一个死锁? 什么情况下Java程序会产生死锁?如何定位、修复? Java 实例 - 死锁及解决方法 ","date":"2019-08-12","objectID":"/post/739f7ebe/:6:0","tags":["校招","秋招","学习","复习","操作系统"],"title":"3. 操作系统 校招复习(持续更新~)","uri":"/post/739f7ebe/"},{"categories":["复习","校招","算法"],"content":"算法 程序员必须掌握的核心算法有哪些? ","date":"2019-08-12","objectID":"/post/2b453e2f/:0:0","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"算法复杂度的分析 ","date":"2019-08-12","objectID":"/post/2b453e2f/:1:0","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"时间复杂度 算法分析神器—时间复杂度 ","date":"2019-08-12","objectID":"/post/2b453e2f/:1:1","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"排序 ","date":"2019-08-12","objectID":"/post/2b453e2f/:2:0","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"术语铺垫 1. 稳定排序 如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。 2. 非稳定排序 如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。 ","date":"2019-08-12","objectID":"/post/2b453e2f/:2:1","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"十大经典排序 十大经典排序算法(动图演示) 这或许是东半球讲十大排序算法最好的一篇文章 十大经典排序算法动画,看我就够了! 必学十大经典排序算法,看这篇就够了(附完整代码动图优质文章) 必学十大经典排序算法,看这篇就够了(附完整代码/动图/优质文章)(修订版) 简单排序 前言 两个 for 循环分别有什么作用? 外层:控制排序轮数 内层:控制每一轮里的每一个比较步骤 冒泡排序(必学) 核心思想 对相邻两个元素进行比较,如果左侧元素比右侧大/小,则交换元素,否则不交换;每一轮会将最小/大的元素“浮”到顶端,最终达到完全有序。 源码实现 private int[] bubbleSort(int[] arr) { // 外n-1 内n-i-1 // 在第n-1轮的时候,只剩下一个元素没有位于有序区,但此时所有元素已经有序,所以总共遍历n-1轮 for (int i = 0; i \u003c arr.length - 1; i++) { // 内层用于遍历无序区 // 每轮排序中:需要比较的元素个数比上一轮少一个 // 即外层遍历i轮,无序区元素减少i个,所以内层应遍历n-i轮; // 又因为j+1可以遍历到最后一个元素,所以只需要遍历n-i-1轮(否则会数组越界),或者看成(n-1)-i来理解 for (int j = 0; j \u003c arr.length - i - 1; j++) { if (arr[j] \u003e arr[j + 1]) { swap(arr, j, j + 1); } } } return arr; } 算法复杂度 时间复杂度:O( n^2 ) 优化 冒泡排序有个问题,是不管你是否原本就有序,都会进行循环比较。排序问题若原本就有序的元素序列就不需要排序了嘛~ 针对这个问题,我们可以设一个临时变量来标记这个数组是否已经有序(在第一次循环的时候进行检查),如果有序就不用再遍历了。 private int[] bubbleSort(int[] arr) { for (int i = 0; i \u003c arr.length - 1; i++) { boolean isSorted = true; for (int j = 0; j \u003c arr.length - i - 1; j++) { if (arr[j] \u003e arr[j + 1]) { swap(arr, j, j + 1); isSorted = false; // 若没有进判断条件则说明没有进行元素互换,也即数组已经有序,后面不需要继续遍历,isSorted值为true。 } } if (isSorted) { break; } } return arr; } 选择排序(必学) 核心思想 遍历元素集合,每次找(选择)最大/最小元素拎出来与无序区的第一个位置交换,如此循环,直到整个集合排序完成。 源码实现 private int[] selectSort(int[] arr) { for (int i = 0; i \u003c arr.length; i++) { int min = i; // 由于无序区的第一个元素一定会被比较,所以直接i+1开始即可 for (int j = i + 1; j \u003c arr.length; j++) { min = minIndex(arr, min, j); } swap(arr, i, min); } return arr; } public int minIndex(int[] arr, int min, int j) { return arr[min] \u003c arr[j] ? min : j; } 算法复杂度 时间复杂度:O( n^2 ) 直接插入排序(必学) 直接插入排序 核心思想 跟打扑克牌时对扑克牌排序的道理是一样的。 随机选取序列里的一个元素作为待插元素(一般是无序区的第一个),与有序区的最后一个元素开始往前做比较; 直接插入排序后移元素示例1\" 直接插入排序后移元素示例1 待插元素与第一个元素比较时,若比它大,则有序区的那个元素后移一位,为待插元素腾出空间,然后继续与下一个元素比较。如此反复,直到遇到不比它大的元素为止。如下图: 直接插入排序后移元素示例2\" 直接插入排序后移元素示例2 然后在这个不比它大的元素的右边插入元素; 直接插入排序后移元素示例3\" 直接插入排序后移元素示例3 重复步骤,直到序列有序。 源码实现 private int[] insertSort(int[] arr) { // 遍历无序区,默认第一个元素有序,从 1 开始 for (int i = 1; i \u003c arr.length; i++) { int sortingVal = arr[i]; // 遍历有序区(0~i-1),找到插入位置;移动有序区,为插入元素挪出一个单元 int j = 0; for (j = i - 1; j \u003e= 0; j--) { // 待排序元素与有序区的每一个元素进行比较,如果遇到小于有序区的元素,则有序区的那个元素之后的所有元素后移一位,往前插入元素 if (sortingVal \u003c arr[j]) arr[j+1] = arr[j]; else break; } // 插入元素,因为循环最后 j-- 了,所以插入位置是 j+1 arr[j+1] = sortingVal; } return arr; } 算法复杂度 时间复杂度:O( n^2 ) 对于 n 个元素,首先我的外层 for 循环要循环 n-1 次 内层 for 循环的循环次数是根据 i 来决定的,i = 1时,循环 1 次,i = 2,循环 2 次,…,i = n-1,循环 n-1 次,那总共加起来就是 直接插入排序时间复杂度计算\" 直接插入排序时间复杂度计算 根据复杂度计算规则,保留高阶项,并去掉系数,那么时间复杂度为O( n^2 ) 分治排序 【漫画】不要再问我快速排序了 归并排序(必学) 核心思想 源码实现 算法复杂度 快速排序(必学) 核心思想 快排的核心思想和归并排序一样也是分治法,分而治之。 它的实现方式是每次从元素集合中选出一个基准值(也有叫中轴元素的),其他元素依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,注意,此时基准值所处的位置是有序的;然后(通过递归的方式)再对左边和右边的元素集合分别选出一个基准值,进行同样的比较移动;重复步骤,直到最后都变成单个元素,整个集合就成了有序的序列了。 快速排序流程示例\" 快速排序流程示例 源码实现 通用代码: private int[] quickSort(int[] arr, int left, int right) { // 递归结束条件 left \u003e= right if (left \u003c right) { // 得到基准元素位置 int pivotIndex = randomPartition(arr, left, right); // 根据基准元素,分成两部分进行递归排序 quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex + 1, right); } return arr; } 单边扫描法 public int partition(int[] arr, int left, int right) { // 设定基准值,由于前面已经随机并且交换到序列头部,因此此处只需要选取头部位置即可 int pivotIndex = left; int mark = left; // 设置一个mark指针指向数列起始位置,表示 小于基准元素的区域边界 // 从基准元素的下一个位置开始遍历数组 for (int i = pivotIndex + 1; i \u003c= right; i++) { if (arr[i] \u003c arr[pivotIndex]) { // 遍历到的元素小于基准元素,需做两件事 mark++; // 1.mark指针右移一位,因为小于基准元素的区域边界增大了1 swap(arr, mark, i); // 2.让最新遍历到的元素和mark指针所在位置的元素交换位置,因为最新遍历的元素要归属于小于pivot的区域 } } // 最后把pivot交换到mark指针所在位置 swap(arr, pivotIndex, mark); return mark; } 双边扫描法 private int partitionV2(int[] arr, int left, int right) { // 设定基准值,由于前面已经随机并且交换到序列头部,因此此处只需要选取头部位置即可 int pivotIndex = left; int start = pivotIndex + 1; // 因为基准值元素是有序的,所以不需要比较 int end = right; while (true) { // 从左往右扫描,找到第一个大于等于 基准值(pivot) 的元素位置 while (start \u003c= end \u0026\u0026 arr[start] \u003c= arr[pivotIndex]) start++; // 从右往左扫描,找到第一个小于等于 基准值(pivot) 的元素位置 while (start \u003c= end \u0026\u0026 arr[end] \u003e= arr[pivotIndex]) end--; // 左右指针相遇 if (start \u003e= end) break; // 交换左右数据,使得左边的元素不大于pivot,右边的不小于pivot swap(arr, start, end)","date":"2019-08-12","objectID":"/post/2b453e2f/:2:2","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"遍历/查找 常用查找算法 查找算法总结及其算法实现(Python/Java) 查找算法 ","date":"2019-08-12","objectID":"/post/2b453e2f/:3:0","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"二分法/分治法/二分查找 二分查找算法(递归与非递归两种方式) 二分查找法的递归和非递归的实现 程序员,你应该知道的二分查找算法 二分查找(折半查找)算法(原理、实现及时间复杂度) 二分查找法的实现和应用汇总 二分查找(面试必备) ","date":"2019-08-12","objectID":"/post/2b453e2f/:3:1","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"递归 为什么你学不会递归?告别递归,谈谈我的一些经验 ","date":"2019-08-12","objectID":"/post/2b453e2f/:3:2","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"其他 哪种Map遍历方法更优?!—Map遍历方法的正确选择 ","date":"2019-08-12","objectID":"/post/2b453e2f/:3:3","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"字符串匹配 算法之「字符串匹配算法」 算法之字符串模式匹配 字符串匹配算法 几种常见的字符串匹配算法 字符串匹配算法KMP详细解释——深入理解 字符串匹配算法总结 (分析及Java实现) 【轮子已造好】来了,字符串匹配算法 如何简单理解字符串匹配算法? Java数据结构之字符串模式匹配算法—Brute-Force算法 Java数据结构之字符串模式匹配算法—KMP算法 [数据结构拾遗]子字符串匹配常用算法总结 Java实现Sunday百万级数据量的字符串快速匹配算法 ","date":"2019-08-12","objectID":"/post/2b453e2f/:4:0","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"朴素算法 Brute-Force ","date":"2019-08-12","objectID":"/post/2b453e2f/:4:1","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"KMP 算法 ","date":"2019-08-12","objectID":"/post/2b453e2f/:4:2","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"位运算 关于位运算看这个就够了 位操作基础篇之位操作全面总结 面试题:位操作实现四则运算 Java 位运算(移位、位与、或、异或、非) java的逻辑运算符和位运算符 ","date":"2019-08-12","objectID":"/post/2b453e2f/:5:0","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"简介 运算符 含义 例子 注意事项 « 左移 0011 « 1 =\u003e 0110 左移1位相当于乘以2,每移动一位数值增倍 » 右移 0110 » 1 =\u003e 0011 右移1位相当于除以2,每移动一位数值减半 »\u003e 0填充的右移 (无符号右移) 1. 8 »\u003e 2 =\u003e 22. -5 »\u003e 3 =\u003e 5368709113. -14 »\u003e 2 ==\u003e 1073741820 Java中int类型占32位; 正数右移,高位用0补; 负数右移,高位用1补; 当负数使用无符号右移时,用0进行部位 (自然而然的,就由负数变成了正数了) \u0026 按位与 1. false \u0026 true =\u003e false2. true \u0026 true =\u003e true3. false \u0026 false =\u003e false4. 0010 \u0026 1001 =\u003e 0000 对于 a \u0026 b, 当 a 为 false 时, 仍需要判断 b 是否为 false | 按位或 1. false | true =\u003e true2. true | true =\u003e true3. false | false =\u003e false4. 0010 | 1001 =\u003e 1011 对于 a | b, 当 a 为 true 时, 仍然需要判断 b 是否为 true ^ 按位异或 1. false ^ true =\u003e false2. true ^ true =\u003e false3. false ^ false =\u003e true4. 0010 ^ 1001 =\u003e 0100 相同为 false,不同为true (相同为0不同为1) ~ 按位取反 0010 =\u003e 1101 ","date":"2019-08-12","objectID":"/post/2b453e2f/:5:1","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","算法"],"content":"常见面试题 假定 0 \u003c x \u003c 65535,那么计算 x*16 最好的方式是?(卓动2020秋招笔试真题) 位运算,x « 4(2^4 =16),可能需要判断一下边界 为什么 x 要小于 65535? 65535 = 2 ^ 16 - 1,同时 16 位系统,每个整数都是用 16 位 2 进制数来表示的,最大的数就是 16 个 1,所以 int 最大值就是 65535。 ","date":"2019-08-12","objectID":"/post/2b453e2f/:5:2","tags":["校招","秋招","学习","复习","算法"],"title":"2. 算法 校招复习(持续更新~)","uri":"/post/2b453e2f/"},{"categories":["复习","校招","数据结构"],"content":"链表 ","date":"2019-08-12","objectID":"/post/dce274cc/:0:0","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/dce274cc/:1:0","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"如何判断链表有环? 漫画算法:如何判断链表有环? 双指针? 队列 ","date":"2019-08-12","objectID":"/post/dce274cc/:1:1","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/dce274cc/:2:0","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"当循环队列满队退了一个队后,如何获取该队列的index? % 取模(取余),第几个元素 % 队列长度 树 ","date":"2019-08-12","objectID":"/post/dce274cc/:2:1","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"二叉树 ","date":"2019-08-12","objectID":"/post/dce274cc/:3:0","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"常见面试题 还原二叉树 还原二叉树–已知先序中序或者后序中序 做题方法: 找到根结点,确定左子树,确定右子树(最重要) 如果给出前序遍历,那么前序遍历的第一个元素就是根结点 对左子树进行递归分析 对右子树进行递归分析 已知先序、中序遍历,求后序遍历 例一 先序遍历: GDAFEMHZ 中序遍历: ADEFGHMZ 思路分析: 根据先序遍历的特点——根左右(中左右都可以),第一个元素一定是根结点,所以立即确定G是根结点。 确定了G是根结点后我们就可以去确定左右子树了。根据中序遍历的特点——左根右,可以得知G结点左边的元素ADEF就是左子树,右边的HMZ就是右子树。 接着分别分析左右子树,思路和前面两步一样,把他们当作独立的二叉树来分析。 把左子树的元素(ADEF)在先序遍历和中序遍历中的顺序拿出来进行比较。 先序:DAFE(根左右) 中序:ADEF(左根右) 通过先序特点我们可以确定D是左子树的根结点,根据中序特点确定左边有唯一一个A在D的左边,A就是D的左叶子,右边是EF。 同理,观察EF的相对位置,在先序(根左右)是FE,在中序是EF,得知F是D的右子树根结点(D的右叶子),E是F的左叶子 得出左子树的形状: 左子树的形状\" 左子树的形状 接着分析右子树,把右子树的元素(HMZ)在先序遍历和中序遍历中的顺序拿出来进行比较。 先序:MHZ(根左右) 中序:HMZ(左根右) 通过先序特点我们可以确定M是右子树的根结点,根据中序特点确定H是左叶子,Z是右叶子 得出右子树的形状: 右子树的形状\" 右子树的形状 得出整棵树的形状 整棵树的形状\" 整棵树的形状 例二 先序遍历: f b a c d e g h 中序遍历: a b d c e f g h 答案:a d c b e h g f a d e c b h g f 例二整棵树的形状\" 例二整棵树的形状 已知中序和后序遍历,求前序遍历 中序遍历: ADEFGHMZ 后序遍历: AEFDHZMG 思路分析: (做题方法是一样的) 已知前序、后序遍历,求中序遍历 这种情况可能会无法还原出一个唯一的二叉树,因为无法唯一确定根结点的左右子树 ","date":"2019-08-12","objectID":"/post/dce274cc/:3:1","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"红黑树 30张图带你彻底理解红黑树 ","date":"2019-08-12","objectID":"/post/dce274cc/:4:0","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"二叉查找树 ","date":"2019-08-12","objectID":"/post/dce274cc/:4:1","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"完美平衡二叉树 ","date":"2019-08-12","objectID":"/post/dce274cc/:4:2","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"红黑树 特性 红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质: // // // // 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。 ","date":"2019-08-12","objectID":"/post/dce274cc/:4:3","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["复习","校招","数据结构"],"content":"常见面试题 ","date":"2019-08-12","objectID":"/post/dce274cc/:4:4","tags":["校招","秋招","学习","复习","数据结构"],"title":"1. 数据结构 校招复习(持续更新~)","uri":"/post/dce274cc/"},{"categories":["Android"],"content":"前言 前面发了一篇“Android多语言切换实现——Java实现”,但是那个方案有缺陷,于是今天想写个更完美的方案。 原理 原理是一样的,这里不多说,看前面那篇文章的原理部分 新的尝试 我又去搜了一波,看了下Activity的生命周期、Application方法的执行顺序等等… ","date":"2019-03-24","objectID":"/post/e836c9c6/:0:0","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"主要思路 App本地保存所设置的语言(通过数据库 or SharePreferences); 每个页面及App类的生命周期中判断当时语言是否是所设置语言,如果不是,则更新Configuration信息; 在Application的attachBaseContext设置当前设置的语言Locale 在Application的onConfigurationChanged(改变系统语言以及横竖屏切换时会调用到)设置当前的语言Locale 在Activity的attachBaseContext设置当前设置的语言Locale,所以一般这里是创建BaseActivity来方便统一改变 在Service的attachBaseContext设置当前设置的语言Locale(如果有 Service 的话) 在Fragment的….暂时没找到,Fragment不知道是不是跟随父Activity改变语言的经过测试的确是跟着Activity改变的 ","date":"2019-03-24","objectID":"/post/e836c9c6/:1:0","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"实现 ","date":"2019-03-24","objectID":"/post/e836c9c6/:2:0","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"App本地保存所设置的语言 本例使用SharePreferences保存设置的语言,所以这里先创建一个SharePreferences的工具类(SharePrefUtils.kt): import android.content.Context import android.content.SharedPreferences import java.util.HashMap class SharePrefUtils { private var pref: SharedPreferences? = null private val map = HashMap\u003cString, Any\u003e() val all: Map\u003cString, *\u003e get() = pref!!.all private fun setPref(pref: SharedPreferences) { map.clear() this.pref = pref } fun getString(key: String): String? { return pref!!.getString(key, \"\") } fun getBoolean(key: String): Boolean { return pref!!.getBoolean(key, false) } fun getInt(key: String): Int { return pref!!.getInt(key, 0) } fun clear() { pref!!.edit().clear().apply() } fun dataPrepare(key: String, value: Any): SharePrefUtils { // map.clear(); map[key] = value return this } fun putData(): SharePrefUtils { // 获取所有键值对对象的集合 for ((key, value) in map) { // 根据键值对对象获取键和值 putObject(key, value) } return this } private fun putObject(key: String, value: Any) { when (value) { is Boolean -\u003e pref!!.edit().putBoolean(key, value).apply() is Float -\u003e pref!!.edit().putFloat(key, value).apply() is Int -\u003e pref!!.edit().putInt(key, value).apply() is Long -\u003e pref!!.edit().putLong(key, value).apply() is String -\u003e pref!!.edit().putString(key, value).apply() } } class Builder { private var context: Context? = null private var pref: SharedPreferences? = null fun setContext(context: Context): Builder { this.context = context return this } fun setPref(prefName: String): Builder { if (pref == null \u0026\u0026 context != null) { pref = context!!.getSharedPreferences(prefName, Context.MODE_PRIVATE) } return this } fun create(): SharePrefUtils { val sharePrefUtils = SharePrefUtils() if (pref != null) { sharePrefUtils.setPref(pref!!) } return sharePrefUtils } } } 这里使用建造者模式写这个工具类,使用方法: 先创建实例 private var sharePrefUtils: SharePrefUtils? = null sharePrefUtils = SharePrefUtils .Builder() .setContext(context) // 传上下文Context .setPref(\"SharePrefName\") // SharePref的名称 .create() 根据Key获取Value var value = sharePrefUtils!!.getInt(\"Key\") 把数据存到SharePref中 sharePrefUtils!! .dataPrepare(\"Key\", value) .putData() ","date":"2019-03-24","objectID":"/post/e836c9c6/:2:1","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"创建管理语言的工具类(LocaleManagementUtil.kt) 首先,获取用户设置过的语言 // 静态属性,修复识别系统语言为英语的问题 private var systemCurrentLocal = Locale.ENGLISH // 默认英语 fun getSelectLanguage(): Int { return sharePrefUtils!!.getInt(Constant.TAG_LANGUAGE) } /** * 获取选择的语言设置 * * @param context * @return Locale对象 */ fun getSetLanguageLocale(context: Context): Locale { return when (getSelectLanguage()) { 0 -\u003e getSystemLocale(context) // 获取系统设置的语言 1 -\u003e Locale.CHINA 2 -\u003e Locale.ENGLISH else -\u003e Locale.ENGLISH } } /** * 获取系统的locale * * @return Locale对象 */ fun getSystemLocale(context: Context): Locale { return systemCurrentLocal } /** * 获取系统的locale并保存到静态变量systemCurrentLocal中 */ fun saveSystemCurrentLanguage(context: Context) { val locale: Locale = if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.N) { LocaleList.getDefault().get(0) } else { Locale.getDefault() } systemCurrentLocal = locale } 改变语言 fun setLocal(context: Context): Context { return updateResources(context, getSetLanguageLocale(context)) } private fun updateResources(context: Context, locale: Locale): Context { var context = context Locale.setDefault(locale) val res = context.resources val config = Configuration(res.configuration) if (Build.VERSION.SDK_INT \u003e= 19) { config.setLocale(locale) context = context.createConfigurationContext(config) } else { config.locale = locale res.updateConfiguration(config, res.displayMetrics) } return context } ","date":"2019-03-24","objectID":"/post/e836c9c6/:2:2","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"调用方法设置语言 Application override fun attachBaseContext(base: Context) { LocaleManageUtil.saveSystemCurrentLanguage(base) super.attachBaseContext(base) } override fun onCreate() { super.onCreate() instance = this LocaleManageUtil.setSharePref(this) } companion object { lateinit var instance: App private set } BaseActivity override fun attachBaseContext(newBase: Context) { super.attachBaseContext(LocaleManageUtil.setLocal(newBase)) } Service override fun attachBaseContext(newBase: Context) { super.attachBaseContext(LocaleManageUtil.setLocal(newBase)) } 设置语言并重启Activity到启动页 LocaleManageUtil.kt fun saveLanguage(select: Int) { sharePrefUtils!! .dataPrepare(Constant.TAG_LANGUAGE, select) .putData() } fun saveSelectLanguage(context: Context, select: Int) { saveLanguage(select) } /** * 跳转主页 * * @param context */ fun toRestartLauncherActivity(context: Context) { val intent = Intent(context, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) } SettingActivity.kt private fun selectLanguage(select: Int) { LocaleManageUtil.saveSelectLanguage(this, select) LocaleManageUtil.toRestartLauncherActivity(this) } 这里稍微解释一下:在Application和Activity中,attachBaseContext都是在onCreate方法之前执行的,所以我们先在Application中的attachBaseContext获取当前系统的语言并存下来(必须在所有获取语言的方法执行前存下当前系统语言,否则在后面获取可能会出现获取到的不是系统语言而是设置的语言的情况)。 到这里理论上语言就可以成功改变啦~ 遇到的坑 ","date":"2019-03-24","objectID":"/post/e836c9c6/:2:3","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"context为applicationContext时切换语言后设置的string没有改变的问题 我们都会在代码中调用context.getResource().getString()这句代码看起来没什么问题,但是你这个context要是用的是applicationContext那么问题就来了。你会发现当你切换语言后用这样方式设置的string没有改变,所以我们需要改动我们的代码。 解决方法就是,在切换语言后把application的updateConfiguration也要更新了,方法如下: LocaleManageUtil.kt /** * 设置语言类型 */ fun setApplicationLanguage(context: Context) { val resources = context.applicationContext.resources val dm = resources.displayMetrics val config = resources.configuration val locale = getSetLanguageLocale(context) config.locale = locale if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.N) { val localeList = LocaleList(locale) LocaleList.setDefault(localeList) config.locales = localeList context.applicationContext.createConfigurationContext(config) Locale.setDefault(locale) } resources.updateConfiguration(config, dm) } fun saveSelectLanguage(context: Context, select: Int) { saveLanguage(select) setApplicationLanguage(context) } App.kt override fun onCreate() { super.onCreate() instance = this LocaleManageUtil.setSharePref(this) LocaleManageUtil.setApplicationLanguage(this) } ","date":"2019-03-24","objectID":"/post/e836c9c6/:3:0","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"解决横竖屏切换后语言变成系统语言的问题 LocaleManageUtil.kt fun onConfigurationChanged(context: Context) { saveSystemCurrentLanguage(context) setLocal(context) setApplicationLanguage(context) } App.kt override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) LocaleManageUtil.onConfigurationChanged(this) } 源码地址 具体的源码在github上,大家可以自行查看 参考文章 Android App 多语言切换 Android面试系列之应用内多语言切换 android程序内多语言切换不需要重新启动的解决方案 - 这第三篇好像讲的有点复杂,但是看完代码觉得效果应该不错,不过还没尝试 安卓App内多语言切换(Kotlin实现),不需要强杀重启app 不只是切换多语言Android(二) - 这个讲的不错,原理跟第三篇一样,直接在View实现,源码在这里。 - BTW:他前一篇换肤的文章——不只是切换多语言Android(一)也写的不错,有兴趣的朋友可以去看看 Android国际化(多语言)实现,支持8.0 - 最终我参考的最多的是这个方案 XD Android Context完全解析,你所不知道的Context的各种细节 - 郭神的文章,主要是为了看Application方法的执行顺序,看看什么方法是在onCreate()之前执行的——attachBaseContext() Android Developer官方文档——处理配置变更 onConfigurationChanged方法介绍及问题解决 - 可解决横竖屏切换后语言变成系统语言的问题 ","date":"2019-03-24","objectID":"/post/e836c9c6/:4:0","tags":["Android"],"title":"Android多语言切换实现进化版--Kotlin实现","uri":"/post/e836c9c6/"},{"categories":["Android"],"content":"前言 最近在想宝可梦的那样记账要加什么功能好呢?突然想到,不如实现多语言手动切换(国际化),方便那些为了开Google Assistant而把语言设置成英文的朋友可以在该App使用中文界面。 需求 可以随着系统切换语言而切换语言(即跟随系统),不支持的语言显示默认(默认为英文) 用户可以自由选择自己想用的语言,且不会随着系统切换语言或者应用重启而还原 虽然看着简单,但是实现起来还是遇到了点问题 原理 参照 Android Developer官网 上的描述,Configuration 包含了设备的所有的配置信息,这些配置信息会影响应用获取的资源。例如string资源,就是在不同的res/value-xx下放置不同语言的strings.xml实现字符的本地化,然后根据Configuration的locale属性来判断该取哪种语言的string资源,而这个value-xx目录的选择是根据locale这项的值来决定的。 如zh中文,就会选择value-zh目录,如果没有匹配到(即res中没有value-zh目录)就使用默认的value目录中的字符资源。 那么我们只需要想办法改变并重新加载Configuration即可!!! 注:value-en、value-zh-rCN、values-zh-rTW分别表示英文、简体中文、繁体中文三种语言 初次尝试 ","date":"2019-03-23","objectID":"/post/8a9e22db/:0:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"1. 添加多语言文件 在不同的value文件夹下(例如value、value-en、value-zh-rCN、values-zh-rTW文件夹)添加不同语言的string.xml文件,我们的项目添加了英文、简体中文、繁体中文三种语言。 翻译之类的就自行翻译啦~繁体可以找个简繁转换网站。 ","date":"2019-03-23","objectID":"/post/8a9e22db/:1:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"2. 更新 Configuration 中的 locale 属性来实现app语言的切换 主要代码如下: Resources resources = getContext().getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); Configuration config = resources.getConfiguration(); // 应用用户选择语言,如: // config.locale = Locale.ENGLISH; resources.updateConfiguration(config, dm); 我们用了Locale中的预设值Locale.ENGLISH、Locale.TRADITIONAL_CHINESE和Locale.SIMPLIFIED_CHINESE,如果你需要设置的语言没有预设值,你可以自己新建一个Locale对象,具体自行 Google 吧。 注:跟随系统设置是Locale.getDefault() ","date":"2019-03-23","objectID":"/post/8a9e22db/:2:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"3. 重启 HomeActivity 我们的 App 有个启动页WelcomeActivity,类似微信那个小人启动页,如果从欢迎页重启,并不是一个好的体验,应该和微信的语言设置一样,直接回到HomeActivity,而不是从 WelcomeActivity重新打开。实现其实也很简单,代码如下: Intent intent = new Intent(this, HomeActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); getActivity().startActivity(intent); // 杀掉进程,如果是跨进程则杀掉当前进程 // android.os.Process.killProcess(android.os.Process.myPid()); // System.exit(0); ","date":"2019-03-23","objectID":"/post/8a9e22db/:3:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"4. 持久化存储语言设置 按照上述三个步骤,你应该已经可以看到了改变语言的效果了,但是当你杀掉应用,重新打开,发现设置又失效了。这是因为应用重启后会读取设备默认的Configuration信息,其中和语言相关的locale属性也会变成默认值,也就是你在系统设置中选择的语言。 当你的应用需要由用户单独设置语言,而不是仅仅跟随系统语言,你就需要持久化存储用户的设置信息了。你可以选择数据库、或SharedPreferences来存储设置信息,存一个Type的int值即可。 ","date":"2019-03-23","objectID":"/post/8a9e22db/:4:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"5. 根据本地缓存的type获取对应的locale 其中7.0以上的系统需要另做处理(在后面会讲处理兼容性问题),具体代码如下: Locale locale; // 应用用户选择语言 switch (type) { case 0: locale = I18NUtils.getSystemLocale(); // getSystemLocale()是一个自定义方法,用于获取系统语言 break; case 1: ...;break; ... default: locale = enLocale; // enLocale 是一个静态 Locale 变量,用于默认为未提供的语言显示英文语言 break; } ","date":"2019-03-23","objectID":"/post/8a9e22db/:5:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"6. 在AppApplication中初始化时设置本地语言 用于每次启动APP后切换到本地缓存的语言 public class App extends Application { @Override public void onCreate() { super.onCreate(); ... Resources resources = App.getContext().getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); Configuration config = resources.getConfiguration(); // 设置本地化语言 config.locale = getSetLocaleInSP(); resources.updateConfiguration(config, dm); } // 得到设置的语言信息 private static Locale getSetLocaleInSP() { // 读取储存的语言设置信息(结合第五步) ... } ","date":"2019-03-23","objectID":"/post/8a9e22db/:6:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"7. 在BaseActivity的OnCreate()方法中设置语言 用于处理每次切换系统语言后app语言会跟随系统变化的问题。 在这一步之前会遇到一个问题:当从应用中切出去,改变了系统语言的设置,当再切应用的时候,我发现语言也会变成系统语言(而我并没在应用内设置跟随系统)。 引用自@写代码的猴子的文章: 简单来说,上一步中,我们在 App 启动时,读取了用户的设置信息,并应用到 Configuration的locale属性上,然后通过resources.updateConfiguration(config, dm)改变了应用的配置信息(Configuration)并生效,保证我们的应用读取的string资源都是用户设置语言对应的资源。在我们改变系统的语言之后,再回到我们的应用中,此时的Configuration的locale属性就会发生变化了,不再是我们刚才自己的在应用启动时设置的了,而是变成了系统的设置了。 那么解决办法也很简单,我们都知道 activity 有生命周期,在切回我们的应用时,在显示的 activity 的生命周期中做一些处理就好了(有点粗暴~) 在实际开发中,我们会建立很多个 activity,而每一个 activity 都要更改语言的。这个时候我们如果一个个做处理是不是很麻烦?这个时候我们利用面向对象语言的继承特性即可,创建一个BaseActivity,在BaseActivity 中处理后其他 Activity 都去继承BaseActivity就好了。@写代码的猴子的文章中说评论中提到,在改变了系统设置之后,回到应用会重走activity的onCreate(如果按照Activity的生命周期看其实应该是onResume才对),那就在OnCreate中处理一下: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!LanguageUtil.isSetValue()) { LanguageUtil.resetAppLanguage(); } ... } public class LanguageUtil { ... /** * 是否是设置值 * * @return 是否是设置值 */ public static boolean isSetValue() { Locale currentLocale = App.getContext().getResources().getConfiguration().locale; return currentLocale.equals(getSetLocaleInSP()); } } ","date":"2019-03-23","objectID":"/post/8a9e22db/:7:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"解决7.0以上系统存在的兼容问题——跟随系统语言失效 Android 7.0 语言设置爬坑这篇文章讲的很详细,具体还可以参考官方文档和官方API 如果不做兼容,可能会出现以下情况:在 App 中,语言默认选择的是「跟随系统」(系统语言列表中「简体中文」是第一个),然后选择「英语」,之后再切换回「跟随系统」,发现语言并没切回「简体中文」,而还是「英语」。 由于 Android7.0 以上Configuration将通过LocaleList来管理语言,并且系统切换语言后,系统默认语言可能并不在LocaleList顶部。 经过调试发现:如果在 App 中手动选择(切换)过语言则在LocaleList中系统语言是第二个,否则是第一个。 所以,获取当前系统locale,代码如下: Locale systemLocale; //由于API仅支持7.0,需要判断,否则程序会crash(解决7.0以上系统不能跟随系统语言问题) if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.N) { LocaleList localeList = LocaleList.getDefault(); // 获取之前选择过语言后缓存在 SharePreferences 的语言(即当前选择的语言) int spType = getLanguageType(AppApplication.getAppContext()); // 如果app已选择不跟随系统语言,则取第二个数据为系统默认语言 if(spType != 0 \u0026\u0026 localeList.size() \u003e 1) { locale = localeList.get(1); } else { locale = localeList.get(0); } } else { locale = Locale.getDefault(); } 缺点 但是这个做出来的效果是每次启动都会重启一次Activity,强迫症的我表示根本受不了😂,而且横竖屏切换后会有语言变成系统语言的问题 参考文章 Android多语言切换完美解决方案(兼容7.0以上版本) 源码地址:https://github.com/Fitem/I18NDemo Android App 多语言切换 Android面试系列之应用内多语言切换 ","date":"2019-03-23","objectID":"/post/8a9e22db/:8:0","tags":["Android"],"title":"Android多语言切换实现--Java实现","uri":"/post/8a9e22db/"},{"categories":["Android"],"content":"背景 昨晚,我又被奕广大佬日常拖去讨论问题(可以,这宿舍很好学🌚)。 这次讨论的问题是关于 Android 中 layout_weight(权重) 的用法,然后在这过程中,我们遇到了一个坑——关于权重的计算问题。 这问题可能比较基础,但是咧,我之前一直都不会用 layout_weight 的,所以是属于 layout_weight 使用的新手,大佬请绕道勿喷😂 layout_weight 的分析 ","date":"2018-10-10","objectID":"/post/73a11b2e/:0:0","tags":["Android","宿舍日常问题探讨","日常挖坑"],"title":"一次对layout_weight和match_parent的深入探究","uri":"/post/73a11b2e/"},{"categories":["Android"],"content":"权重分配的是哪些空间? 首先要明白权重分配的是哪些空间? 权重是按照比例分配屏幕的剩余空间,对这句话不理解的可以看下图: layout_weight analyse\" layout_weight analyse 假如我们希望剩余的空间平分给空间1 和空间2 , 我们分别在2个控件的设置android:layout_weight=\"1\" ","date":"2018-10-10","objectID":"/post/73a11b2e/:1:0","tags":["Android","宿舍日常问题探讨","日常挖坑"],"title":"一次对layout_weight和match_parent的深入探究","uri":"/post/73a11b2e/"},{"categories":["Android"],"content":"计算原理 然后我们来说下 layout_weight 的计算原理: 控件的尺寸 = 设置的控件尺寸 + 剩余空间中控件的百分比 详细的计算方式则为: 控件的宽(高)度 = 控件 的 width(height) + 控件的 weight * [父布局(LinearLayout)的宽(高)度-所有控件所占宽(高)的和] / weightSum 其中, 控件的 weight / weightSum 就是权重比; 父布局(LinearLayout)的宽(高)度-所有控件所占宽(高)的和为剩余可分配空间 补充: android:layout_weight的真实含义是:一旦 View 设置了该属性(假设有效的情况下),那么该 View的宽度等于原有宽度(android:layout_width)加上剩余空间的占比! 如果 width 设置了 match_parent ,那么,加上的是负的长度(相当于减去一部分长度) 如果 width 设置了 wrap_content,那么,剩余空间是“父容器总长度”减去“组件的内容占的长度”,然后再按比重值分。 遇到的问题 我们遇到的是以下代码: \u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\" android:orientation=\"vertical\" android:id=\"@+id/activity_main\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\"\u003e \u003cButton android:layout_weight=\"1\" android:layout_width=\"match_parent\" android:layout_height=\"0dp\" android:text=\"button1\" /\u003e \u003cButton android:layout_weight=\"1\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\" android:text=\"button2\" /\u003e \u003c/LinearLayout\u003e 然后我跟奕广就蒙圈了,为啥两个都是match_parent或0dp的时候控件可以各占一半, both match or 0dp\" both match or 0dp 而一个是 0dp 和 一个是 match_parent 却不能,反倒被button2给覆盖了? bt1 0dp bt2 match\" bt1 0dp bt2 match 后来知道计算方法之后算了一下,又蒙了,算不对,我们是这样算的: 根据公式: 控件的宽(高)度 = 控件 的 width(height) + 控件的 weight * [父布局(LinearLayout)的宽(高)度-所有控件的宽(高)的和] / weightSum 得计算过程: 设父布局为1列(宽)x2行(高),则对半分之后每个控件的布局为1列(宽)x1行(高),所以每个控件的实际高度应该为1行,得: Button2的实际显示高度 = 1行 + 1weight * (2-1-1)行 / (1+1)weight = 1行 \u003c 2行 (错) 很明显,我们的正确答案应该是算到Button2实际显示的 2 行才是正确的,但是现在算到了 1 行,所以显然是算错了。 研究了很久才发现我们对 match_parent 的理解错了😂。 ☆☆接下来是重点!!!☆☆ 从计算过程中可以看出,我们认为,match_parent的实际值是 1 行。实则不然,match_parent是铺满父布局的宽(高),那么父布局为1列(宽)*2行(高)的话,match_parent的值应为 2 行!! 我们再算一遍: 设父布局为1列(宽)x2行(高),则对半分之后每个控件的布局为1列(宽)x1行(高),但 由于Button2的高度设置了match_parent,所以Button2设置的高度应该为2行,得: Button2的实际显示高度 = 2行 + 1weight * (父布局2-2-0)行 / (1+1)weight = 2-0行 = 2行 由于Button1的高度设置了 0dp,所以Button1设置的高度应该为0行,得: Button1的实际显示高度 = 0行 + 1weight * (2-2-0)行 / (1+1)weight = 0+0行 = 0行 这样数据就符合实际显示的效果了,完~ 参考 android中weight的一个坑 Android:layout_weight的使用和坑 Android自适应布局(关于权重weight的使用技巧!) 从源码切入 透彻理解Android的weight属性 android weight(权重)的具体分析 ","date":"2018-10-10","objectID":"/post/73a11b2e/:2:0","tags":["Android","宿舍日常问题探讨","日常挖坑"],"title":"一次对layout_weight和match_parent的深入探究","uri":"/post/73a11b2e/"},{"categories":["逆向"],"content":"背景 在昨天,我发布了一篇又长又臭的StarUML 3.x 完美破解方案 然后今天早上一起床就想到,啊~好麻烦啊!能不能让完美再简单点? 好的,然后我又开始了折腾(真丶生命在于折腾😂) 我的想法 这一次,我要先从about-dialog.js开始分析 (about-dialog.js路径:解压app.asar后/app/src/dialogs/about-dialog.js) 正所谓:你要什么我就给什么才是最方便的🌚 正文 下面开始正式分析破解 ","date":"2018-09-15","objectID":"/post/d94bd648/:0:0","tags":["逆向","破解","UML"],"title":"另一种 StarUML 3.x 完美破解的思路——比较暴力","uri":"/post/d94bd648/"},{"categories":["逆向"],"content":"查看关于中要显示的信息需要获取的数据 about-dialog.js中第 45-71 行: // set license info if (app.licenseManager.getStatus() === true) { var info = app.licenseManager.getLicenseInfo() var licenseTypeName = 'Unknown' switch (info.licenseType) { case 'PS': licenseTypeName = 'Personal' break case 'CO': licenseTypeName = 'Commercial' break case 'ED': licenseTypeName = 'Educational' break case 'CR': licenseTypeName = 'Classroom' break } $license.html('Licensed to ' + info.name) $licenseType.html(licenseTypeName + ' License') $quantity.html(info.quantity + ' User(s)') $crackedAuthor.html('Cracked by ' + info.crackedAuthor) } else { $license.html('UNREGISTERED') } return dialog } 可以看出,它首先会先调用license-manager.js中的getStatus()方法判断程序的注册状态。那现在我们去license-manager.js看一下getStatus()这个方法: /** * Get Registration Status * @return {string} */ getStatus () { return status } 嗯……很简单的一个方法,我是试过直接在这里设置\"true\"(注意!这里是返回字符串),但是没什么用,我就没改这里了。那么这里到底有什么用呢?大家看它返回的那个变量:status,这是我们唯一从这里得到的信息,我们全文搜索一下status,可以找到一个setStatus(...)的方法。我修改了一下让它总是设置为true,代码如下: function setStatus (licenseManager, newStat) { if (status !== newStat) { status = newStat licenseManager.emit('statusChanged', 'true') // status修改为'true',注意要带单引号 } } 好了,现在我们可以进入那个if语句了🌚。不难看出,接下来需要的数据都在变量info里,从这一句 var info = app.licenseManager.getLicenseInfo() 可以看出,它调用了license-manager.js中的getLicenseInfo()方法获取所需数据,我们去看一下getLicenseInfo()方法: getLicenseInfo () { return licenseInfo } 嗯…..还是那么简洁,但是从昨天的文章我们已经知道这个licenseInfo的数据内容格式,他要的数据也是licenseInfo的数据。那么,我们直接模拟licenseInfo的数据即可: getLicenseInfo () { licenseInfo = { name: \"Reborn\", product: \"Reborn product\", licenseType: \"PS\", quantity: \"Reborn Quantity\", timestamp: \"1529049036\", licenseKey: \"It's Cracked!!\", crackedAuthor: \"Reborn\" }; return licenseInfo } 好了,模拟成功,about-dialog.js那边应该能获取到licenseInfo的数据了。但是,仅仅是这样还不行哦!还有最重要的一点你们别忘了——我们还没破解! ","date":"2018-09-15","objectID":"/post/d94bd648/:1:0","tags":["逆向","破解","UML"],"title":"另一种 StarUML 3.x 完美破解的思路——比较暴力","uri":"/post/d94bd648/"},{"categories":["逆向"],"content":"破解注册 破解注册很简单,直接修改license-manager.js中的checkLicenseValidity()这个方法就好了。 修改后的代码如下: checkLicenseValidity () { this.validate().then(() =\u003e { setStatus(this, true) }, () =\u003e { // 原来的代码,如果失败就会将状态设置成false // setStatus(this, false) // UnregisteredDialog.showDialog() //修改后的代码 setStatus(this, true) }) } ","date":"2018-09-15","objectID":"/post/d94bd648/:2:0","tags":["逆向","破解","UML"],"title":"另一种 StarUML 3.x 完美破解的思路——比较暴力","uri":"/post/d94bd648/"},{"categories":["逆向"],"content":"注册成功!! 完成以上流程后应该就能成功直接破解了,不用输入注册码,并且这种方法破解后同样能在关于显示你自定义的破解信息!!一样完美~ staruml-about\" staruml-about 这种方法和昨天的比起来更简单,但是也更暴力。昨天的比较接近正常的验证流程,这种就有点爆破的味道了。这里给出另一种思路给大家参考,希望对大家有帮助。 ","date":"2018-09-15","objectID":"/post/d94bd648/:3:0","tags":["逆向","破解","UML"],"title":"另一种 StarUML 3.x 完美破解的思路——比较暴力","uri":"/post/d94bd648/"},{"categories":["逆向"],"content":"背景 终于开始新的学期了,这学期我有个 UML建模 的课程,上机作业就是要用到 StarUML 这个软件,但是……啊——为什么 StarUML 那么丑啊!!!而且,为什么这么难用啊!!!(马上开始怀疑是旧旧旧…\u003c省略一万个“旧”\u003e…版) (图片下次上机后附上) 好吧,颜值帝的我无奈翻了一下官方,发现还机房的还真的是超级旧版,一进去官网就能看到我最喜欢的暗黑风格😍。 staruml-officialwebsite\" staruml-officialwebsite 果断下载来试用了一下,哇!这个操作比机房的体验好超级多了好吗😂 这软件开源,但是却收费,穷得要死(铁公鸡)的我肯定是不会付费用的了(其实是想顺便看下这个软件的验证逻辑),于是研究起了破解之道~ 关于 StarUML 的破解原理 参考:https://www.jianshu.com/p/b6b1f6ad0bd6 StarUML 是用 nodejs 写的,确切的说是用 Electron前端框架 写的。 新版本中所有的 StarUML 源代码是通过 asar 工具打包而成,确切的代码位置在%LOCALAPPDATA%\\Programs\\StarUML\\resources\\app.asar。 我们可以通过asar工具解压修改达到破解目的,关于asar工具使用可看本文附录1。 提取 app.asar 下载的 StarUML.app,右键显示包内容 进入Contents/Resources/ 把app.asar复制出来 为什么网上那么多破解教程我还要写? 参考: Mac StarUML 3.0 破解 那……当然不是为了存档! 相信搜过 StarUML3 破解的朋友都知道,想要破解就修改app.asar解压出来的app/src/engine/license-manager.js,把其中的checkLicenseValidity函数修改成: checkLicenseValidity () { this.validate().then(() =\u003e { setStatus(this, true) }, () =\u003e { // 原来的代码,如果失败就会将状态设置成false // setStatus(this, false) // UnregisteredDialog.showDialog() //修改后的代码 setStatus(this, true) }) } 然后重新打包app.asar放回StarUML.app/Contents/Resources/,破解成功了。 那么,为什么网上那么多破解教程我还要写一个教程呢? 因为我是个完美主义者,我发现网上的教程都不能达到完美破解的效果——我的注册信息怎么没法显示(自定义)!!!于是我就着手研究起app.asar解压出来的代码了🌚 开始分析 ","date":"2018-09-14","objectID":"/post/ee00472d/:0:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"破解的核心:license-manager.js 参考: 绕过StarUML3 正版验证,去除水印 破解的核心还是license-manager.js这个跑不掉,那么我们先从这个文件开始分析。 ","date":"2018-09-14","objectID":"/post/ee00472d/:1:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"根据注册码验证流程分析 流程图 在这之前,我们先想一下正常的注册码验证流程是什么: {% raw %} 正常的注册码验证流程 {% endraw %} 打开 app 可以看到,我们首先要打开 app,对应的代码是: htmlReady () { this.projectManager.on('projectSaved', (filename, project) =\u003e { var val = Math.floor(Math.random() * (1.0 / LICENSE_CHECK_PROBABILITY)) if (val === 0) { this.checkLicenseValidity() } }) } appReady () { this.checkLicenseValidity() } 可以看到,这两个方法中都会用到this.checkLicenseValidity(),看英文意思明显是检查许可证是否有效,所以把checkLicenseValidity这个方法修改一下就可以破解了(修改方法看上面) 但是,这里我们先不修改!!!因为修改后没法弹出无法弹出输入注册码的窗口,就无法完美破解了!! 输入注册码 接下来我们来看一下输入注册码也就是注册的逻辑: register (licenseKey) { return new Promise((resolve, reject) =\u003e { $.post(app.config.validation_url, {licenseKey: licenseKey}) .done(data =\u003e { var file = path.join(app.getUserPath(), '/license.key') fs.writeFileSync(file, JSON.stringify(data, 2)) licenseInfo = data setStatus(this, true) resolve(data) }) .fail(err =\u003e { setStatus(this, false) if (err.status === 499) { /* License key not exists */ reject('invalid') } else { reject() } }) }) } 解释一下,这个register函数需要传入的参数licenseKey就是输入的注册码,然后用POST请求 balabala,请求成功后返回一个data,然后在 StarUML 的配置信息目录新建一个license.key的文件,里面包含着所有许可证信息。可以看到,这个许可证信息是用 JSON 格式储存的,而请求后获取的data就是许可证信息的内容,那么我们是不是只要模拟已经获取到了许可证信息是不是就ok了? 那么问题又来了,我们怎么知道data的内容格式呢? 我们不妨参考一下 StarUML2 的破解:https://www.jianshu.com/p/0c49ebf342e0 在 StarUML2 的破解中我们可以看到如下代码: return { name: \"0xcb\", product: \"StarUML\", licenseType: \"vip\", quantity: \"bbs.chinapyg.com\", licenseKey: \"later equals never!\" }; 我斗胆猜测这就是data的内容格式(也就是许可证信息的内容格式)!!! 我们广东人常常会说一句话:讲多无谓,行动最实际。我们下面就来尝试一下! 既然是本地模拟,那当然要把网络请求去掉然后模拟data的内容,修改整理后得到以下代码: register (licenseKey) { return new Promise((resolve, reject) =\u003e { var data = { name: \"Reborn\", product: \"Reborn product\", licenseType: \"PS\", quantity: \"Reborn Quantity\", timestamp: \"1529049036\", licenseKey: \"It's Cracked!!\", crackedAuthor: \"Reborn\" } var file = path.join(app.getUserPath(), '/license.key') fs.writeFileSync(file, JSON.stringify(data, 2)) licenseInfo = data setStatus(this, true) resolve(data) }) } 到这里许可证信息应该是模拟成功了,看 StarUML 的配置信息目录下应该能看到一个license.key的文件。输完注册码后就要验证注册码是否有效,接下来我们看验证许可证信息模块。 这里要注意一下:licenseType的设置是固定几个参数的,在这里我设置为PS,之前设置Reborn Personal导致关于那里显示Unknown License。关于这个大家可以去看看app/src/dialogs/about-dialog.js。 验证许可证信息 先来看下代码吧: validate () { return new Promise((resolve, reject) =\u003e { try { // Local check var file = this.findLicense() if (!file) { reject('License key not found') } else { var data = fs.readFileSync(file, 'utf8') licenseInfo = JSON.parse(data) var base = SK + licenseInfo.name + SK + licenseInfo.product + '-' + licenseInfo.licenseType + SK + licenseInfo.quantity + SK + licenseInfo.timestamp + SK var _key = crypto.createHash('sha1').update(base).digest('hex').toUpperCase() if (_key !== licenseInfo.licenseKey) { reject('Invalid license key') } else { // Server check $.post(app.config.validation_url, {licenseKey: licenseInfo.licenseKey}) .done(data =\u003e { resolve(data) }) .fail(err =\u003e { if (err \u0026\u0026 err.status === 499) { /* License key not exists */ reject(err) } else { // If server is not available, assume that license key is valid resolve(licenseInfo) } }) } } } catch (err) { reject(err) } }) } 可以看到验证分两部分:先本地验证再进行网络验证,网络验证成功后返回许可证信息。 本地验证 我们一步步来,先处理本地验证: // Local check var file = this.findLicense() if (!file) { reject('License key not found') } else { var data = fs.readFileSync(file, 'utf8') licenseInfo = JSON.parse(data) var base = SK + licenseInfo.name + SK + licenseInfo.product + '-' + licenseInfo.licenseType + SK + licenseInfo.quantity + SK + licenseInfo.timestamp + SK var _key = crypto.createHash('sha1').update(base).digest('hex').toUpperCase() if (_key !== licenseInfo.licenseKey) { reject('Invalid license key') } ..... } 本地验证真正开始验证的地方是从第 8 行开始的。通过文件license.key中的部分许可证信息计算出一个注册码然后和许可证信息中的licenseKey进行比较,如果不相等那就返回注册码失效(从这里的licenseInfo调用也可以看出许可证信息包含了什么,与上一步的data验证)。 既然如此,我们就不让它比较嘛,我们直接返回成功不就得了! 好的,第 8 行之后的都删掉。 网络验证 由于返回的逻辑不在本地验证,所以我们还要分析网络验证: // Server check $.post(app.config.validation_url, {licenseKey: licenseInfo.licenseKey}) .done(data =\u003e { resolve(data) }) .fail(err =\u003e { if (err ","date":"2018-09-14","objectID":"/post/ee00472d/:1:1","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"安装asar sudo npm install -g asar ","date":"2018-09-14","objectID":"/post/ee00472d/:2:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"解压app.asar asar extract app.asar app ","date":"2018-09-14","objectID":"/post/ee00472d/:3:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"重新打包app.asar asar pack app app.asar 附录2:自定义关于界面 大家做完上面的操作肯定会发现,为什么你们的关于没有Cracked By XXX的字样,那是因为我修改了关于的 GUI,在前面并没有说,在这里我特别分出来写一下,算是个小彩蛋吧! ","date":"2018-09-14","objectID":"/post/ee00472d/:4:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"找到代码所在文件 首先我们要找到关于这个界面的代码文件,经过分析和查找,我找到了about-dialog.js和about-dialog.html 至于怎么找到的,大家看一下about-dialog.js的这句代码就知道了: const aboutDialogTemplate = fs.readFileSync(path.join(__dirname, '../static/html-contents/about-dialog.html'), 'utf8') ","date":"2018-09-14","objectID":"/post/ee00472d/:5:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"开始分析 ","date":"2018-09-14","objectID":"/post/ee00472d/:6:0","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"about-dialog.html 我们先来看一下 html 吧,在这个文件中大家可以很明显看到这么一段: \u003cdiv style=\"font-size: 20px;\"\u003e{{metadata.name}}\u003c/div\u003e \u003cdiv style=\"font-size: 14px;\" class=\"license\"\u003e\u003c/div\u003e \u003cdiv style=\"font-size: 14px;\" class=\"licenseType\"\u003e\u003c/div\u003e \u003cdiv style=\"font-size: 14px;\" class=\"quantity\"\u003e\u003c/div\u003e \u003cbr\u003e \u003cdiv\u003e{{metadata.copyright}}\u003c/div\u003e \u003cdiv\u003e\u003cb\u003eVersion {{metadata.version}}\u003c/b\u003e\u003c/div\u003e \u003cdiv\u003e\u003ca class=\"thirdparty\" href=\"#\"\u003eThird party softwares\u003c/a\u003e\u003c/div\u003e 对比一下关于的界面,是不是觉得刚好对上了🌚 好的那么接下来我们加一个破解者的信息: ...... \u003cdiv style=\"font-size: 14px;\" class=\"quantity\"\u003e\u003c/div\u003e \u003cdiv style=\"font-size: 16px;\" class=\"crackedAuthor\"\u003e\u003c/div\u003e \u003cbr\u003e ...... 这里第 3 行就是我添加的信息,其中font-size是调整字体大小,class=\"crackedAuthor\"这个类选择器名crackedAuthor要记住,等下要用到。 接下来我们看about-dialog.js ","date":"2018-09-14","objectID":"/post/ee00472d/:6:1","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"about-dialog.js 在这个 js 文件里面,我们可以看到这几行代码: ...... var $license = $dlg.find('.license') var $licenseType = $dlg.find('.licenseType') var $quantity = $dlg.find('.quantity') ...... $license.html('Licensed to ' + info.name) $licenseType.html(licenseTypeName + ' License') $quantity.html(info.quantity + ' User(s)') ...... 我们来分析一下这几行代码吧! $dlg.find('.xxx')的意思是搜索所有类选择器名为.xxx的元素。 $xxx.html('abc')的意思是设置xxx元素的内容为abc。 说到这里大家应该知道怎么修改了吧!这是我修改的代码: ...... var $license = $dlg.find('.license') var $licenseType = $dlg.find('.licenseType') var $quantity = $dlg.find('.quantity') var $crackedAuthor = $dlg.find('.crackedAuthor') ...... $license.html('Licensed to ' + info.name) $licenseType.html(licenseTypeName + ' License') $quantity.html(info.quantity + ' User(s)') $crackedAuthor.html('Cracked by ' + info.crackedAuthor) ...... ","date":"2018-09-14","objectID":"/post/ee00472d/:6:2","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["逆向"],"content":"修改成功 保存好代码重新打包app.asar就可以看到修改后的关于界面啦! staruml-about\" staruml-about ","date":"2018-09-14","objectID":"/post/ee00472d/:6:3","tags":["逆向","破解","UML"],"title":"StarUML 3.x 完美破解全过程含详细分析(可自定义许可证信息)","uri":"/post/ee00472d/"},{"categories":["python"],"content":" 参考:http://www.freebuf.com/articles/database/145261.html 背景 昨天有个朋友说准考证没带回来,问我能不能帮她查下准考证号,我突然想起了今年2月份穷举的四级准考证,于是就翻出来试试。 当时我对源码进行了一些修改,不过一年前我还没搭建博客,如今也算有个机会对当时的学习做个记录和总结。 昨天我也继续对源码进行了一番改进,改的都非常简单,认真阅读了原作者那部分修改前的源码就肯定能看懂。 要懂得感激 首先,感谢@FlashYo提供的源码,让我也学习了不少东西。里面链接原本是失效了的,我又去问了@FlashYo,想看下utils.py是怎么写的,他想都没想就分享了,我表示很感激。 于是呢,我就开始了第一次阅读 Python 代码的经历(其实这也是我第一次接触 Python ),代码看完之后感觉也不难,@FlashYo的代码写的很有条理,他的文章也有很详细的代码说明。 但由于原文说不经允许禁止转载,我也尊重作者,所以这里就不贴他的代码和文章内容啦!有兴趣的可以去上面提供的参考链接看。 2018-08-25更新:已获得作者授权引用他的文章内容,感谢@FlashYo🌚 那么……我这篇博文是写什么的呢? 自己的收获 那当然是写自己的收获啦!(说了那么多也该进入正文了嗯。。。) ","date":"2018-08-23","objectID":"/post/90d0bd8/:0:0","tags":["实用","CET","python","学习"],"title":"暴力查询四六级准考证号——真的很暴力","uri":"/post/90d0bd8/"},{"categories":["python"],"content":"其实我对原程序做了点改进🌚 在说我改进了什么之前,先给大家科普一下四六级准考证号的组成吧! ","date":"2018-08-23","objectID":"/post/90d0bd8/:1:0","tags":["实用","CET","python","学习"],"title":"暴力查询四六级准考证号——真的很暴力","uri":"/post/90d0bd8/"},{"categories":["python"],"content":"四六级准考证号的组成元素 这东西百度谷歌随便都查的到,一张图带你了解英语六级准考证号组成~ CET-ID\" CET-ID 六级准考证号一共由15位组成: 前5位是学校代码 第6位是学校的校区代码 大学英语四六级考试院校考点代码大全(前6位) 第7-8位是考试年份 如2014年考试此处应为14,14代表2014年。 第9位是该年中第几次四六级考试 上半年是第1次,下半年是第2次,那么2014年12月就是2。 第10位是四六级类别,四级是1,六级是2。 第11-13位是考场号 比如你在100考场的,那就是100,14考场的就是014。 第14-15位是座位号 如果你是01号座位,那就是01,29号就是29 。 学校代码和校区代码查不到的直接随便问一个自己学校的考四六级的同学问一下就好了,那么前6位已经得到了;7~10位就不用我说了吧,自己是什么时候考的什么试对应就好了。前10位确定好了,所以接下来不知道的其实就只剩下考场号和座位号了 ","date":"2018-08-23","objectID":"/post/90d0bd8/:1:1","tags":["实用","CET","python","学习"],"title":"暴力查询四六级准考证号——真的很暴力","uri":"/post/90d0bd8/"},{"categories":["python"],"content":"改进1: 支持自定义试室范围和座位号范围(2018年2月…忘了具体啥时候了哈哈哈) 其实一开始的想法是自定义座位号范围,因为我想到很多学校一间试室都不会坐99个人。原程序的座位号是从 1 跑到 99 的,没法自定义范围,这样的话就会浪费很多时间去查询很多不存在的准考证号。 于是我做了以下改进: 既然要自定义座位号范围,那么就要把试室号和座位号拆开了,于是我顺便把自定义试室范围也搞了,这个很简单,加个for循环以及改下函数参数就好了。 原代码: myid = \"你的准考证号前10位{id:05d}\" ... for num in range(1, 10001): query_text =send_query_until_true(num) ... 改进后: myid = \"你的准考证号前10位{room_id:03d}{seat_id:02d}\" ... # 试室范围:1~999,试室人数 1~40 for room_num in range(1, 999): for seat_num in range(1, 40): query_text = send_query_until_true(room_num, seat_num) ... 记得把send_query_until_true()函数的参数也修改成room_num, seat_num ","date":"2018-08-23","objectID":"/post/90d0bd8/:1:2","tags":["实用","CET","python","学习"],"title":"暴力查询四六级准考证号——真的很暴力","uri":"/post/90d0bd8/"},{"categories":["python"],"content":"改进2: 让程序遇到 Error 不中断执行(2018-08-22 更新) @FlashYo在他的文章最后讲过,程序停止了不是报错就是查到了,我当时好奇会什么报错咧? 跑了一段时间后终于中断了,看了一下终端信息,原来是requests模块抛出的异常。后面又试了好几次中断,然后跑自己的准考证号时遇上了网络高峰期,尤其是这渣网络,简直疯狂断线,不带重连那种😂。终于忍无可忍,卷起袖子就开撸! (咳~不能那么粗鲁,要文明🌚(逃~ 直接上代码: ... # 2018-08-22 更新 捕捉网络异常实现程序遇到 Error 不中断执行 while True: # 一直循环,直到访问站点成功 try: # 以下except都是用来捕获当requests请求出现异常时, # 通过捕获然后等待网络情况的变化,以此来保护程序的不间断运行 # 此处写requests请求 ... ...查询准考证的函数`send_query_until_true(...)`的内容... ... except requests.exceptions.ConnectionError: log_info('Unfortunitely -- ConnectionError Happened, please wait 3 seconds') time.sleep(3) except requests.exceptions.ChunkedEncodingError: log_info('Unfortunitely -- ChunkedEncodingError Happened, please wait 3 seconds') time.sleep(3) except: log_info('Unfortunitely -- An Unknow Error Happened, Please wait 3 seconds') time.sleep(3) Umm…我一口气把所有异常都捕捉了哈哈哈!!!这个代码也很简单,捕捉到异常就等待3秒,然后尝试重新跑 上一个 准考证号,有种断线重连的效果。 log_info(*arg)是我自定义的一个函数,用于在终端输出信息的同时把信息存到文件中,方便后期查看(好吧,其实是避免电脑突然蓝屏或者什么原因重启导致的终端记录丢失,那我又要重新跑,多亏哦!)。 ","date":"2018-08-23","objectID":"/post/90d0bd8/:1:3","tags":["实用","CET","python","学习"],"title":"暴力查询四六级准考证号——真的很暴力","uri":"/post/90d0bd8/"},{"categories":["python"],"content":"真丶写收获 时间问题,先在手机便签写着,写完马上补上,大家不要太期待哈哈哈🌚 战果 完成这些之后,终于能 全自动 跑了!!全自动!!! Oh, Yeah!妈妈再也不用担心我的程序突然中断了!! 昨天晚上,噢不,今天凌晨😂3点多,帮我的朋友跑出了她的准考证号,跑了好久哦,好几个小时,还好全自动🌚。。。 嗯…顺便在这里再次恭喜一下你过了六级吧👍👏 @念念 ,被我吊了一手胃口刺激不🌚? (不要问我为什么不查自己的,因为我还没考哈哈哈!) ","date":"2018-08-23","objectID":"/post/90d0bd8/:2:0","tags":["实用","CET","python","学习"],"title":"暴力查询四六级准考证号——真的很暴力","uri":"/post/90d0bd8/"},{"categories":["Web前端"],"content":"背景 我在前面写博文的时候,有遇到一次需要嵌套别人的网页来做预览,我想到了用iframe标签,结果发现根本没法自适应高度??!!什么玩意,强迫症的我又走上了填坑之路。对,你没看错,又。。。。 iframe自适应高度 参考:完美解决Iframe高度自适应(兼容性好并且支持跨域) 经过一番搜索,终于找到了自适应高度的方法。 具体代码如下: \u003cscript type=\"text/javascript\"\u003e function SetCwinHeight(){ var iframeid=document.getElementById(\"iframeid\"); //iframe id if (document.getElementById){ if (iframeid \u0026\u0026 !window.opera){ if (iframeid.contentDocument \u0026\u0026 iframeid.contentDocument.body.offsetHeight){ iframeid.height = iframeid.contentDocument.body.offsetHeight; }else if(iframeid.Document \u0026\u0026 iframeid.Document.body.scrollHeight){ iframeid.height = iframeid.Document.body.scrollHeight; } } } } \u003c/script\u003e \u003ciframe width=\"100%\" id=\"iframeid\" onload=\"Javascript:SetCwinHeight()\" height=\"1\" frameborder=\"0\" src=\"www.xxx.com\"\u003e\u003c/iframe\u003e 然后。。。然后。。。没有然后,一点反应都没有??!!什么东西??我直觉告诉我很快就能搞定了,于是我继续填(wa)坑。 解决跨域问题 原来。。。是 js 的跨域问题在搞鬼(好吧,我承认我主学的不是前端😂) 跨域的解决方法有很多,我用的是document.domain+Nginx域名转发的方法,比较偷懒嗯😂。 这种方法只适用于在自己博客上插入iframe的情况,而且需要博客在自己的服务器上和拥有自己的域名。 废话不多说,我先说一下Nginx域名转发。 ","date":"2018-08-21","objectID":"/post/a29672a0/:0:0","tags":["Web前端","iframe"],"title":"解决iframe跨域高度自适应问题","uri":"/post/a29672a0/"},{"categories":["Web前端"],"content":"Nginx域名转发 20180823更新:本节内容已转移到Nginx域名转发(反向代理) ","date":"2018-08-21","objectID":"/post/a29672a0/:1:0","tags":["Web前端","iframe"],"title":"解决iframe跨域高度自适应问题","uri":"/post/a29672a0/"},{"categories":["Web前端"],"content":"document.domain 参考: 小tip:iframe高度动态自适应 新手学跨域之iframe 把 iframe 中src的网址转发到自己的域名后就可以用document.domain解决跨域问题啦! document.domain是比较常用的跨域方法。实现最简单但只能用于同一个主域下不同子域之间的跨域请求,比如 foo.com和 img.foo.com 之间,img1.foo.com 和 img2.foo.com 之间。只要把两个页面的document.domain都指向主域就可以了,比如document.domain='foo.com';。 设置好后父页面和子页面就可以像同一个域下两个页面之间访问了。父页面通过ifr.contentWindow就可以访问子页面的window,子页面通过parent.window或parent访问父页面的window,接下来可以进一步获取dom和js。 \u003c!-- foo.com/a.html --\u003e \u003ciframe id=\"ifr\" src=\"http://img.foo.com/b.html\"\u003e\u003c/iframe\u003e \u003cscript\u003e document.domain = 'foo.com'; function aa(str) { console.log(str); } window.onload = function () { document.querySelector('#ifr').contentWindow.bb('aaa'); } \u003c/script\u003e \u003c!-- img.foo.com/b.html --\u003e \u003cscript\u003e document.domain = 'foo.com'; function bb(str) { console.log(str); } parent.aa('bbb'); \u003c/script\u003e 总结 下面是我修改后的代码: iframe_auto_height.js function SetCwinHeight(id) { document.domain='www.abc.com' var iframeid = document.getElementById(id); //iframe id if (document.getElementById) { if (iframeid \u0026\u0026 !window.opera) { console.log(\"iframeid:\" + iframeid.contentDocument); if (iframeid.contentDocument \u0026\u0026 iframeid.contentDocument.body.offsetHeight) { iframeid.height = iframeid.contentDocument.body.offsetHeight + 50; } else if (iframeid.Document \u0026\u0026 iframeid.Document.body.scrollHeight) { frameid.height = iframeid.Document.body.scrollHeight + 50; } } } } xxx.md \u003cscript type=\"text/javascript\" src=\"../js/iframe_auto_height.js\"\u003e\u003c/script\u003e \u003ciframe width=\"100%\" id=\"iframe_id\" name=\"iframe_id\" onload=\"Javascript:SetCwinHeight(this.id)\" scrolling=\"auto\" height=\"0\" frameborder=\"0\" src=\"https://www.abc.com/tools/a/\"\u003e \u003c/iframe\u003e 特别说明: 在这里我说一下为什么要分开写,因为我发现不分开写没法正常加载js😂。 像下面这种: function SetCwinHeight(id) { document.domain='www.abc.com' var iframeid = document.getElementById(id); //iframe id if (document.getElementById) { if (iframeid \u0026\u0026 !window.opera) { console.log(\"iframeid:\" + iframeid.contentDocument); if (iframeid.contentDocument \u0026\u0026 iframeid.contentDocument.body.offsetHeight) { iframeid.height = iframeid.contentDocument.body.offsetHeight + 50; } else if (iframeid.Document \u0026\u0026 iframeid.Document.body.scrollHeight) { frameid.height = iframeid.Document.body.scrollHeight + 50; } } } } ``` 合起来写然后插入到 Markdown 里面貌似 hexo 渲染后 js 总有问题,不知是不是主题原因,我看过有一个 Next 主题的加载正常,蒙逼。。 出现的问题如下图: cross-domain-erro\" cross-domain-erro 相信大家都看出来了,js代码乱掉了😂. 我自己也找过一些资料,其中不乏试过[https://www.v2ex.com/t/162951#r_1717560](https://www.v2ex.com/t/162951#r_1717560)说的使用[Raw标签插件](https://hexo.io/zh-cn/docs/tag-plugins.html#Raw),但是没有用。。。 如果有人知道是什么原因的话,请告诉我一声谢谢!(在下方评论即可,PM我也可以) ","date":"2018-08-21","objectID":"/post/a29672a0/:2:0","tags":["Web前端","iframe"],"title":"解决iframe跨域高度自适应问题","uri":"/post/a29672a0/"},{"categories":["Markdown"],"content":"背景 今天在转载几篇文章的时候,看到别人的博文写的不错,想要转载到自己博客里。 但是由于我们是自己搭建的博客,发表的文章都是以源码方式编辑的,没法一键转载。 于是我就想到复制粘贴的时候能不能自动转换粘贴内容为Markdown呢? (其实还是hexo 博客神速转载这篇文章给我的启发) 一开始我是在试用为知的一个插件预览Wiz.Editor.md 预览地址:http://akof1314.github.io/Wiz.Editor.md/ 后来发现用着用着感觉有点问题,觉得很不爽,弃用。 紧接着我脑子一热就想自己写一个。。。(好吧,最近项目比较多,这个得搁置一段时间了) 于是去github上找了一下,不负所望,这类工具还是有的! 介绍 这几个工具基本都有在线预览,使用的时候直接预览就好了,如果自己需要整合源码进自己项目的话,就去下源码吧! ","date":"2018-08-21","objectID":"/post/48ffa897/:0:0","tags":["Markdown"],"title":"推荐几个能自动转换粘贴内容为Markdown的工具","uri":"/post/48ffa897/"},{"categories":["Markdown"],"content":"2MD 推荐指数:☆☆☆☆☆ 源码:https://github.com/phodal/2md 原文:http://www.sohu.com/a/134631771_385076 \u0017预览地址:https://phodal.github.io/2md/ 我加了个iframe提供给大家直接预览: 20180822更新: 注意!!!手机版目测无法正常使用,具体表现为无法粘贴,若想有完美体验,请转移到电脑端使用。不过,我只在我的魅族MX4 Pro上测试过(穷孩子没其他手机了😂),如果有机型成功粘贴了,可在下方评论告诉我😂 天哪!终于解决跨域自适应高度问题了😂 ","date":"2018-08-21","objectID":"/post/48ffa897/:1:0","tags":["Markdown"],"title":"推荐几个能自动转换粘贴内容为Markdown的工具","uri":"/post/48ffa897/"},{"categories":["Markdown"],"content":"clipboard2markdown 推荐指数:☆☆☆☆(因为操作没有2MD和界面好,所以就低一星) 源码:https://github.com/euangoddard/clipboard2markdown 预览地址:http://euangoddard.github.io/clipboard2markdown/ 20180822更新: 注意!!!和上面的情况一样,手机版目测无法正常使用,具体表现为无法粘贴,若想有完美体验,请转移到电脑端使用。不过,我只在我的魅族MX4 Pro上测试过(穷孩子没其他手机了😂),如果有机型成功粘贴了,可在下方评论告诉我😂 clipboard2markdown-preview\" clipboard2markdown-preview 咳~这个…因为预览地址是http的,iframe在Chrome会直接空白,所以换上预览图。。。。点击预览图可直接跳转到预览页面哦🌚! ","date":"2018-08-21","objectID":"/post/48ffa897/:2:0","tags":["Markdown"],"title":"推荐几个能自动转换粘贴内容为Markdown的工具","uri":"/post/48ffa897/"},{"categories":["Markdown"],"content":"paste-as-markdown 推荐指数:Umm…没有预览我懒得下了,没有指数没有预览哈哈哈! 源码:https://github.com/letiantian/paste-as-markdown 原文:https://my.oschina.net/letiantian/blog/754905 ","date":"2018-08-21","objectID":"/post/48ffa897/:3:0","tags":["Markdown"],"title":"推荐几个能自动转换粘贴内容为Markdown的工具","uri":"/post/48ffa897/"},{"categories":["运维日志"],"content":" 参考: 【Nginx】关于域名转发proxy_pass 另一种利用Nginx反向代理来简单镜像 HTTP(S)网站 的方法 ","date":"2018-08-21","objectID":"/post/357640ff/:0:0","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"背景 在开发过程中,有时候我们会有一个这样的需求:访问m.XXX.com的时候,需要实际访问www.YYY.com/m,并且域名不能发生变化(这种也可称为镜像?)。 达成这个需求可以使用Nginx,有两种做法: 第一种就是301跳转,使用rewrite来跳转域名,不过这样域名就会发生变化,与需求不符。 第二种就是用proxy_pass跳转,只要指定跳转目的域名,就可以在访问的时候自动跳转访问目的域名,而且域名也不会发生变化。所以这里需要使用第二种方法。 ","date":"2018-08-21","objectID":"/post/357640ff/:1:0","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"配置文件示例 以下教程 域名以m.XXX.com为例,被镜像网站以www.YYY.com/m为例 然后访问你的域名看一看是否成功镜像,需要注意的一点是,如果被镜像的网站设置了防盗链,那么静态文件(js/css/图片)可能无法显示,这就没办法了。 ","date":"2018-08-21","objectID":"/post/357640ff/:2:0","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"参数解释 一般情况下只需要更改这几个参数。 server_name 你的域名; sub_filter 欲被镜像的域名 你的域名; proxy_set_header Referer http://欲被镜像的域名; proxy_set_header Host 欲被镜像的域名; proxy_pass http://欲被镜像的域名; ","date":"2018-08-21","objectID":"/post/357640ff/:2:1","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"建立 Nginx 配置文件 首先 在/etc/nginx/sites-available/建立一个m.XXX.com.conf配置文件,内容参考HTTP 示例和HTTPS 示例。 ","date":"2018-08-21","objectID":"/post/357640ff/:2:2","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"HTTP 示例 以下示例是以 m.XXX.com 镜像 www.YYY.com/m 为例。自行替换 其中的参数: 第二段是 屏蔽搜索引擎收录,比如镜像自己的网站,如果不屏蔽会导致 收录流失。 注意:不管你是镜像 www.baidu.com 还是 www.google.com.hk (不要直接使用 .com 会被谷歌自动根据VPS所在地区重定向的),他们两个目前都是强制重定向到 https ,这意味着如果你只配置了 http 反向代理,那么访问反向代理域名后会重定向到 https 的目标域名,所以你也必须配置 https 才行。 # 下面这段代码才是 HTTP 完整示例配置文件,注意使用时修改里面的默认域名等信息。 server { listen 80; server_name m.XXX.com; if ($http_user_agent ~* (baiduspider|360spider|haosouspider|googlebot|soso|bing|sogou|yahoo|sohu-search|yodao|YoudaoBot|robozilla|msnbot|MJ12bot|NHN|Twiceler)) { return 403; } location / { sub_filter www.YYY.com m.XXX.com; sub_filter_once off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Referer http://www.YYY.com; proxy_set_header Host www.YYY.com; proxy_pass http://www.YYY.com/m; proxy_set_header Accept-Encoding \"\"; } } ","date":"2018-08-21","objectID":"/post/357640ff/:2:3","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"HTTPS 示例 当你要镜像的网站不开放 HTTP 或者 强制HTTPS 的时候,你就需要加上 SSL 来转成 HTTPS 了。 假设SSL证书文件位置是:/root/ssl.crt 假设SSL密匙文件位置是:/root/ssl.key 第二段的 301 代码是,强制走HTTPS,如果不需要可以去掉。 第三段是 屏蔽搜索引擎收录,比如镜像自己的网站,如果不屏蔽会导致 收录流失。 同时下面这两个选项的记得把http://改成https://。 proxy_set_header Referer https://www.YYY.com; proxy_pass https://www.YYY.com/m; # 下面这段代码才是 HTTPS 完整示例配置文件,注意使用时修改里面的默认域名等信息。 server { listen 80; listen 443 ssl; ssl on; ssl_certificate /root/ssl.crt; ssl_certificate_key /root/ssl.key; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; server_name m.XXX.com; add_header Strict-Transport-Security \"max-age=31536000\"; if ( $scheme = http ){ return 301 https://$server_name$request_uri; } if ($http_user_agent ~* (baiduspider|360spider|haosouspider|googlebot|soso|bing|sogou|yahoo|sohu-search|yodao|YoudaoBot|robozilla|msnbot|MJ12bot|NHN|Twiceler)) { return 403; } location / { sub_filter www.YYY.com m.XXX.com; sub_filter_once off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Referer http://www.YYY.com; proxy_set_header Host www.YYY.com; proxy_pass http://www.YYY.com/m; proxy_set_header Accept-Encoding \"\"; } } ","date":"2018-08-21","objectID":"/post/357640ff/:2:4","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["运维日志"],"content":"注意事项 若是转发的地址含二级目录如:www.YYY.com/m,只需要把proxy_pass值修改为具体地址即可,若是不行则将proxy_set_header值也修改为具体地址。 若是你输入的地址中要有二级目录如m.XXX.com/tools/a,则location模块要改为: location /tools/a { // ... } ","date":"2018-08-21","objectID":"/post/357640ff/:2:5","tags":["Nginx","服务器","运维"],"title":"Nginx域名转发(反向代理)","uri":"/post/357640ff/"},{"categories":["hexo"],"content":"背景 Disqus 不用说,大家都知道他的主域名disqus.com在国内被屏蔽了,导致在国内不能评论也不能看评论,体验十分不好。看了一下 Disqus 官方发现有个 API 反代加速访问的用法,于是去找了相关资料,原本想自己造轮子,后来发现已经有现成的,我就先拿来用着吧。 原理 我们可以来看看disqus反向代理的原理图: disqus-api-diagra\" disqus-api-diagra 整体流程是这样的,在前端页面上测试disqus加载是否成功,如果成功则显示disqus的评论框,反之加载独立的评论框,并将请求发送给自己在国外的vps,利用vps做反向代理,接收来自客户端的请求到disqus服务器并再转发给客户端。 一句话概括就是:客户端发送请求给服务器,服务器通过Disqus Api提交评论。 工具介绍 目前现成的disqus反代主要有以下两种: disqus-proxy –NodeJS实现 disqus-proxy\" disqus-proxy disqus-php-api –PHP实现 disqus-php-api\" disqus-php-api 上述两种方案均支持根据网络情况判断加载简易/原版评论框,看完UI后我很果断的选择了disqus-php-api…… 以下为替换为disqus-php-api的具体步骤,供大家参考,也给自己留下一个记录。 配置 一台国外的VPS服务器 ","date":"2018-08-03","objectID":"/post/d32fb564/:0:0","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"后端配置 ","date":"2018-08-03","objectID":"/post/d32fb564/:1:0","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"获取 disqus-php-api git clone https://github.com/fooleap/disqus-php-api.git ","date":"2018-08-03","objectID":"/post/d32fb564/:1:1","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"移动并重命名 mv -r disqus-php-api /path/to/disqus-php-api 注意: 此处建议新建一个 Nginx 站点,如:api.xxx.com这类,专门放 API 的地方,注意DNS解析添加A记录和写好 Nginx 配置,尤其是要配置SSL证书。 ","date":"2018-08-03","objectID":"/post/d32fb564/:1:2","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"修改 config.php 根据 disqus-php-api/api/config.php 中的注释修改相应配置: define('DISQUS_PUBKEY', 'E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F'); define('PUBLIC_KEY', 'your-public-key'); define('SECRET_KEY', 'your-secret-key'); define('DISQUS_USERNAME', 'your-disqus-username'); define('DISQUS_EMAIL', '[email protected]'); define('DISQUS_PASSWORD', 'your-disqus-password'); define('DISQUS_WEBSITE', 'https:/\\/your-website.com'); // 实际使用时要去掉链接的 \\ ,这里是为了避免md直接吧双斜杠解析成注释了 define('DISQUS_SHORTNAME', 'your-disqus-shortname'); define('DISQUS_APPROVED', true); 这里要注意的是,DISQUS_WEBSITE选项中,域名不能这样写:‘https://reb.mallotec.com/',域名后不能以'/‘结尾,否则会出现一直创建Thread的情况 ","date":"2018-08-03","objectID":"/post/d32fb564/:1:3","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"前端配置 ","date":"2018-08-03","objectID":"/post/d32fb564/:2:0","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"disqus-php-api集成至hexo 修改 hexo 的主题配置文件 在主题配置文件_config.yml中添加 disqusapi 相关参数 disqusapi: forum: 'your-short-name' site: 'https://your-site' api: 'https://your-site/.../disqus-php-api/api' mode: 1 badge: '博主' timeout: 3000 参数参考文档:https://github.com/fooleap/disqus-php-api/blob/master/readme.md forum:Disqus form的shortname site:网站域名 api:PHP代码部署的网址:https://yoursite.com/disqus/api mode: 1 检测能否访问 Disqus,若能则加载 Disqus 原生评论框,超时则加载简易评论框 2 仅加载简易评论框 3 同时加载两种评论框,先显示简易评论框,Disqus 加载完成则切换至 Disqus 评论框 badge:管理员徽章文本 timeout:当mode为1时的超时时间 ","date":"2018-08-03","objectID":"/post/d32fb564/:2:1","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"添加 disqusapi 模块 Material主题的 disqusapi 模块添加可以参考官方文档的评论系统适配指南。 直接拷贝一份官方提供的 disqus 评论模块到\u003c主题根目录\u003e/layout/_widget/comment/disqusapi/下。下面是common.ejs、enter.ejs、main.ejs的具体代码: common.ejs \u003c!-- 使用 DISQUS js 代码 --\u003e common.ejs为空 enter.ejs \u003c!-- 使用 DISQUS --\u003e \u003c% if(page.comments) {%\u003e \u003clink rel=\"stylesheet\" href=\"/disqus-php-api-dist/iDisqus.min.css\" /\u003e \u003cscript src=\"/disqus-php-api-dist/iDisqus.min.js\"\u003e\u003c/script\u003e \u003cdiv id=\"comment\"\u003e \u003c%- partial('_widget/comment/' + theme.comment.use + '/main') %\u003e \u003c/div\u003e \u003cstyle\u003e #comment{ background-color: #eee; padding: 2pc; } \u003c/style\u003e \u003c% } %\u003e page.comments是为了控制每一篇文章是否需要开启评论功能,需要在每一篇文章的Front-matter中设置comments={Boolean} 如果服务器很远,连接比较慢,建议把iDisqus.min.css和iDisqus.min.js放到主题目录的source里会更方便,渲染调试的时候直接从本地加载,不然调试的时候从网络加载半年网页都没加载完。当然,放到了CDN那更好,此处主要是为了方便hexo s调试。 main.ejs \u003cscript\u003e var emojiList = [{ code:'smile', title:'笑脸', unicode:'1f604' },{ code:'mask', title:'生病', unicode:'1f637' },{ code:'joy', title:'破涕为笑', unicode:'1f602' },{ code:'stuck_out_tongue_closed_eyes', title:'吐舌', unicode:'1f61d' },{ code:'flushed', title:'脸红', unicode:'1f633' },{ code:'scream', title:'恐惧', unicode:'1f631' },{ code:'pensive', title:'失望', unicode:'1f614' },{ code:'unamused', title:'无语', unicode:'1f612' },{ code:'grin', title:'露齿笑', unicode:'1f601' },{ code:'heart_eyes', title:'色', unicode:'1f60d' },{ code:'sweat', title:'汗', unicode:'1f613' },{ code:'smirk', title:'得意', unicode:'1f60f' }]; var disq = new iDisqus('comment', { forum: '\u003c%= theme.disqusapi.forum %\u003e', site: '\u003c%= theme.disqusapi.site %\u003e', api: '\u003c%= theme.disqusapi.api %\u003e', mode: '\u003c%= theme.disqusapi.mode %\u003e', badge: '\u003c%= theme.disqusapi.badge %\u003e', timeout: '\u003c%= theme.disqusapi.timeout %\u003e', init: true, emoji_list: emojiList }); disq.count(); \u003c/script\u003e 最后编辑主题配置文件中的comment模块开启disqusapi: comment: use: disqusapi 问题 ","date":"2018-08-03","objectID":"/post/d32fb564/:2:2","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"1.没有权限 参考: https://blog.fooleap.org/disqus-php-api.html#comment-3954303091 Linux查看、修改文件读写权限 问题如图: disqus-php-api-no-permission\" disqus-php-api-no-permission 据作者 fooleap 所说,显示没有权限的话,就是没有权限在当前目录创建文件夹。 于是我修改了 api 所在目录的权限就好了: sudo chown -R $USER:$USER path/to/disqus-php-api sudo chmod -R 777 path/to/disqus-php-api ","date":"2018-08-03","objectID":"/post/d32fb564/:3:0","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":["hexo"],"content":"2.由于disqus没有本页面的项管thread,故需先创建thread 参考:https://blog.fooleap.org/disqus-php-api.html#comment-3829428059 据作者 fooleap 所说,加载评论框的页面,若在 Disqus 没有相关的 thread,是必须先手动创建的,不设置成自动是避免生成无用的 thread。若想自动创建可以在配置参数里添加autoCreate: true。 还有一种解决方法**(推荐)**: 如果出现创建thread的问题,那么可以通过 mode选择1,再翻墙访问你的文章页面,这时候加载原生Disqus评论系统,thread就创建成功了。 **特别要注意的是:**如果你的文章标题中带有中文,会无法创建thread,所以不要使用中文命名文章。 参考链接 解决Hexo博客中 Disqus 在国内不能访问的方案 Hexo折腾记之科学使用Disqus与Next的集成 disqus-php-api 科学评论 科学使用 Disqus 为Hexo主题fexo添加disqus反代 〖原创〗基于disqus-php-api在Hexo博客中使用Disqus ","date":"2018-08-03","objectID":"/post/d32fb564/:4:0","tags":["hexo","Disqus"],"title":"Disqus API科学评论大法","uri":"/post/d32fb564/"},{"categories":null,"content":"此页面无实质内容,用于博客添加新功能时测试使用! 博客开发日志: 2018-08-05 博客加入 hexo-lazyload-image 插件,实现延迟加载图像 2018-08-05 部分文章加入 hexo-blog-encrypt 插件进行加密 2018-08-04 使用七牛云作为博客图床(广告插件会拦截七牛云资源导致报错Failed to load resource: net::ERR_BLOCKED_BY_CLIENT,而且默认 http 会导致浏览器提示网络不安全) 2018-08-03 Disqus 评论添加国内使用 Disqus API 方式 –修改Md主题源码 2018-07-27 使用 Disqus 延迟加载 –修改Md主题源码 2018-07-26 添加 Disqus 评论 2018-07-23 文章结尾添加版权声明 –修改Md主题源码 2018-07-22 修复插件hexo-prism-plugin实现代码高亮导致的行号不显示问题 –修改Md主题源码 2018-07-21 主题由 Next 切换为 Material 2018-07-21 博客启用https 2018-07-15 开始搭建 Hexo 博客 ","date":"2018-08-03","objectID":"/post/b84ba97/:0:0","tags":null,"title":"测试专页","uri":"/post/b84ba97/"},{"categories":["hexo"],"content":"背景 国内的评论系统畅言都需要ICP备案,像我这种打死不备案的人来说,国内的评论系统就用不了了。最终决定博客使用的是国外的 Disqus 评论系统,但他的主域名disqus.com在国内被屏蔽了,只能科学上网后使用。 果然有空还是自己造轮子爽一点,逃~ 配置 Disqus Disqus官网:https://disqus.com 打开链接后, 可以直接用Facebook,Twitter以及 Google 登录,也可以用邮箱注册后登录。 ","date":"2018-08-03","objectID":"/post/2cb4e595/:0:0","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["hexo"],"content":"选择创建目的 登录后,点击首页的GET STARTED 按钮,然后点击图示按钮: disqus-step1-intent\" disqus-step1-intent ","date":"2018-08-03","objectID":"/post/2cb4e595/:1:0","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["hexo"],"content":"创建一个新站点 disqus-step2-newsite\" disqus-step2-newsite Website Name 填的是shortname,到时候要填到主题配置文件中 Category 选择种类,我选的是 Tech ","date":"2018-08-03","objectID":"/post/2cb4e595/:2:0","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["hexo"],"content":"安装 ","date":"2018-08-03","objectID":"/post/2cb4e595/:3:0","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["hexo"],"content":"Select Plan 接下来你会看到以下页面: disqus-step3-choose-plan\" disqus-step3-choose-plan 这一步不需要选计划,跳过Select Plan,直接到Install Disqus。 ","date":"2018-08-03","objectID":"/post/2cb4e595/:3:1","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["hexo"],"content":"Install Disqus Select Platform disqus-step4-choose-platform\" disqus-step4-choose-platform Hexo 不属于以上任何形式,因此选择Universal Code(通用代码)。 Install Instructions disqus-step5-universalcode-instal\" disqus-step5-universalcode-instal 然后拉到最下面选择Configure ","date":"2018-08-03","objectID":"/post/2cb4e595/:3:2","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["hexo"],"content":"Configure Disqus disqus-step6-configure\" disqus-step6-configure Website Name 一般就刚刚第一步填的short name(我刚刚填了 justtestme,到这步的时候一般会自动填充) Website URL 填写你的博客地址 填完信息后点击Complete Setup完成配置。 配置 Hexo 在主题配置文件中找到comment项: (以 Material 主题为例) # Comment Systems # Available value of \"use\": # disqus | disqus_click | changyan | 163gentie comment: use: disqus_click shortname: justtestme # duoshuo or disqus shortname use:选择disqus或disqus_click,两者差别请参阅Material主题文档 shortname:填写你刚刚在 Disqus 设置的 Website Name,英文冒号后空格。(严格意义上是填站点配置面板中的 Shortname ) 部署应用 使用如下命令提交到服务器: hexo clean hexo g hexo d 科学上网后,任意打开一篇你的博文就能评论了。 总结 在https://justtestme.disqus.com/admin/settings/general/页面管理 Your Sites( justtestme 换成自己的 shortname )。 此处的 Shortname 和 Websit Name 是可以不同的。如下图: disqus-configure-differentname-short-website\" disqus-configure-differentname-short-website 注意:主题配置文件中comment项填的是此处的 Shortname 而不是 Website Name!! 参考 Hexo搭建博客系列:(六)Hexo添加Disqus评论 ","date":"2018-08-03","objectID":"/post/2cb4e595/:3:3","tags":["hexo","Disqus"],"title":"Hexo添加Disqus评论","uri":"/post/2cb4e595/"},{"categories":["数据库"],"content":" 参考:https://www.jianshu.com/p/f1a5a680b464 ☆☆20180801更新:通过测试发现直接使用brew services start [email protected]即可切换至5.6版本使用,不需要brew unlink,是否可用请自行测试,测试前请备份好数据!!☆☆ ","date":"2018-07-27","objectID":"/post/a96dbfbe/:0:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac使用brew安装的MySQL版本切换","uri":"/post/a96dbfbe/"},{"categories":["数据库"],"content":"背景 今天因为要写JavaEE大作业运行了一遍队友发过来的项目,却无法正常运行。经过一番追踪,发现是MySQL版本不对的问题,项目使用的MySQL版本是5.6(从使用的JDBC驱动就可以看出来)。本来想着更新JDBC驱动就可以解决,却发现Hibernate也要更新,还不清楚Hibernate支不支持8.0的MySQL,想了一下还是决定装多一个版本的MySQL吧。 ","date":"2018-07-27","objectID":"/post/a96dbfbe/:1:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac使用brew安装的MySQL版本切换","uri":"/post/a96dbfbe/"},{"categories":["数据库"],"content":"切换重点 /usr/local/var/mysql/通过切换每个版本来使用数据库设置和数据 使用brew unlink和brew link切换活动版本的MySQL ","date":"2018-07-27","objectID":"/post/a96dbfbe/:2:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac使用brew安装的MySQL版本切换","uri":"/post/a96dbfbe/"},{"categories":["数据库"],"content":"教程摘要 下面我们来看一下大概流程: 停止运行MySQL 在设置和数据目录(usr/local/var/mysql)之间切换 brew unlink并brew link与切换活动的MySQL 启动MySQL ","date":"2018-07-27","objectID":"/post/a96dbfbe/:3:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac使用brew安装的MySQL版本切换","uri":"/post/a96dbfbe/"},{"categories":["数据库"],"content":"详细步骤 本例以已安装MySQL 8.0再安装5.6 为例 1. 备份/usr/local/var/mysql/ 由于存储了设置和DB数据,因此应该备份好,然后撤离。 mv /usr/local/var/mysql /usr/local/var/mysql_80 2. 删除MySQL 8.0的符号链接 brew unlink mysql 解释: 这个符号链接 指的是诸如 /usr/local/bin/mysql -\u003e ../Cellar/mysql/5.7.10/bin/mysql 和 /usr/local/lib/libmysqlclient.20.dylib -\u003e ../Cellar/mysql/5.7.10/lib/libmysqlclient.20.dylib 之类的东西。 注意: 如果此符号链接存在,安装另一个版本将可能导致以下错误: $ brew install mysql56 ==\u003e Reinstalling homebrew/versions/mysql56 ==\u003e Downloading https://homebrew.bintray.com/bottles-versions/mysql56-5.6.27.el_capitan.bottle.tar.gz Already downloaded: /Library/Caches/Homebrew/mysql56-5.6.27.el_capitan.bottle.tar.gz ==\u003e Pouring mysql56-5.6.27.el_capitan.bottle.tar.gz Error: The `brew link` step did not complete successfully The formula built, but is not symlinked into /usr/local Could not symlink bin/innochecksum Target /usr/local/bin/innochecksum is a symlink belonging to mysql. You can unlink it: brew unlink mysql To force the link and overwrite all conflicting files: brew link --overwrite mysql56 To list all files that would be deleted: brew link --overwrite --dry-run mysql56 或: $ brew install mysql56 Updating Homebrew... ==\u003e Auto-updated Homebrew! Updated 1 tap (homebrew/cask). No changes to formulae. ==\u003e Downloading https://homebrew.bintray.com/bottles/[email protected] ######################################################################## 100.0% ==\u003e Pouring [email protected] ==\u003e /usr/local/Cellar/[email protected]/5.6.40/bin/mysql_install_db --verbose --user=xxx --basedir=/usr/local/Cellar/[email protected]/5.6.40 --datadir=/usr/local/var/mysql --tmpdir=/tmp ==\u003e Caveats A \"/etc/my.cnf\" from another install may interfere with a Homebrew-built server starting up correctly. MySQL is configured to only allow connections from localhost by default To connect: mysql -uroot This formula is keg-only, which means it was not symlinked into /usr/local, because this is an alternate version of another formula. If you need to have this software first in your PATH run: echo 'export PATH=\"/usr/local/opt/[email protected]/bin:$PATH\"' \u003e\u003e ~/.zshrc For compilers to find this software you may need to set: LDFLAGS: -L/usr/local/opt/[email protected]/lib CPPFLAGS: -I/usr/local/opt/[email protected]/include To have launchd start [email protected] now and restart at login: brew services start [email protected] Or, if you don't want/need a background service you can just run: /usr/local/opt/[email protected]/bin/mysql.server start ==\u003e Summary 🍺 /usr/local/Cellar/[email protected]/5.6.40: 340 files, 154.0MB 3. 安装MySQL 5.6 很简单,执行以下命令即可: brew install mysql56 安装后记得导入环境变量: echo 'export PATH=\"/usr/local/opt/[email protected]/bin:$PATH\"' \u003e\u003e ~/.zshrc 附录:Mac通过brew安装MySQL后详细配置过程 4. 分配MySQL 5.6链接 安装MySQL 5.6之后,执行以下命令: brew unlink mysql \u0026\u0026 brew link [email protected] --force **注意:**尽管可以通过执行链接来重新分配链接,但最好首先清除链接,因为发出大量日志很难发现错误。--force参数看具体情况使用,若brew link [email protected]不提示要强制链接的话就不需要该参数了。 5. 启动MySQL 5.6并检查版本 用brew启动 brew services start [email protected] 或 mysql.server start 检查版本: $ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 1 Server version: 5.6.40 Homebrew Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement. mysql\u003e 从日志可以确认MySQL 5.6启动。 如果版本中显示5.7,我们要检查日志等,因为有可能无法替换符号链接。 6. 停止MySQL 5.6服务并保存数据 $ mysql.server stop Shutting down MySQL . SUCCESS! $ mv /usr/local/var/mysql /usr/local/var/mysql_56 7. 恢复最初安装的MySQL 5.7启动 $ mv /usr/local/var/mysql_80 /usr/local/var/mysql $ brew unlink mysql56 \u0026\u0026 brew link mysql Unlinking /usr/local/Cellar/mysql56/5.6.40... 99 symlinks removed Linking /usr/local/Cellar/mysql/8.0.11... 79 symlinks created $ mysql.server start Starting MySQL . SUCCESS! $ mysql -u root Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 2 Server version: 8.0.11 Homebrew ","date":"2018-07-27","objectID":"/post/a96dbfbe/:4:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac使用brew安装的MySQL版本切换","uri":"/post/a96dbfbe/"},{"categories":["数据库"],"content":"疑难解答 /usr/local/var/mysql/为什么有必要切换 如果至少启动一次8.0,如果使用相同的数据运行5.6,将出现以下错误: Starting MySQL . ERROR! The server quit without updating PID file (/usr/local/var/mysql/xxx.local.pid). ``` 由于mysqld等没有动,似乎是权威,但解决不了。 (由于目录结构根据版本不同而不同,可能会影响权限以外的内容) MySQL 5.6和MySQL 5.7(/usr/local/var/mysql/)的初始数据目录结构 # MySQL 5.6 $ ls -l /usr/local/var/mysql -rw-rw---- 1 xxx admin 56 7 26 20:54 auto.cnf -rw-r----- 1 xxx admin 0 7 26 20:50 binlog.index -rw-r----- 1 xxx admin 10369 7 27 18:30 xxx.local.err -rw-rw---- 1 xxx admin 5 7 27 18:30 xxx.local.pid -rw-rw---- 1 xxx admin 50331648 7 27 18:30 ib_logfile0 -rw-rw---- 1 xxx admin 50331648 7 26 20:46 ib_logfile1 -rw-rw---- 1 xxx admin 12582912 7 27 18:30 ibdata1 drwx------ 81 xxx admin 2754 7 26 20:46 mysql drwx------ 55 xxx admin 1870 7 26 20:46 performance_schema # MySQL 8.0 $ ls -l /usr/local/var/mysql_80 -rw-r----- 1 xxx admin 56 7 26 19:44 auto.cnf -rw-r----- 1 xxx admin 178 7 26 19:50 binlog.000001 -rw-r----- 1 xxx admin 11628 7 26 20:42 binlog.000002 -rw-r----- 1 xxx admin 32 7 26 19:51 binlog.index -rw------- 1 xxx admin 1680 7 26 19:44 ca-key.pem -rw-r--r-- 1 xxx admin 1112 7 26 19:44 ca.pem -rw-r----- 1 xxx admin 2783 7 26 20:42 xxx.local.err -rw-r----- 1 xxx admin 6 7 26 19:51 xxx.local.pid -rw-r--r-- 1 xxx admin 1112 7 26 19:44 client-cert.pem -rw------- 1 xxx admin 1676 7 26 19:44 client-key.pem -rw-r----- 1 xxx admin 3346 7 26 20:42 ib_buffer_pool -rw-r----- 1 xxx admin 50331648 7 26 20:42 ib_logfile0 -rw-r----- 1 xxx admin 50331648 7 26 19:44 ib_logfile1 -rw-r----- 1 xxx admin 12582912 7 26 20:42 ibdata1 drwxr-x--- 8 xxx admin 272 7 26 19:44 mysql -rw-r----- 1 xxx admin 25165824 7 26 20:19 mysql.ibd drwxr-x--- 104 xxx admin 3536 7 26 19:44 performance_schema -rw------- 1 xxx admin 1680 7 26 19:44 private_key.pem -rw-r--r-- 1 xxx admin 452 7 26 19:44 public_key.pem -rw-r--r-- 1 xxx admin 1112 7 26 19:44 server-cert.pem -rw------- 1 xxx admin 1676 7 26 19:44 server-key.pem drwxr-x--- 3 xxx admin 102 7 26 19:44 sys -rw-r----- 1 xxx admin 10485760 7 26 20:42 undo_001 -rw-r----- 1 xxx admin 10485760 7 26 20:42 undo_002 ","date":"2018-07-27","objectID":"/post/a96dbfbe/:4:1","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac使用brew安装的MySQL版本切换","uri":"/post/a96dbfbe/"},{"categories":["数据库"],"content":"一、解决 ERROR! The server quit without updating PID file (/usr/local/var/mysql/xxx.local.pid). 参考:https://stackoverflow.com/a/36156848 ","date":"2018-07-27","objectID":"/post/6965849b/:1:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac通过brew安装的MySQL各种无法运行问题总结","uri":"/post/6965849b/"},{"categories":["数据库"],"content":"问题描述 之前用的好好的,经过一次更新(5.6-\u003e8.0)后某一天突然炸了。 通过brew services start mysql启动 MySQL 后,使用 Navicat 连接提示: 2003 - Can't connect to MySQL server on '127.0.0.1' (61 \"Connection refused\") 经过一番查询,据说是 MySQL 服务没启动的问题(其实我觉得这说法有点不对),然后我就尝试启动MySQL服务,命令mysql.server start。 接着我收到以下错误: ERROR! The server quit without updating PID file (/usr/local/var/mysql/xxx.local.pid). xxx为我的机器名字。 然后执行mysql_secure_installation又提示: Securing the MySQL server deployment. Enter password for user root: Error: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) 后来经过各种尝试如: rm /usr/local/var/mysql/*.err rm /usr/local/var/mysql/xxx.local.err find / -name mysql.sock sudo chmod -R 755 /usr/local/var/mysql/ sudo chown -R _mysql:mysql /usr/local/var/mysql ls -laF /usr/local/var/mysql/ sudo rm -f /tmp/mysql.sock.lock ls -laF /tmp 还有通过ps aux | grep mysql找出 MySQL 的 PID,再用kill -9 [PID]杀掉进程都没用。 ","date":"2018-07-27","objectID":"/post/6965849b/:1:1","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac通过brew安装的MySQL各种无法运行问题总结","uri":"/post/6965849b/"},{"categories":["数据库"],"content":"最终解决方法:重装大法 最后尝试完全重新安装 MySQL 终于解决了。 **警告:**这将删除所有数据库,因此请确保先保存转储。 brew remove mysql brew cleanup --force launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist rm ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist sudo rm -rf /usr/local/var/mysql brew install mysql mysqld --initialize --explicit_defaults_for_timestamp mysql.server start # no sudo! 重装了MySQL后建议重新执行一次配置脚本:mysql_secure_installation 注意: 安装后必须执行 mysqld --initialize --explicit_defaults_for_timestamp 否则仍无法解决以下问题: Error: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) ","date":"2018-07-27","objectID":"/post/6965849b/:1:2","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac通过brew安装的MySQL各种无法运行问题总结","uri":"/post/6965849b/"},{"categories":["数据库"],"content":"附录 卸载过程如下(保留以便日后需要): $ brew remove mysql Uninstalling /usr/local/Cellar/mysql/8.0.11... (254 files, 232.9MB) mysql 5.7.21, 5.7.22 2 are still installed. Remove all versions with `brew uninstall --force mysql`. $ brew uninstall --force mysql Uninstalling mysql... (640 files, 467.8MB) $ brew cleanup --force Removing: /usr/local/Cellar/dash/0.5.10... (6 files, 196.2KB) Removing: /usr/local/Cellar/dash/0.5.9.1... (5 files, 199KB) Removing: /usr/local/Cellar/freetds/1.00.89... (2,094 files, 11.9MB) Removing: /usr/local/Cellar/freetds/1.00.91... (2,094 files, 11.9MB) Removing: /usr/local/Cellar/freetype/2.9... (60 files, 2.7MB) Removing: /usr/local/Cellar/gdbm/1.14.1_1... (20 files, 555.8KB) Removing: /usr/local/Cellar/icu4c/61.1... (249 files, 67.2MB) Removing: /usr/local/Cellar/libidn2/2.0.4... (46 files, 580.8KB) Removing: /usr/local/Cellar/libpq/10.3... (2,425 files, 26.8MB) Removing: /usr/local/Cellar/libunistring/0.9.9... (54 files, 4.4MB) Removing: /usr/local/Cellar/nginx/1.13.12... (23 files, 1.4MB) Removing: /usr/local/Cellar/nginx/1.15.0... (23 files, 1.4MB) Removing: /usr/local/Cellar/node/10.0.0... (5,301 files, 51.7MB) Removing: /usr/local/Cellar/node/10.1.0... (7,283 files, 55.4MB) Removing: /usr/local/Cellar/node/10.2.1... (5,301 files, 51.9MB) Removing: /usr/local/Cellar/node/9.11.1... (5,125 files, 49.7MB) Removing: /usr/local/Cellar/openssl/1.0.2n... (1,792 files, 12.3MB) Removing: /usr/local/Cellar/openssl/1.0.2o_1... (1,791 files, 12.3MB) Removing: /usr/local/Cellar/php/7.2.5... (515 files, 78.9MB) Removing: /usr/local/Cellar/php/7.2.6... (515 files, 78.9MB) Removing: /usr/local/Cellar/[email protected]/5.6.35_1... (498 files, 63.8MB) Removing: /usr/local/Cellar/[email protected]/5.6.36... (498 files, 63.8MB) Removing: /usr/local/Cellar/[email protected]/7.0.29_1... (502 files, 65.5MB) Removing: /usr/local/Cellar/[email protected]/7.0.30... (502 files, 65.5MB) Removing: /usr/local/Cellar/python/3.6.5... (4,794 files, 99.9MB) Removing: /usr/local/Cellar/readline/7.0.3_1... (46 files, 1.5MB) Removing: /usr/local/Cellar/sqlite/3.23.1... (11 files, 3MB) Removing: /usr/local/Cellar/webp/0.6.1... (38 files, 2MB) Removing: /usr/local/Cellar/wget/1.19.4_1... (50 files, 3.7MB) Removing: /usr/local/Cellar/xz/5.2.3... (92 files, 1.4MB) Removing: /usr/local/Cellar/zsh/5.4.2_3... (1,390 files, 11.8MB) Removing: /Users/seal/Library/Caches/Homebrew/dash-0.5.10.sierra.bottle.tar.gz... (83.6KB) Removing: /Users/seal/Library/Caches/Homebrew/dash-0.5.9.1.sierra.bottle.tar.gz... (83.2KB) Removing: /Users/seal/Library/Caches/Homebrew/freetds-1.00.89.sierra.bottle.tar.gz... (2.3MB) Removing: /Users/seal/Library/Caches/Homebrew/freetds-1.00.91.sierra.bottle.tar.gz... (2.3MB) Removing: /Users/seal/Library/Caches/Homebrew/freetype-2.9.sierra.bottle.1.tar.gz... (896.8KB) Removing: /Users/seal/Library/Caches/Homebrew/gdbm-1.14.1_1.sierra.bottle.tar.gz... (182.5KB) Removing: /Users/seal/Library/Caches/Homebrew/icu4c-61.1.sierra.bottle.tar.gz... (25.4MB) Removing: /Users/seal/Library/Caches/Homebrew/libidn2-2.0.4.sierra.bottle.tar.gz... (190.4KB) Removing: /Users/seal/Library/Caches/Homebrew/libpq-10.3.sierra.bottle.tar.gz... (6MB) Removing: /Users/seal/Library/Caches/Homebrew/libunistring-0.9.9.sierra.bottle.tar.gz... (1.4MB) Removing: /Users/seal/Library/Caches/Homebrew/mysql-5.7.21.sierra.bottle.tar.gz... (72.2MB) Removing: /Users/seal/Library/Caches/Homebrew/mysql-5.7.22.sierra.bottle.tar.gz... (72.2MB) Removing: /Users/seal/Library/Caches/Homebrew/nginx-1.13.12.sierra.bottle.tar.gz... (570.5KB) Removing: /Users/seal/Library/Caches/Homebrew/nginx-1.15.0.sierra.bottle.tar.gz... (572.6KB) Removing: /Users/seal/Library/Caches/Homebrew/node-10.0.0.sierra.bottle.tar.gz... (13.7MB) Removing: /Users/seal/Library/Caches/Homebrew/node-10.1.0.sierra.bottle.tar.gz... (13.7MB) Removing: /Users/seal/Library/Caches/Homebrew/node-10.2.1.sierra.bottle.tar.gz... (13.8MB) Removing: /Users/seal/Library/Caches/Homebrew/node-9.11.1.sierra.bottle.tar.gz... (13.3MB) Removing: /Users/seal/Library/Cac","date":"2018-07-27","objectID":"/post/6965849b/:1:3","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac通过brew安装的MySQL各种无法运行问题总结","uri":"/post/6965849b/"},{"categories":["数据库"],"content":" 参考:https://segmentfault.com/q/1010000000475470 ","date":"2018-07-26","objectID":"/post/e87d9191/:0:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac上使用brew安装MySQL","uri":"/post/e87d9191/"},{"categories":["数据库"],"content":"安装 brew install mysql ","date":"2018-07-26","objectID":"/post/e87d9191/:1:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac上使用brew安装MySQL","uri":"/post/e87d9191/"},{"categories":["数据库"],"content":"开启MySQL 通过brew brew services start mysql 或 mysql.server start ","date":"2018-07-26","objectID":"/post/e87d9191/:2:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac上使用brew安装MySQL","uri":"/post/e87d9191/"},{"categories":["数据库"],"content":"使用MySQL的配置脚本(MySQL 提供的配置向导) mysql_secure_installation # 完整路径 /usr/local/opt/mysql/bin/mysql_secure_installation 启动这个脚本后,即可根据如下命令提示进行初始化设置: $ mysql_secure_installation NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MySQL SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY! In order to log into MySQL to secure it, we'll need the current password for the root user. If you've just installed MySQL, and you haven't set the root password yet, the password will be blank, so you should just press enter here. # 输入当前root密码,一般为空 Enter current password for root (enter for none): OK, successfully used password, moving on... Setting the root password ensures that nobody can log into the MySQL root user without the proper authorisation. # 首次使用自带配置脚本,设置root密码 Set root password? [Y/n] y New password: Re-enter new password: Password updated successfully! Reloading privilege tables.. ... Success! By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. # 是否删除匿名用户 Remove anonymous users? [Y/n] y ... Success! Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. # 是否禁止远程登录 Disallow root login remotely? [Y/n] y ... Success! By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. # 是否删除测试数据库,并登录 Remove test database and access to it? [Y/n] y - Dropping test database... ... Success! - Removing privileges on test database... ... Success! Reloading the privilege tables will ensure that all changes made so far will take effect immediately. # 是否重新载入权限表 Reload privilege tables now? [Y/n] y ... Success! All done! If you've completed all of the above steps, your MySQL installation should now be secure. Thanks for using MySQL! Cleaning up... 5.6版本和8.0版本的有点不一样,8.0版似乎有个密码安全检测插件。 下面是8.0版本的: $ mysql_secure_installation Securing the MySQL server deployment. Connecting to MySQL using a blank password. Securing the MySQL server deployment. Connecting to MySQL using a blank password. VALIDATE PASSWORD PLUGIN can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD plugin? # 是否采用mysql密码安全检测插件(这里我选择了是,密码检查插件要求密码复杂程度高,大小写字母+数字+字符等) Press y|Y for Yes, any other key for No: y There are three levels of password validation policy: LOW Length \u003e= 8 MEDIUM Length \u003e= 8, numeric, mixed case, and special characters STRONG Length \u003e= 8, numeric, mixed case, special characters and dictionary file # 根据上面提示选择密码强度 Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 2 # 首次使用自带配置脚本,设置root密码 Please set the password for root here. New password: Re-enter new password: Estimated strength of the password: 100 Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. # 是否删除匿名用户 Remove anonymous users? (Press y|Y for Yes, any other key for No) : y Success. Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. # 是否禁止远程登录 Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y Success. By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a pro","date":"2018-07-26","objectID":"/post/e87d9191/:3:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac上使用brew安装MySQL","uri":"/post/e87d9191/"},{"categories":["数据库"],"content":"停止MySQL brew services stop mysql ","date":"2018-07-26","objectID":"/post/e87d9191/:4:0","tags":["Mac","Homebrew","数据库","MySQL"],"title":"Mac上使用brew安装MySQL","uri":"/post/e87d9191/"},{"categories":["hexo"],"content":"一、插件hexo-prism-plugin实现代码高亮导致的行号不显示问题 从官方文档我们可以看到md主题支持的代码高亮方式: document-md+code\" document-md+code 由于我使用的是1.4.0稳定版(时间20180722),所以只能使用 hexo-prism-plugin 插件,具体用法官方文档讲的很详细我就不多说了。 敲黑板,重点重点!!! 我在这里主要说一下1.4.0版的md主题对 hexo-prism-plugin 插件似乎不太兼容。 这是我的 prism 插件配置: prism_plugin: mode: 'preprocess' # realtime/preprocess theme: 'solarizedlight' line_number: true # default false 再给你们看一下截图: bug-MD+pris\" bug-MD+pris 是不是感觉有什么不对劲?? 相信看到这里大家应该都知道什么回事了,line_number我设置为true表示我开了行号显示,那么问题来了,我们的行号呢?还有那个诡异的竖线是什么鬼? 于是我用chrome的开发者模式调试了一下,发现了一个很眼熟的CSS属性——padding。如图: md-css-contentpre-padding\" md-css-contentpre-padding 这里给没学过编程的同学科普一下padding是什么意思: 先放个W3School的链接压压惊= =:CSS padding 属性 padding定义和用法: padding 简写属性在一个声明中设置所有内边距属性。 例: padding:10px; 类似于这种的意思就是上下左右四个内边距都是 10px。 那么内边距又是什么东西呢? 看图: explain-padding\" explain-padding 所以内边距的意思就是组件的内容到边框的距离。内边距的改变不会影响整个组件的大小,但是会影响组件内容的显示大小 废话就不多说了….. 我的直觉告诉我就是它的锅,我们现在来把它去掉试试! md-css-contentpre-nopadding\" md-css-contentpre-nopadding Unbelievable! 行号出现了!但是在Chrome的开发者模式改只是临时的,刷新就回到原来的样子了,我们要去主题的源代码修改。 经过一番查找终于找到了相关代码所在的文件hexo根目录/themes/material/source/css/style.min.css md-css-stylemin\" md-css-stylemin **注意:**是style.min.css这个压缩过的css文件,而不是style.css!! 打开style.min.css,查找padding:1pc或者#post-content pre都OK,然后把padding:1pc;注释掉即可。如图: md-css-stylemin-padding-commented\" md-css-stylemin-padding-commented ","date":"2018-07-22","objectID":"/post/832fa05b/:0:1","tags":["hexo","hexo主题"],"title":"关于Material主题源码的一些小修改(修复Bug?)","uri":"/post/832fa05b/"},{"categories":["hexo"],"content":"二、文章结尾添加版权声明 参考:http://tianma.space/post/1810369046/index.html 现在是版权时代,很多博主都会在文章结尾出写上诸如“转载请保留声明信息”之类的。下面我来说下如何在 Hexo 中添加版权声明。这里我拿 Material 主题作为例子,其他主题可参考后自行修改。 自定义字段:考虑到有些博文可能是不需要版权声明的,比如转载、翻译之类的,所以自定义 post_license 作为开关: 主题配置文件 _config.yml 中添加字段,作为总开关: post_license: enable: true # or false 文章的 Front-matter 中添加字段,作为独立开关: post_license: true # or false 创建 material/layout/_partial/post_license.ejs: \u003cdiv\u003e \u003cbr/\u003e \u003cul id=\"post-license\" class=\"post-license\"\u003e \u003cli class=\"post-license-author\"\u003e \u003cstrong\u003e本文作者:\u003c/strong\u003e \u003ca href=\"\u003c%= config.url %\u003e\"\u003e\u003c%= theme.author %\u003e\u003c/a\u003e \u003c/li\u003e \u003cli class=\"post-license-link\"\u003e \u003cstrong\u003e本文链接:\u003c/strong\u003e \u003ca href=\"\u003c%= page.permalink %\u003e\"\u003e\u003c%= page.title %\u003e\u003c/a\u003e \u003c/li\u003e \u003cli class=\"post-license-statement\"\u003e \u003cstrong\u003e版权声明: \u003c/strong\u003e 本文由 \u003c%= config.author %\u003e 原创,采用 \u003ca href=\"https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh\" rel=\"license\" target=\"_blank\"\u003e署名-非商业性使用-相同方式共享(CC BY-NC-SA)4.0 国际许可协议\u003c/a\u003e \u003c/br\u003e转载请保留以上声明信息! \u003c/li\u003e \u003c/ul\u003e \u003c/div\u003e 在 material/layout/_partial/post-content.ejs 引入 post_license.ejs: \u003c% if(theme.post_license.enable \u0026\u0026 page.post_license !== false){%\u003e \u003c%- partial('_partial/post_license') %\u003e \u003c% } %\u003e 在 material/source/css/style.min.css 中添加 版权声明 样式: #post-license { margin: 2em 0 0; padding: 0.5em 1em; border-left: 3px solid #ff4081; background-color: #f9f9f9; list-style: none; } ","date":"2018-07-22","objectID":"/post/832fa05b/:0:2","tags":["hexo","hexo主题"],"title":"关于Material主题源码的一些小修改(修复Bug?)","uri":"/post/832fa05b/"},{"categories":["hexo"],"content":"三、Disqus延迟加载 ☆☆ 2018-08-11 更新:Material主题1.5.6版本开始disqus_click自带延迟加载 ☆☆ 参考:https://blog.itswincer.com/posts/e5d13eb/ 先讲下考虑延迟加载的初衷: 好吧,其实是因为在大局域网内没法做到所有用户都能看评论,如果加载不出评论就会有一段时间在下方显示白块,强迫症的我怎么能忍受??!!于是我就想把评论功能设计成网络好的话就自动显示评论,否则需要手动点击。正好看到网上有,于是就借鉴了。 然后说下原理: 原理嘛,先用 ajax 异步发送一个 get 请求至 Disqus 服务器,接收成功则屏蔽按钮,加载评论;超时则自动断开,并显示加载按钮: 修改文件位置:\u003cMaterial主题目录\u003e/layout/_widget/comment/disqus_click/main.ejs \u003cdiv class=\"btn_click_load\"\u003e \u003cbutton class=\"disqus_click_btn\"\u003e\u003c%= __('post.comments_load_button') %\u003e\u003c/button\u003e \u003c/div\u003e \u003c!-- \u003cscript type=\"text/ls-javascript\" id=\"disqus-lazy-load-script\"\u003e $('.btn_click_load').click(function() { //click to load comments (function() { // DON'T EDIT BELOW THIS LINE var d = document; var s = d.createElement('script'); s.src = '//\u003c%= theme.comment.shortname %\u003e.disqus.com/embed.js'; s.setAttribute('data-timestamp', + new Date()); (d.head || d.body).appendChild(s); })(); $('.btn_click_load').css('display','none'); }); \u003c/script\u003e --\u003e \u003c% /* 延迟加载 disqus,timeout 可以自己设置时长 */ %\u003e \u003cscript type=\"text/javascript\" id=\"disqus-lazy-load-script\"\u003e $.ajax({ url: 'https://disqus.com/next/config.json', timeout: 1000, type: 'GET', success: function(){ var d = document; var s = d.createElement('script'); s.src = '//\u003c%= theme.comment.shortname %\u003e.disqus.com/embed.js'; s.setAttribute('data-timestamp', + new Date()); (d.head || d.body).appendChild(s); $('.disqus_click_btn').css('display', 'none'); }, error: function() { $('.disqus_click_btn').css('display', 'block'); } }); \u003c/script\u003e \u003c% /* 由于我超时时长设置得比较短,所以可能翻墙了还是没有自动加载评论,这时就需要手动点击加载了 */ %\u003e \u003cscript type=\"text/javascript\" id=\"disqus-click-load\"\u003e $('.btn_click_load').click(() =\u003e { //click to load comments (() =\u003e { // DON'T EDIT BELOW THIS LINE var d = document; var s = d.createElement('script'); s.src = '//\u003c%= theme.comment.shortname %\u003e.disqus.com/embed.js'; s.setAttribute('data-timestamp', + new Date()); (d.head || d.body).appendChild(s); })(); $('.disqus_click_btn').css('display','none'); }); \u003c/script\u003e 我是把原来的注释掉再加后面的,链接中说超时时间设为300比较短,的确短,我测试了一下,设置为1000就不影响阅读了。 **注意:**改完后记得确认主题配置文件是否启用了disqus_click ","date":"2018-07-22","objectID":"/post/832fa05b/:0:3","tags":["hexo","hexo主题"],"title":"关于Material主题源码的一些小修改(修复Bug?)","uri":"/post/832fa05b/"},{"categories":["运维日志"],"content":"前言 在搭建完Hexo之后,我就开始思考要不要配置https呢?最后深思熟虑,还是决定上https吧!毕竟现在https已经开始普及,像Chrome、Firfox这些浏览器如果不是https协议的会提示如下图,警告“不安全”什么的。强迫症患者表示看着最不舒服了(逃~ http browser show\" http browser show 于是我就开始了配置https之路…… 2018-12-02更新:添加通配符证书申请方法 SSL证书 ","date":"2018-07-21","objectID":"/post/41e1ea07/:0:0","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["运维日志"],"content":"选取合适的ssl证书 https = http + ssl,所以要走https协议首先要申请ssl证书。 但是我第一次建站,对ssl证书一点都不懂啊!!哪个ssl证书更好我也不知道,去搜索一翻发现各种说法,最后还是去请教了各位前辈。 经过各位前辈的建议,我了解到了Let's Encrypt,详细介绍我就不说了,这是它在维基百科的资料,然后这是官网 除此之外还有Digicert、SSL For Free、CA Cert。Digicert很贵,但是据某位前辈说似乎大企业喜欢用它的挺多,而且人家还支持签发 .onion 域名的证书(EV)。 看自己需求吧,我最后还是用了Let's Encrypt。 老父亲说这是免费的,但验证过程要在服务器上做,有点复杂。我觉得复杂点没所谓,好用就行。 某群里的前辈说也可以不验证,自签名就好(其实我还没懂哈哈哈,尴尬.jpg);前辈们还说了一种DNS方式验证,不需要架设额外服务;在我问了“Let’s encrypt用standalone还是webroot模式更好”之后(其实这个我也不懂),终于决定用DNS模式,DNS 模式更方便,还可以签通配符证书。 ","date":"2018-07-21","objectID":"/post/41e1ea07/:1:0","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["运维日志"],"content":"开始获取SSL证书 参考:使用acme.sh脚本的DNS API方式申请及更新let’s encrypt证书 群里多位前辈介绍了一个在 Linux 上申请let’s encrypt证书的脚本——acme.sh ","date":"2018-07-21","objectID":"/post/41e1ea07/:2:0","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["运维日志"],"content":"acme.sh脚本 Linux服务器下使用acme.sh脚本是申请let’s encrypt证书最便捷的方式,比官方推荐的certbot脚本工具要方便强大的多。好处是对环境要求较低,路由器上 busybox ash 都能跑,只需要使用到 curl 等少数几个外部程序 安装文档: https://github.com/Neilpang/acme.sh/wiki/说明 前提条件 拥有一个域名,例如 mydomain.com (在国内主机的用的话,还需要通过ICP备案) 确定二级域名,并且在域名服务器创建一条A记录,执行云主机的公网IP地址。www.mydomain.com指向xxx.xxx.xxx.xxx的IP地址 要等到新创建的域名解析能在公网上被解析到。 据说国内的域名提供商对letsencrypt的支持非常差,但是据说现阶段用dnspod解析的域名还没碰到问题。我用的是国外的Cloudflare解析我的域名,但是经常被墙(想骂人…),移动表示大多数都没有体验= =,联通情况好一点。 安装acme.sh脚本 执行一下命令后重新ssh登录服务器即可: curl https://get.acme.sh | sh 该命令会把 acme.sh 安装到你的 home 目录下: ~/.acme.sh/,一般会自动创建环境变量,后期直接命令行输入acme.sh即可。 使用DNS API申请证书 (推荐配置使用API的方式,否则每隔三个月,仍然需要重新验证DNS TXT记录) 创建DNS API的Key及Secret 参考: https://github.com/Neilpang/acme.sh/tree/master/dnsapi https://github.com/Neilpang/acme.sh/blob/master/dnsapi/README.md acme.sh 目前支持 cloudflare, dnspod, cloudxns, godaddy 以及 ovh 等数十种解析商的自动集成. 【例子一】域名由Cloudflare提供解析的网站,使用CloudFlare域名API自动颁发证书 以 Cloudflare 为例, 你需要先登录到 Cloudflare 账号才能获得API密钥, 都是免费的。 Cloudflare-Api-1\" Cloudflare-Api-1 Cloudflare-Api-2\" Cloudflare-Api-2 然后在Linux下使用acme.sh命令申请证书: 命令使用参考:Linux 下使用 acme.sh 配置 Let’s Encrypt 免费 SSL 证书 + 通配符证书 先导入密钥和邮箱: export CF_Key=\"sdfsdfsdfljlbjkljlkjsdfoiwje\" export CF_Email=\"[email protected]\" 我们现在发行一个证书: acme.sh --issue --dns dns_cf -d example.com -d www.example.com --accountemail [email protected] --accountemail是指定帐户电子邮件,仅对--install和--update-account命令有效(待验证,因为我执行命令时并没有带上该参数)。 输入命令后回车,需要等两分钟,终端会有倒计时。 倒计时结束后,执行命令: acme.sh --renew -d example.com -d www.example.com 补充:获取 Let’s Encrypt 通配符证书 通配符证书,英文 Wildcard Certificate 国内黑话叫做野卡,经过一个月的跳票后,Let’s Encrypt 目前已经支持通配符的证书,同样 acme.sh 也是支持的,和多域名证书不同,通配符证书必须使用 DNS TXT 记录验证方式,我们以 example.com 和 *.example.com 为例 acme.sh --issue -d example.com -d '*.example.com' --dns 记得*.example.com加单引号 如果你的 DNS 提供商支持 API,你也可以直接使用 API 而不需要手工修改 TXT 记录,详细用法请见这里 以 CloudFlare 为例: acme.sh --issue --dns dns_cf -d example.com -d '*.example.com' 生效后给证书续一秒 acme.sh --renew -d example.com -d '*.example.com' 然后后面你想要一个二级域名(如:abc.example.com)的时候就不用再申请了,只需要在DNS添加A记录等它生效就可以用了。当然,下面的安装证书和配置站点(Nginx)步骤还是要做的😂 安装证书 # 创建安装目录 mkdir /etc/ssl/example.com/ -p # reloadcmd参数里面填写的是在更新证书后需要重新启动的服务,例如重启nginx或者apache或者其它脚本。 acme.sh --installcert -d example.com \\ --key-file /etc/ssl/example.com/example.com.key \\ --fullchain-file /etc/ssl/example.com/fullchain.cer \\ --reloadcmd \"systemctl reload nginx\" 注意: /etc/ssl/example.com/的所有者以及用户组必须为当前用户,否则后面installcert的时候会提示没有权限,可执行如下命令,后面可更改回原来的所有者和用户组: #更改所有者和用户组 sudo chown $USER:$USER -R /etc/ssl/example.com/ #查看指定文件夹的权限、所有者和用户组 ls -l /etc/ssl/ ls -l /etc/ssl/example.com/ (一个小提醒, 部分服务器系统用的是 service nginx force-reload, 不是 service nginx reload, 据测试, reload 并不会重新加载证书, 所以用的 force-reload) Nginx 的配置 ssl_certificate 使用 /etc/nginx/ssl/fullchain.cer ,而非 /etc/nginx/ssl/\u003cdomain\u003e.cer ,否则 SSL Labs的测试会报 Chain issues Incomplete 错误。 --installcert命令可以携带很多参数, 来指定目标文件. 并且可以指定 reloadcmd, 当证书更新以后, reloadcmd 会被自动调用,让服务器生效. 值得注意的是, 这里指定的所有参数都会被自动记录下来, 并在将来证书自动更新以后, 被再次自动调用. mkdir的-p参数解释: -p, –parents 可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不存在的目录,即一次可以建立多个目录; mkdir命令参考:https://www.linuxdaxue.com/linux-command-intro-mkdir.html 更新证书 目前证书在 60 天以后会自动更新, 你无需任何操作. 今后有可能会缩短这个时间, 不过都是自动的, 你不用关心. 更新 acme.sh 目前由于 acme 协议和 letsencrypt CA 都在频繁的更新, 因此 acme.sh 也经常更新以保持同步. 升级 acme.sh 到最新版 : acme.sh --upgrade 如果你不想手动升级, 可以开启自动升级: acme.sh --upgrade --auto-upgrade 之后, acme.sh 就会自动保持更新了 你也可以随时关闭自动更新: acme.sh --upgrade --auto-upgrade 0 应用实例 ","date":"2018-07-21","objectID":"/post/41e1ea07/:2:1","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["运维日志"],"content":"配置Nginx使用证书开通https站点 生成Perfect Forward Security(PFS)键值 Perfect Forward Security(PFS)是个什么东西,我也不清楚,中文翻译成完美前向保密,反正是这几年才提倡的加强安全性的技术。如果本地还没有生成这个键值,需要先执行生成的命令。 /etc/ssl/example.com/ openssl dhparam -out dhparam.pem 2048 生成的过程还挺花时间的,据我查到的资料,据说参数为2048的要4分钟左右,还受服务器性能影响,此时不妨做点其他东西。 (时间一分一秒的过去…) 好了,PFS键值生成了,我们用ls命令来确认一下目录下是否生成了dhparam.pem文件,确认完毕就可以执行下一步了。 特别说明: 一定要确认文件是否已经生成!!!在这一步我原本是执行openssl dhparam 2048 -out dhparam.pem,不知道为什么执行了多次都无法生成dhparam.pem文件,后来换了个目录执行了多次openssl dhparam -out dhparam.pem 2048才成功,这是后面配置完Nginx后无法重新启动Nginx服务了,我输出日志才发现的,具体后面会说。 nginx配置使用证书。 编辑你的Nginx配置文件,如:/etc/nginx/sites-available/\u003cyour-nginx-config\u003e ☆☆修改配置文件前建议先备份☆☆ 可以通过Mozilla SSL Configuration Generator自动生成对应的参考配置 修改http对应的server模块: server { listen 80; listen [::]:80; server_name example.com; #return 301 https://example.com$request_uri; if ( $scheme = http ){ return 301 https://$server_name$request_uri; } root /var/www/html/example.com; index index.html index.htm; } return 301 https://$server_name$request_uri;是强制http跳转https 在配置文件中添加https的server模块: server { listen 443 ssl; server_name example.com; #charset utf-8; location / { root /var/www/html/example.com; #index index.html index.htm; } #root /var/www/html/example.com; index index.html index.htm; access_log /var/log/nginx/example.com_access.log; error_log /var/log/nginx/example.com_error.log; ssl_certificate /etc/ssl/example.com/fullchain.cer; ssl_certificate_key /etc/ssl/example.com/example.com.key; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; ssl_dhparam /etc/ssl/example.com/dhparam.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK'; ssl_prefer_server_ciphers on; } 总的样例内容如下: server { listen 80; listen [::]:80; server_name example.com; #return 301 https://example.com$request_uri; if ( $scheme = http ){ return 301 https://$server_name$request_uri; } root /var/www/html/example.com; index index.html index.htm; } server { listen 443 ssl; server_name example.com; #charset utf-8; location / { root /var/www/html/example.com; #index index.html index.htm; } #root /var/www/html/example.com; index index.html index.htm; access_log /var/log/nginx/example.com_access.log; error_log /var/log/nginx/example.com_error.log; ssl_certificate /etc/ssl/example.com/fullchain.cer; ssl_certificate_key /etc/ssl/example.com/example.com.key; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; ssl_dhparam /etc/ssl/example.com/dhparam.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK'; ssl_prefer_server_ciphers on; } 配置文件没有问题的话执行systemctl reload nginx,然后在浏览器打开http://example.com, 如果正常跳转到https://example.com,就算成功了。 如果是chrome浏览器,在地址栏点击绿色小锁的图标,可以查看证书的详情,如图。 https browser sho\" https browser sho ","date":"2018-07-21","objectID":"/post/41e1ea07/:3:0","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["运维日志"],"content":"此步骤遇到的难题 这里要注意的是如果你是Nginx+Cloudflare,那么Cloudflare的\"Crypto—SSL\"要设置成Full(strict),否则会导致example.com将你重定向次数过多的情况,默认是Flexible。CF设置如图: cloudflare config of starting https with nginx\" cloudflare config of starting https with nginx 这里还有个坑就是前面说的无法生成dhparam.pem文件导致无法重载Nginx。一开始我还没发现,一直在以为Nginx配置文件哪里错了,后来sudo journalctl -xe输出日志才发现Nginx找不到dhparam.pem文件: nginx cannot found dhparam.pem-1\" nginx cannot found dhparam.pem-1 用ls命令可以发现目录下并不存在dhparam.pem文件: nginx cannot found dhparam.pem-2\" nginx cannot found dhparam.pem-2 于是我又执行了几次openssl dhparam 2048 -out dhparam.pem,结果还是一样,而且过程竟然只需要1分钟不到。如图: nginx cannot found dhparam.pem-3\" nginx cannot found dhparam.pem-3 察觉到不对劲之后我马上去找了跟dhparam的资料,后来在一个博客上发现他的命令顺序跟我不一样,这个文件存放目录也跟我不一样,于是我就把命令改成openssl dhparam -out dhparam.pem 2048和改目录到example.com(此时该目录权限还是$USER的)再试了几次,第一次是不行的,执行多几次后突然就成功了。如图: nginx cannot found dhparam.pem-4\" nginx cannot found dhparam.pem-4 可以发现此次执行命令时间十分的长,我等了大概5分钟才执行完毕。 然后我们再用ls来看看目录下的文件,可以发现文件已经生成: nginx cannot found dhparam.pem-5\" nginx cannot found dhparam.pem-5 ","date":"2018-07-21","objectID":"/post/41e1ea07/:3:1","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["运维日志"],"content":"测试证书 如果是网站的话,可以使用第三方网站工具测试自己网站的HTTPS配置的安全性 SSL Lab就是个不错的选择: https://ssllabs.com/ssltest/analyze.html?d=example.com 这是我的测试结果,都是A哦!! SSL Lab Server Test\" SSL Lab Server Test 也可以在Linux下使用openssl命令查看证书的过期时间 openssl x509 -noout -dates -in /etc/ssl/example.com/example.com.key ","date":"2018-07-21","objectID":"/post/41e1ea07/:4:0","tags":["运维","服务器","Linux","SSL","Nginx","HTTPS"],"title":"全站开启https总结(艰苦历程?)","uri":"/post/41e1ea07/"},{"categories":["hexo"],"content":"第一次在自己服务器搭建博客,总会有那么一点点失误,碰到一点坑,在这里总结一下搭建hexo的具体步骤。 参考: Hexo之旅(二):Hexo博客搭建(在 Mac OS 平台) 如何在服务器上搭建hexo博客 Hexo架构 首先我们要理解hexo是如何生成静态博客以及如何实现通过服务器访问静态博客的 通过上图我们可以知道,整个流程就是在本地通过 hexo g 渲染博客的静态文件,然后通过 hexo d 把静态文件 push到服务器上我们自己创建的git仓库,服务器再通过 git-hooks 同步网站根目录,这样就可以访问了 配置本地环境 ","date":"2018-07-16","objectID":"/post/1572acec/:0:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"安装依赖 Hexo 依赖于 Node.js 和 Git,需要先安装。 ","date":"2018-07-16","objectID":"/post/1572acec/:1:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"安装Git Git安装(廖雪峰教程) ","date":"2018-07-16","objectID":"/post/1572acec/:1:1","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"安装Node.js Node的话建议用nvm管理,否则很容易出现 command not found: hexo 的情况,出现这种情况主要是node的版本问题 可以通过nvm来控制一下node的版本来解决 参考:Hexo安装后hexo指令无法被找到的解决方法 使用Homebrew安裝nvm brew install nvm //这个过程中可能需要安装gcc或者其它需要依赖的工具 按照提示一次安装好即可 mkdir ~/.nvm echo \"export NVM_DIR=~/.nvm\" \u003e\u003e .bash_profile echo \". $(brew --prefix nvm)/nvm.sh\" \u003e\u003e .bash_profile //这个步骤中mac下的brew需要通过--prefix这种形式完成,如果是zsh的话可以输出到.zshrc文件 重启终端后用 nvm help 来验证nvm是否正确安装 使用nvm安裝Node.js 安裝完了nvm,接著安裝主角 Node.js。先用 nvm ls-remote 指令看一下有哪些版本可以安裝: nvm ls-remote . . . v9.10.0 v9.10.1 v9.11.0 v9.11.1 v9.11.2 v10.0.0 v10.1.0 v10.2.0 v10.2.1 v10.3.0 v10.4.0 . . . 然后直接用一下命令安装最新稳定版node nvm install stable 使用nvm无痛切换Node.js版本 检查当前使用的 Node.js 版本使用命令 nvm ls。如果输出结果如下表示正确: $ nvm ls v0.12.10 -\u003e v10.6.0 system default -\u003e v10.6.0 node -\u003e stable (-\u003e v10.6.0) (default) stable -\u003e 10.6 (-\u003e v10.6.0) (default) iojs -\u003e N/A (default) lts/* -\u003e lts/carbon (-\u003e N/A) lts/argon -\u003e v4.9.1 (-\u003e N/A) lts/boron -\u003e v6.14.3 (-\u003e N/A) lts/carbon -\u003e v8.11.3 (-\u003e N/A) 第一个 -\u003e 表示当前使用的版本,default -\u003e 表示默认版本,必须保证这两个,不然后面安装hexo会提示 -bash: hexo: command not found ,不能在命令行使用。 设置的方法是,先通过 nvm ls 看看本地安装了什么版本,如果本地没有,则应该使用上一步的方法先安装,然后执行这个命令指定版本: nvm use v10.6.0 再通过这个命令指定默认版本: nvm alias default v10.6.0 ","date":"2018-07-16","objectID":"/post/1572acec/:1:2","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"部署Hexo ","date":"2018-07-16","objectID":"/post/1572acec/:2:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"安装Hexo 所有必备的依赖都安装完成后,即可使用 npm 安装 Hexo: npm install -g hexo-cli -g 或 –global 表示全局安装模块,如果没有这个参数,会安装在当前目录的node_modules子目录下。 安装 Hexo 完成后,请执行下列命令,Hexo 将会在指定文件夹中新建所需要的文件。 hexo init \u003cfolder\u003e cd \u003cfolder\u003e npm install npm install 表示安装当前目录package.json文件中配置的dependencies模块。 ","date":"2018-07-16","objectID":"/post/1572acec/:2:1","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"更新 Hexo 官方发布了新版本后,可以在Hexo建立的博客目录内运行: npm update 用如下命令可以检查package.json文件中配置的dependencies的版本号: npm ls --depth=0 ","date":"2018-07-16","objectID":"/post/1572acec/:2:2","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"安装 Hexo 插件 npm install \u003cmodule\u003e --save 下面是一些可选插件的安装 npm i hexo-all-minifier --save npm install hexo-admin --save npm install hexo-generator-archive --save npm install hexo-generator-tag --save npm install hexo-generator-feed --save npm install hexo-wordcount --save npm install hexo-helper-qrcode --save npm install hexo-generator-search --save npm install hexo-generator-searchdb --save npm i -S hexo-prism-plugin npm install hexo-abbrlink --save npm install hexo-generator-sitemap --save 配置服务器环境 ","date":"2018-07-16","objectID":"/post/1572acec/:2:3","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"部署 Nginx ","date":"2018-07-16","objectID":"/post/1572acec/:3:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"安装 Nginx \u0026\u0026 启动 Nginx 我服务器的Nginx是用Docker部署的,早已经部署好。这步暂时跳过,日后补充…(又偷懒了(逃~ ","date":"2018-07-16","objectID":"/post/1572acec/:3:1","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"配置虚拟主机 \u0026\u0026 创建新的网站目录 \u0026\u0026 创建虚拟主机配置文件 同上(逃~ ","date":"2018-07-16","objectID":"/post/1572acec/:3:2","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"部署 Hexo 到服务器 Hexo 可以使用 git 方式部署。 (网站的目录在下面统一用 \u003cyour-website\u003e 代替,如你的目录在 /var/www/html ,那就用 /var/www/html 替换 \u003cyour-website\u003e ) 创建空白 git 仓库,并且设置 git hook cd ~ mkdir blog_hexo.git \u0026\u0026 cd blog_hexo.git git init --bare 新建 post-receive 脚本 cd hooks touch post-receive 编辑 ~/blog_hexo.git/hooks/post-receive 脚本 #!/bin/bash GIT_REPO=/home/xxx/blog_hexo.git TMP_GIT_CLONE=/tmp/HexoBlog PUBLIC_WWW=\u003cyour-website\u003e rm -rf ${TMP_GIT_CLONE} git clone $GIT_REPO $TMP_GIT_CLONE rm -rf ${PUBLIC_WWW}/* cp -rf ${TMP_GIT_CLONE}/* ${PUBLIC_WWW} 赋予脚本执行权限 chmod 755 post-receive //或 chmod +x post-receive 注意: 这里要先确保 \u003cyour-website\u003e 目录下没有任何文件。备份好文件后执行一下命令: sudo rm -rf \u003cyour-website\u003e/* 执行 ls -l \u003cyour-website\u003e 命令检查该目录及其子目录(文件)所有者和用户组是否为你当前使用的用户。 若显示: drwxr-xr-x 2 root root 4096 Jun 4 07:00 \u003cyour-website\u003e 则说明你的网站目录的所有者以及用户组都是root,那么当你用git-hooks把tmp目录的静态博客拷贝到 \u003cyour-website\u003e 时,会由于没有权限而导致拷贝失败,所以我们需要把的所有者和用户组都改为你当前使用的用户。执行以下命令即可: sudo chown $USER:$USER -R \u003cyour-website\u003e ","date":"2018-07-16","objectID":"/post/1572acec/:4:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"配置本机 在博客目录下运行下面命令,安装 git 部署插件。 npm install hexo-deployer-git --save 修改博客的配置文件 _config.yml,修改deploy选项: deploy: type: git message: update repo: [email protected]:/home/xxx/blog_hexo.git branch: master 然后运行 hexo clean \u0026\u0026 hexo g \u0026\u0026 hexo d 即可部署本地渲染网页到服务器上,之后只需要访问额你的域名就可以访问你的hexo博客啦!如我的博客是 reb.mallotec.com **注:**请把127.0.0.1替换成自己服务器的ip 附:Hexo 个性化–\u003eNexT主题 参考:Hexo之旅(三):Hexo博客个性化及NexT主题配置 ","date":"2018-07-16","objectID":"/post/1572acec/:5:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"文档 NexT 主题提供了详细的 使用文档 。 ","date":"2018-07-16","objectID":"/post/1572acec/:6:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"下载主题 主题的安装很简单,执行以下命令即可: cd your-hexo-folder mkdir themes/next curl -s https://api.github.com/repos/iissnan/hexo-theme-next/releases/latest | grep tarball_url | cut -d '\"' -f 4 | wget -i - -O- | tar -zx -C themes/next --strip-components=1 或 cd your-hexo-site git clone https://github.com/iissnan/hexo-theme-next themes/next ","date":"2018-07-16","objectID":"/post/1572acec/:7:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"启用 NexT 主题 克隆/下载 完成后,打开 站点配置文件,找到theme字段,并将其值更改为next。 这里注意区分两个配置文件: 站点配置文件:是你的 hexo 博客目录下面的 _config.yml 文件。 主题配置文件:是 themes/next 目录下的 _config.yml 文件。 ","date":"2018-07-16","objectID":"/post/1572acec/:8:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"验证主题是否启用(这步我没执行过= =) 运行hexo s --debug,并访问http://localhost:4000,确保站点正确运行。 ","date":"2018-07-16","objectID":"/post/1572acec/:9:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":["hexo"],"content":"重新渲染博客 启用主题后最好重新渲染一遍博客,命令: hexo clean \u0026\u0026 hexo g \u0026\u0026 hexo d ","date":"2018-07-16","objectID":"/post/1572acec/:10:0","tags":["教程","个人经验","hexo"],"title":"第一次搭建Hexo博客过程 in MacOS","uri":"/post/1572acec/"},{"categories":null,"content":"献上一首我最爱听的歌: 够爱——曾沛慈 QQ音乐的 东城卫\u0026谢和弦(A-Chord)版本 和 曾沛慈版本 已失效 博客的换成了 曾沛慈 版的音乐直链,没有歌词了我补上本地歌词文件啦~我能再听亿遍🙈️!!! 我是谁 我是 Reborn,嗯……因为特别喜欢家庭教师杀手里面的彩虹之子 Reborn (中文名:里包恩),也可以叫我小婴儿🌚。 其实我有个花名叫“橘子”,是以前中学老师不会念我的姓念错了然后同学们就叫我橘子了😂,不过感觉挺好听嗯。 喜欢宅,不善交际。 好吧,这两年有点改变,都快成计算机协会头号戏精了😂 喜欢研究各种技术,对开源的事物特感兴趣 感谢老父亲带我走进开源世界🌚 喜欢自由和无拘无束、不喜欢被束缚。 这也是我喜欢跳街舞的原因 目前已经是个 在校大三 大四狗 社畜🙈️,主要研究 Android、JavaEE 等。(现在不搞 PHP 啦~) 这是哪里 这是我的个人技术分享博客,随便记录点东西。目前主要以 Android、Java 和逆向的东西为主。(现在不搞 Web 和 PHP 啦~) 联系方式 E-Mail: [email protected] QQ: 62***92 (不再提供 QQ) Telegram: @RebornQ Github: https://github.com/RebornQ 如果你想和我交换友链 如果你想与我交换友链,请在下方评论。 我仅接受个人博客申请友链。 我的友链信息如下: 博客名: Reborn’s Blog 地址: https://reb.mallotec.com 头像: https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/avatar/android-chrome-192x192.png 描述: 一壶酒,一蓑衣,渔歌唱晚,了此一生 最后说几句吧 我个人不是很会写文章,上面也说了随便记录点东西,所以估计文章都是一些记录或者想法而已~而且更新估计会很少了。但是,还是很感谢你们来到我的博客,谢谢你们的支持噢~ 博客存活时间 🐼 本站已运行 载入天数...载入时分秒... 🐼","date":"2018-07-16","objectID":"/about/:0:0","tags":null,"title":"关于我","uri":"/about/"}]