-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 94.1 KB
/
index.json
1
[{"content":"讲道理,我还从来没关注过VNC这个东西。早在几年前就知道了VNC,但其实我一直都用不上。\n最近,我的Android设备因为电池寿命问题已经终止使用了,我就打算让他成为一个永久的·被插入的状态(指充电)。\n为了方便,我直接用傻瓜脚本在Termux中装上Debian。\n一键安装Debian脚本:https://github.com/wahasa/Debian\n一键安装Ubuntu脚本:https://github.com/MFDGaming/ubuntu-in-termux\n不过我不推荐在termux用ubuntu。因为Termux无法使用systemd,所以在ubuntu的snapd也就用不了。(但是如果root后似乎是有办法的。)而Ubuntu深度绑定了snap,就算有办法移除snap也挺麻烦的。snap对arm64处理器的优化也很少,为什么不直接用debian呢。\n就比如在Ubuntu要通过传统的deb去安装firefox浏览器,就先需要用snap remove firefox来先删除,但是\u0026hellip;\u0026hellip;snap是完全用不了的。\nPhantom Process Killer 自Android12开始有一个Phantom Process Killer机制,会限制应用后台的子进程数量。所以Termux很容易被杀,从而出现Process completed (signal 9) 。 我用的是adb命令去关闭这项限制。\nADB工具下载链接:https://developer.android.com/studio/releases/platform-tools\nadb devices adb shell \u0026#34;/system/bin/device_config set_sync_disabled_for_tests persistent\u0026#34; adb shell \u0026#34;/system/bin/device_config put activity_manager max_phantom_processes 2147483647\u0026#34; adb shell settings put global settings_enable_monitor_phantom_procs false 还有一种方法是在开发者设置中找到Feature flag,在里面关闭settings_enable_monitor_phantom_procs。不过不知道为什么,我的手机中的feature flag里什么都没有。\n桌面环境 \u0026amp; VNC 为了让VNC客户端能够连接到手机中的Debian,要先装一个桌面。\n我用的是xfce,可以直接用apt安装:\nsudo apt install xfce4 xfce4-goodies 但是也可以用@wahasa的脚本,顺带就安装了tigervnc,而且配置好了/.vnc/xstartup:\napt install wget -y ; wget https://raw.githubusercontent.com/wahasa/Debian/main/Desktop/de-xfce.sh ; chmod +x de-xfce.sh ; ./de-xfce.sh 这里我使用使用vnc-start去启动VNC Server只能监听本地的端口,在局域网内是搜不到的。\n所以我添加了一个no localhost的flag:\n$ vncserver -localhost no TightVNC 如果要使用TightVNC:\n$ apt install tightvncserver 删除Server:vncserver -kill :1\n修改VNC连接密码:vncpasswd\n然后修改下~/.vnc/xstartup:\n#!/bin/bash xrdb $HOME/.Xresources startxfce4 \u0026amp; 更多 后续还可以安装gogs作为本地的git服务器。\nRefs\nhttps://docs.andronix.app/android-12/andronix-on-android-12-and-beyond\n","permalink":"https://moonlab.top/posts/2024/debian-on-android/","summary":"文章介绍了如何在Android设备上通过Termux安装Debian,并配置VNC以便远程访问。作者提到,由于Android 12引入的Phantom Process Killer机制,Termux容易被杀死,因此提供了关闭该限制的ADB命令。接着,文章详细说明了如何安装XFCE桌面环境和配置VNC服务器,包括使用TightVNC的步骤。最后,作者提到可以进一步安装gogs作为本地Git服务器。","title":"Android设备安装Debian成为BT下载服务器"},{"content":"和平常一样,在我结束使用Windows并点击更新并关机,在那一刻我只当这次windows更新是一次无足轻重的事情,不会对我的电脑造成什么影响。\n但下一秒,我转到Linux系统,发现了一些不对劲的事情。\n我在refind直接启动Linux,在最开始还一切正常,直到中间出现了一个90秒的等待:\n90秒倒计时结束后,就无法启动至系统了:\nDependency failed for file system check Dependency failed for local file system 然后系统进入了emergency状态,但是可以使用终端。\n而且grub也进入了rescue界面,引导自然是有问题了。\n我看了一下磁盘分区,发现Windows的10GB的Recovery分区与Linux系统的分区的顺序互换了。全靠windows在我的电脑里驰骋。\n修复Linux 看起来是某个分区出现了问题。\n我简直一头雾水。\n这个被一直执行某个任务的E4F3的分区我查了下是不存在的。然后看了看fstab,发现这个E4F3是要被挂载到/boot/efi的。看来这个分区本来应该是我的ESP,不知道Windows更新做了什么事情让ESP的UUID改变了。\n之后我就直接用device path而不是UUID了。\n修复Grub 看起来grub.cfg中也是用UUID来定位ESP的,所以这次就出现了问题。\n使用两行命令重装一下就能修好。(但是grub会被添加到uefi boot的第一顺序)\n$ sudo grub-install $ sudo grub-mkconfig -p /boot/grub/grub.cfg 再用efibootmgr更改一下启动顺序(我是用refind的)\n$ sudo efibootmgr -o 0001,0002,0003... 我实在不明白,Windows更新你怎么那么事事呢 ","permalink":"https://moonlab.top/posts/2024/windows-update-broke-linux-system/","summary":"在 Windows 更新后,用户的 Linux 系统因等待 90 秒而无法启动,并且出现依赖失败。调查发现,Windows 调换了 Linux 分区和恢复分区的顺序,从而导致了问题。用户通过修复 Linux、更新 fstab、重新安装 Grub 和使用 efibootmgr 更改启动顺序解决了问题。","title":"[双系统] Windows 更新摧毁了我的Linux系统"},{"content":"FICTION [ ] Cryptonomicon by Neal Stephenson - 2024.1\n[x] 雪国 川端康成 - 2024.4 美的留存是徒劳的\n[x] 千只鹤 川端康成 - 2024.5 有罪的爱也是爱吗?\nPHILOSOPHY 规训与惩罚:监狱的诞生 米歇尔·福柯 - 2024.6 Doing Philosophy: From Common Curiosity to Logical Reasoning\nTECH [ ] How to Solve It by G.Polya\n[ ] Code Complete by McConnell, Steve - 2024 I like the well-structured, clear table of contents with the related references in a chapter\u0026rsquo;s beginning, and concise summaries at the end. Besides that, what things need to be done before project construction is well\n[ ] Break the Code: Cryptography for Beginners by Bud Johnson\n","permalink":"https://moonlab.top/reading/","summary":"FICTION [ ] Cryptonomicon by Neal Stephenson - 2024.1\n[x] 雪国 川端康成 - 2024.4 美的留存是徒劳的\n[x] 千只鹤 川端康成 - 2024.5 有罪的爱也是爱吗?\nPHILOSOPHY 规训与惩罚:监狱的诞生 米歇尔·福柯 - 2024.6 Doing Philosophy: From Common Curiosity to Logical Reasoning\nTECH [ ] How to Solve It by G.Polya\n[ ] Code Complete by McConnell, Steve - 2024 I like the well-structured, clear table of contents with the related references in a chapter\u0026rsquo;s beginning, and concise summaries at the end.","title":"Reading List"},{"content":"Golang一般用到embed package的主要场景就是Web server需要携带html css js等静态文件。\n使用embed directive可以让外部文件在编译时自动加入到二进制程序中。这样我们如果要分发程序,只需一个二进制文件,不需要外带任何其他资源文件。\n而我们在代码中去引用embedded文件有三种方法(类型):string , []byte 和 FS\n但实际上,embed还是有一些限制的,本文就详细讲讲这些限制是什么,以及如何曲线救国。\n文件层级问题 如果embed directive与被embedded的文件不在同一级(文件系统中)。\n比如说\n│ main.go │ ├─server │ handler.go │ └─tepl index.html 如果handler.go试图去包含tepl文件夹里的文件:\n// handler.go //go:embed tepl var s embed.FS embed pattern将无法被成功解析,因为被引用者的文件层级高于引用者。\n除了将被引用者的文件层级降至同一级或更低,还有一种方法就是直接在main.go这样同级的文件中进行处理,然后传递给需要的包中。\n// main.go //go:embed tepl var tepl embed.FS func main() { handler.injectFS(\u0026amp;tepl) } 目前来说,没有其他更好的解决方法了。\n复杂路径问题 假如我们在tepl中放入一个static文件夹:\n│ main.go │ └─server │ handler.go │ └─tepl │ index.html │ └─static main.css 在handler.go中,我们用http.FileServer来处理对静态文件们的请求:\n//go:embed tepl var tepl embed.FS func handle() { http.Handle(\u0026#34;/static/\u0026#34;, http.StripPrefix(\u0026#34;/static/\u0026#34;, http.FileServer(http.FS(tepl)))) } 很自然地,定义的是/static/的路由,而不是/,因为根路径常常要处理其他事情。\n但此时由于 embed 的局限,如果我们要获取mian.css,就需要访问这个路径/static/tepl/static/main.css\n解决的方法也很简单,用 io.fs 包中的Sub方法即可:\nvar tepl embed.FS func handle() { sub, _ := fs.Sub(tepl, \u0026#34;tepl/static\u0026#34;) http.Handle(\u0026#34;/static/\u0026#34;, http.StripPrefix(\u0026#34;/static/\u0026#34;, http.FileServer(http.FS(sub)))) } Refs\nhttps://pkg.go.dev/embed\n","permalink":"https://moonlab.top/posts/2023/golang-embed-package-use-issues/","summary":"Golang 使用 embed 包在编译时将外部文件包含到二进制程序中。使用 embed 指令可以将 html、css、js 等静态文件添加到二进制文件中,而无需额外的资源文件。嵌入文件可以使用字符串、[]字节和 FS 来引用。但也有一些限制,如文件层次结构问题和复杂路径问题。例如,如果嵌入文件和被嵌入文件不在同一层次,嵌入模式将无法成功解析。另一个问题是处理复杂路径,即静态文件夹被放置在嵌入文件的子文件夹中。解决办法是使用 io/fs 软件包中的 Sub 方法来处理这些复杂路径。","title":"Golang embed 使用问题"},{"content":"上篇文章整理了一下博客的评论系统,而现在我需要一个稳定好用的博客数据备份方案。\n老生常谈的321原则,即至少保留3个备份副本,使用2种不同的储存介质,并将其中1个备份存储在远离数据源的地方。\n最开始我考虑用Github Action构建一个Workflow来实现在每次commit自动将仓库内的文件同步在OneDrive中。\n但后来想想这种方法局限性很大。首先这样做只能备份已发布的文章和页面,对于博客的草稿,主题文件,package.json,_config.cml 这些同样重要的文件并没有保存的方法。而且十分受限于Github平台。\n因此我开发了一个Hexo插件用于实现多平台备份,项目GitHub仓库:HelloLingC/hexo-auto-backup (github.com)\n此插件可以在执行 hexo deploy 命令时自动根据配置将source, themes, package.json, _config.yml 同步在其他的本地磁盘(如USB),或云盘之中。\n借助rclone,支持所有主流的云盘,OneDrive, Google Drive, Pikpak等等,同时也支持SMTP, FTP, HTTP等协议。 详细可以查看插件的readme\n安装插件的方法很简单,和大部分的插件一样用npm:\nnpm install hexo-auto-backup 目前还仍需改善:\nExpire 的设置,即设置每个备份最大保留时间,如30天 多系统支持,目前只支持Windows ","permalink":"https://moonlab.top/posts/2023/hexo-auto-backup/","summary":"本文讨论了博客数据备份方案,强调321原则:保留3个备份副本,使用2种不同储存介质,1个备份远离数据源。最初考虑使用GitHub Action进行备份,但发现其局限性。为此,开发了Hexo插件“hexo-auto-backup”,可在执行hexo deploy时自动备份重要文件到本地或云盘,支持多种云服务和协议。插件安装简单,通过npm安装。未来计划改进备份过期设置和多系统支持。详细信息可见插件的GitHub仓库。","title":"Hexo博客自动备份插件 云盘备份支持"},{"content":"栈 Stack 栈是一种数据结构,遵循Last in, First out (LIFO)规则,栈在中文语境中有时也被叫做堆栈。在实际中,是一片内存空间。\n在栈中,有两个重要的寄存器\nSP (Stack Pointer)\nBP (Base Pointer)\n在16位系统中是这样的,在32位系统中分别叫做esp ebp,在64位中\n栈是从高地址向低地址搭起来的,因此栈顶位于低地址,栈底位于高地址。一般来说是这样,当然你的程序也可以不这样做,主打一个叛逆。\nESP 函数调用 要理解栈和那两个有关栈的寄存器,可以通过函数调用来理解。\n以下是x86 32bit的汇编程序。\npush 1 push 2 call 002110D add esp,8 nop ; this func will add up a and b mov eax,dword ptr [esp+4] add eax,dword ptr [esp+8] ret 其中push操作码不仅会将参数压入栈中,还会使esp寄存器的值减4,即sub esp,#4。以确保esp始终指向栈顶。\ncall首先会用push来把下一个操作码的地址压入栈中,这个地址叫做返回地址,然后跳转到函数所在的地址。\n在进入到func后,此时esp指向之前call所压入栈内的地址。就可以直接通过exp+4向下拿到参数。\n执行完函数后,就需要考虑栈平衡了。要让调用函数后的栈和调用函数前的保存一致。\nret 跳转到返回地址,执行add esp,8 清除栈内的函数参数。\nEBP 函数调用 但分析C语言的函数调用,会发现与上文的esp寻址有些不同,一般是用ebp来进行参数的寻址。\nint add(int a, int b) { return a + b; } int main() { add(1, 2); return 0; } 这是一段简单的C代码,用gcc将其编译成32位可执行文件\ngcc -m32 -o main main.c\n对于Windows 64 bit要想编译32位程序,如果MinGW的exception model是SEH,那么可以用i686-w64-mingw32-gcc来编译32位程序\n对main.exe进行反汇编\n在上图中可以看到,在调用call前,先处理两个参数,但这里因为编译器等等问题使用了mov而不是push,实现的效果也是一样的。这里没有用push的区别就是esp没有始终指向栈顶,,所以也不用考虑栈平衡了。\n这里就是通过ebp来使用两个形参,分别赋到了EDX, EAX。然后EAX储存函数返回值。\nRefs\nhttps://blog.csdn.net/song_lee/article/details/105297902\n","permalink":"https://moonlab.top/posts/2023/aem-esp-ebp/","summary":"栈是一种遵循后进先出(LIFO)规则的数据结构,通常用于内存管理。重要寄存器包括栈指针(SP)和基指针(BP)。在函数调用中,使用\u003ccode\u003epush\u003c/code\u003e将参数压入栈中,\u003ccode\u003ecall\u003c/code\u003e指令保存返回地址。ESP寄存器指向栈顶,函数执行后需平衡栈。C语言函数调用中,参数通过EBP寻址,编译器可能使用\u003ccode\u003emov\u003c/code\u003e而非\u003ccode\u003epush\u003c/code\u003e,使得ESP不指向栈顶,简化栈平衡的处理。","title":"通过汇编分析栈、函数调用 esp\u0026ebp"},{"content":"在最开始尝试push:\n$ git push -u origin main 结果出现:\nremote: Permission to XXX/repo.git denied to Olduser. fatal: unable to access \u0026#39;https://github.com/xxx/repo.git/\u0026#39;: The requested URL returned error: 403 因为我之前设置了local的username,但是这里却显示了旧用户被拒绝请求了,所以我以为是上一次的commit的author还是旧用户,导致验证不通过。\n所以我再次尝试:\n$ git commit --amend --reset-author 结果还是一样,最后在 stackoverflow 中找到了方法\n把这个凭证删了就行,浪费我1个小时。\n","permalink":"https://moonlab.top/posts/2023/git-err/","summary":"在尝试使用 \u003ccode\u003egit push -u origin main\u003c/code\u003e 时,出现了权限错误,提示旧用户被拒绝访问。为了解决这个问题,作者尝试通过 \u003ccode\u003egit commit --amend --reset-author\u003c/code\u003e 重置提交作者,但问题依旧。最终在 Stack Overflow 找到解决方案:删除旧的凭证,成功解决了问题,节省了一个小时的时间。","title":"Git push 出现 permisson denied error 403"},{"content":"在开发 Madoka 日记时,做到导出导入的功能时,我想偷个懒导出直接用 Gson 将 Java 对象转换为 json 然后放在 txt 中,导入的时候再将 json 转为 javaBean 就行啦。\n而数据库我用的是 Litepal 直接搞,使用这个库的好处就是速度快,方便,适合新手。\n但其实 sqlite 的语法和一些其他的语句并不难,所以用这个库就是为了方便\u0026hellip;\n使用 Litepal 储存数据时,需要调用 save() 方法。\n这里我就遇到了一个问题,明明 save() 方法返回的是 true ,但数据库里没有需要保存的数据啊?\nGson gson = new Gson(); Diary diary = gson.fromJson(json, Diary.class); return diary.save(); 这是调用 Gson 将 json 转换为 javabean 对象。\n明明 Diary 的内容全是正确的,我开始询问度娘。\n结果没找到跟我的问题直接相关的文章,结果我看到了有个人在吐槽啥 setting getting,我就想到了会不会是因为没有调用 Diary 的构造方法?\nDiary 作为一个 Litepal 的,它是继承自 LitePalSupport 的,会不会是因为没有调用 LitePalSupport 的构造方法呢?\n我将导入代码改了一下:\nGson gson = new Gson(); Diary diary = gson.fromJson(json, Diary.class); Diary saveDiary = new Diary(); saveDiary.setId(diary.getId()); saveDiary.setCreatedTime(diary.getCreatedTime()); saveDiary.setLastUpdatedTime(diary.getLastUpdatedTime()); ... 省略一万个代码 return saveDiary.save(); 果然,最后正常了\u0026hellip;\n(水文真开心)\n","permalink":"https://moonlab.top/posts/2020/pit-for-litepal-save/","summary":"在开发 Madoka 日记时,做到导出导入的功能时,我想偷个懒导出直接用 Gson 将 Java 对象转换为 json 然后放在 txt 中,导入的时候再将 json 转为 javaBean 就行啦。\n而数据库我用的是 Litepal 直接搞,使用这个库的好处就是速度快,方便,适合新手。\n但其实 sqlite 的语法和一些其他的语句并不难,所以用这个库就是为了方便\u0026hellip;\n使用 Litepal 储存数据时,需要调用 save() 方法。\n这里我就遇到了一个问题,明明 save() 方法返回的是 true ,但数据库里没有需要保存的数据啊?\nGson gson = new Gson(); Diary diary = gson.fromJson(json, Diary.class); return diary.save(); 这是调用 Gson 将 json 转换为 javabean 对象。\n明明 Diary 的内容全是正确的,我开始询问度娘。\n结果没找到跟我的问题直接相关的文章,结果我看到了有个人在吐槽啥 setting getting,我就想到了会不会是因为没有调用 Diary 的构造方法?\nDiary 作为一个 Litepal 的,它是继承自 LitePalSupport 的,会不会是因为没有调用 LitePalSupport 的构造方法呢?\n我将导入代码改了一下:\nGson gson = new Gson(); Diary diary = gson.","title":"坑:Litepal save方法返回true却没有保存"},{"content":"又是前言 上一篇的 Shizuku 源码分析,我大概从我们开发的应用到 ShizukuService 再到 SystemService 都通了一遍。在文章的结尾我只提到了 moe.shizuku.service 包下的 Starter 这个 Java 类的 main 方法启动了整个 ShizukuService ,所以我们才能愉快地调用 Shizuku 的 API。\n我们接下来要更深入分析,去 Navtive 层看看。我想要知道 Starter 的 main 方法又是谁调用的?怎么调用的?\nShizuku 是什么?我就不再多说了。\nGithub: https://github.com/RikkaApps/Shizuku\n(之后会简单涉及到 Android 中的 NDK 开发,和一些 C++ 的内容)\nstart.sh 当用户使用 ShizukuManager ,通过 ShizukuManager 激活应用时,需要先启动 ShizukuService 。\n这里有 adb 和 root 两种启动方式,我们只分析 adb 方式。\nadb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh 这条 adb 命令想必用过 ShizukuManager 的人都很熟悉吧?只要在 adb 终端输入这条命令,就能激活 ShizukuManager。\n这条命令很简单,就是调用 sh 脚本 start.sh 。这个 start.sh 在源码中 manager 模块的 raw 文件夹中:\n这个 shell 脚本的内容是什么我们等会再看,我们先要知道 ShizukuManager 这个 APP 在启动时干了什么 ,看看 Manager 的 MainActivity 的 onCreate 方法:\nprivate static boolean sWriteFilesCalled; @Override protected void onCreate(Bundle savedInstanceState) { if (!sWriteFilesCalled) { ServerLauncher.writeFiles(this, true); sWriteFilesCalled = true; } } 这里调用了 ServiceLauncher 的 writeFiles 方法,让我们来康康:\npublic static void writeFiles(Context context, boolean external) { // 从 MainActivity 传入的 external 是 true try { File out; if (external) // 执行这里,得到 /sdcard/Android/data/com.example.app/files/ out = context.getExternalFilesDir(null); else out = getParent(context); if (out == null) return; int apiVersion = Math.min(ShizukuLegacy.MAX_SDK, Build.VERSION.SDK_INT); String source = String.format(Locale.ENGLISH, \u0026#34;server-v2-%d.dex\u0026#34;, apiVersion); // 此时 i 为 1 int i = external ? 1 : 0; // copyDex DEX_LEGACY_PATH[i] = copyDex(context, source, new File(out, V2_DEX_NAME)); DEX_PATH[i] = copyDex(context, \u0026#34;server.dex\u0026#34;, new File(out, V3_DEX_NAME)); // 注意注意这个 writeShellFile 方法 String command = writeShellFile(context, new File(out, \u0026#34;start.sh\u0026#34;), DEX_LEGACY_PATH[i], DEX_PATH[i]); // external 为 true ,不执行 if (!external) { COMMAND_ROOT = command; } } catch (IOException e) { e.printStackTrace(); } } 看到 writeShellFile 这个方法了吗?里面的参数是一个 File 类和一个 \u0026ldquo;start.sh\u0026rdquo;,还有两个 dex 文件路径。\n继续看 ServiceLauncher#writeShellFile:\nprivate static String writeShellFile(Context context, File out, String dexLegacy, String dex) throws IOException { if (!out.exists()) { //noinspection ResultOfMethodCallIgnored out.createNewFile(); } BufferedReader is = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.start))); PrintWriter os = new PrintWriter(new FileWriter(out)); String line; while ((line = is.readLine()) != null) { // 这里将 start.sh 写出,并把 start.sh 的那些文字替换成路径值。 os.println(line .replace(\u0026#34;%%%STARTER_PATH%%%\u0026#34;, getLibPath(context, \u0026#34;libshizuku.so\u0026#34;)) .replace(\u0026#34;%%%STARTER_PARAM%%%\u0026#34;, getStarterParam(dexLegacy, dex)) .replace(\u0026#34;%%%LIBRARY_PATH%%%\u0026#34;, getLibPath(context, \u0026#34;libhelper.so\u0026#34;)) ); } os.flush(); os.close(); return \u0026#34;sh \u0026#34; + out.getAbsolutePath(); } 还记得我们之前看的那个 start.sh 的内容吗?这里实际上就是把 raw 内的 start.sh 写出放到 /sdcard/Android/data/moe.shizuku.privileged.api/files。\n这就是为什么那条激活用的 adb 命令能够执行:\nadb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh 看到了吗?如果你安装了 ShizukuManager 并打开了,你可以在 /sdcard/Android/data/moe.shizuku.privileged.api/files 目录里找到这个 start.sh 和两个 dex 文件。\n这俩 dex 文件就是通过 ServiceLauncher#writeFiles 用 copyDex 方法把 assets 的 dex 写出到SD目录。\n同时 start.sh 中的那开头的三个变量也在写出的时候被 ShizukuManager 替换了:\n#!/system/bin/sh // 这三个变量在 ShizukuManager 写出时被替换了 STARTER_PATH=\u0026#34;%%%STARTER_PATH%%%\u0026#34; // 对应 libshizuku.so 文件路径 STARTER_PARAM=\u0026#34;%%%STARTER_PARAM%%%\u0026#34; // 对应调用 main 函数的参数,里面包含了那两个 dex 文件的路径和 token LIBRARY_PATH=\u0026#34;%%%LIBRARY_PATH%%%\u0026#34; // 对应 libhelper.so 文件路径 echo \u0026#34;info: start.sh begin\u0026#34; if [[ -f \u0026#34;$STARTER_PATH\u0026#34; ]]; then rm -f /data/local/tmp/shizuku_starter // 将 libshizuku.so 文件移动到 // 实际上 /data/local/tmp/ 下的 shizuku_starter 文件就是 libshizuku.so 文件 cp \u0026#34;$STARTER_PATH\u0026#34; /data/local/tmp/shizuku_starter // 修改权限 chmod 700 /data/local/tmp/shizuku_starter chown 2000 /data/local/tmp/shizuku_starter chgrp 2000 /data/local/tmp/shizuku_starter // 这似乎是在修改 Selinux 啥的什么东西的啥子命令 chcon u:object_r:shell_data_file:s0 /data/local/tmp/shizuku_starter // 设置环境变量 export PATH=/data/local/tmp:/system/bin:$PATH // 调用 shizku_starter so文件(原来是 libshizuku.so)的 main 方法,传入参数 shizuku_starter ${STARTER_PARAM} $1 // 返回上 shizuku_starer 内 main 函数的返回值 result=$? if [[ ${result} -ne 0 ]]; then echo \u0026#34;info: shizuku_starter exit with non-zero value $result\u0026#34; else echo \u0026#34;info: shizuku_starter exit with 0\u0026#34; fi else echo \u0026#34;Starter file not exist, please open Shizuku Manager and try again.\u0026#34; fi 当我们输入那条用来激活 ShizukuManager 的 adb 命令时候,输出框会噼里啪啦滚出一大堆输出。那些输出是哪里来的?很明显在 start.sh 中没有几条 echo 命令,只能是在 shizuku_starter 的 main 方法里有什么大动作。\n这个 libshizuku.so 文件的原身你可以在 ShizukuManager 源码中的 jni 文件夹中找到,文件名是 starter.cpp。\n没错,它是一个 C++ 文件,我们看看它的 main 方法(我会适当地跳过一些与主题无关的代码):\n#define SERVER_CLASS_PATH_LEGACY \u0026#34;moe.shizuku.server.ShizukuServer\u0026#34; #define SERVER_CLASS_PATH \u0026#34;moe.shizuku.server.Starter\u0026#34; int main(int argc, char **argv) { // argc 是传进来的参数的数量,argv是传入的参数的数组。里面包含了 token, 两个 dec 文件的路径 ... char *token = nullptr; char *_path = nullptr; char *_path_legacy = nullptr; int v2 = 1; int i; int use_shell_context = 0; // for 循环,可以看到这是在取出参数 for (i = 0; i \u0026lt; argc; ++i) { if (strncmp(argv[i], \u0026#34;--token=\u0026#34;, 8) == 0) { token = strdup(argv[i] + 8); } else if (strncmp(argv[i], \u0026#34;--path=\u0026#34;, 7) == 0) { _path = strdup(argv[i] + 7); } else if (strncmp(argv[i], \u0026#34;--path-legacy=\u0026#34;, 14) == 0) { _path_legacy = strdup(argv[i] + 14); } else if (strncmp(argv[i], \u0026#34;--no-v2\u0026#34;, 7) == 0) { v2 = 0; } else if (strncmp(argv[i], \u0026#34;--use-shell-context\u0026#34;, 19) == 0) { use_shell_context = 1; } } ... // check_acess 会调用 acess 函数判断路径是否有读写权限 check_access(_path, \u0026#34;source dex path\u0026#34;); if (v2) check_access(_path_legacy, \u0026#34;source legacy dex path\u0026#34;); // 这里我是没看懂的,不知道为什么又建了一个 /data/local/tmp/shizuku 文件夹,将之前 _path 内的文件全部移动到了 path 这个路径里。 mkdir(\u0026#34;/data/local/tmp/shizuku\u0026#34;, 0707); chmod(\u0026#34;/data/local/tmp/shizuku\u0026#34;, 0707); ... // 一个 char 数组,也就相当于一个 string char path[PATH_MAX], path_legacy[PATH_MAX]; // 格式化字符,也就是为 path 赋值了 sprintf(path, \u0026#34;/data/local/tmp/shizuku/%s\u0026#34;, basename(_path)); sprintf(path_legacy, \u0026#34;/data/local/tmp/shizuku/%s\u0026#34;, basename(_path_legacy)); // 开始复制了,将 _path 路径下的两个 dex 移动到 path 路径 copy_if_not_exist(_path, path); if (v2) copy_if_not_exist(_path_legacy, path_legacy); check_access(path, \u0026#34;dex path\u0026#34;); if (v2) check_access(path_legacy, \u0026#34;legacy dex path\u0026#34;); printf(\u0026#34;info: starter begin\\n\u0026#34;); // 强制输出缓冲区的信息,就是为了快速输出上面的 printf 内容啦 fflush(stdout); ... printf(\u0026#34;info: starting server v3...\\n\u0026#34;); fflush(stdout); // 我们重点分析这个 start_service 函数,传入 SERVICE_CLASS_PATH 这个常量,值为 \u0026#34;moe.shizuku.server.Starter\u0026#34;,熟悉吗?这就是在应用层的主角啊,用于启动 ShizukuService 的 Java 类。 start_server(path, SERVER_CLASS_PATH, token, SERVER_NAME, use_shell_context); if (v2) { printf(\u0026#34;info: starting server v2 (legacy)...\\n\u0026#34;); fflush(stdout); start_server(path_legacy, SERVER_CLASS_PATH_LEGACY, token, SERVER_NAME_LEGACY, false); } exit_with_logcat(EXIT_SUCCESS); // starter.cpp 结束 } 注释上已经大概地说了一遍,我们接下来看 start_service 函数,同样 start_service 也是 starter.cpp 中的一个函数:\nstatic int start_server(const char *path, const char *main_class, const char *token, const char *nice_name, int change_context) { ... /* 省略了很多内容,详细可自己去看看 Shizuku 的源码 */ char buf[128], class_path[PATH_MAX]; sprintf(buf, \u0026#34;--nice-name=%s\u0026#34;, nice_name); setClasspathEnv(path); snprintf(class_path, PATH_MAX, \u0026#34;-Djava.class.path=%s\u0026#34;, path); char *appProcessArgs[] = { // app_process 是 Android 里专门用来启动 Java 程序的 const_cast\u0026lt;char *\u0026gt;(\u0026#34;/system/bin/app_process\u0026#34;), class_path, const_cast\u0026lt;char *\u0026gt;(\u0026#34;/system/bin\u0026#34;), const_cast\u0026lt;char *\u0026gt;(buf), // main_class 值为 \u0026#34;moe.shizuku.server.Starter\u0026#34; const_cast\u0026lt;char *\u0026gt;(main_class), const_cast\u0026lt;char *\u0026gt;(token), nullptr }; // 调用 app_process 去运行 main_class if (execvp(appProcessArgs[0], appProcessArgs)) { exit_with_logcat(EXIT_FATAL_APP_PROCESS); } ... } 到这里我们的分析已经差不多了。\n我们再仔细说说 app_process 的内容:\n调用 app_process 的话,你需要在参数中提供 dex 文件,同时必须要使 dex 文件有执行权限\napp_process -Djava.class.path=dex文件名 dex所处的目录路径 你要启动的 Java 类(有类名和包名) 我找来了一个参数列表:\nvm-options – VM 选项 cmd-dir –父目录 (/system/bin) options –运行的参数 : –zygote –start-system-server –application (api\u0026gt;=14) –nice-name=nice_proc_name (api\u0026gt;=14) start-class-name –包含main方法的主类 (com.android.commands.am.Am) main-options –启动时候传递到main方法中的参数 小总结 用户通过输入一条 adb 命令去执行 start.sh 文件,start.sh 文件会执行 ShizukuManager 早已准备好的 so 文件。在 so 文件中经过一大堆操作,会通过 app_process 运行 Starter 类的 main 方法,而 Starter 这个 Java 类的 main 方法会直接 new 一个 ShizukuService 使它跑起来。\n看了一上午的代码,翘了一上午的在铛铛的网络课。\n尼玛,都延长假期了和上个屎的网络课。\n","permalink":"https://moonlab.top/posts/2020/android-shizuku-theory2/","summary":"本文深入分析了 Shizuku 的启动过程,特别是 \u003ccode\u003eStarter\u003c/code\u003e 类的 \u003ccode\u003emain\u003c/code\u003e 方法是如何被调用的。文章首先回顾了 Shizuku 的基本概念,并介绍了通过 \u003ccode\u003eadb\u003c/code\u003e 命令激活 ShizukuService 的过程,重点分析了 \u003ccode\u003estart.sh\u003c/code\u003e 脚本的执行。当用户通过 ShizukuManager 启动应用时,\u003ccode\u003estart.sh\u003c/code\u003e 脚本被调用,该脚本负责设置环境并执行 \u003ccode\u003elibshizuku.so\u003c/code\u003e 文件。文章详细描述了 \u003ccode\u003eShizukuManager\u003c/code\u003e 在启动时如何写入 \u003ccode\u003estart.sh\u003c/code\u003e 文件及其相关的 dex 文件路径。接着,文章探讨了 \u003ccode\u003elibshizuku.so\u003c/code\u003e 中的 \u003ccode\u003emain\u003c/code\u003e 方法,说明了如何通过 \u003ccode\u003eapp_process\u003c/code\u003e 启动 Java 类 \u003ccode\u003emoe.shizuku.server.Starter\u003c/code\u003e。在 \u003ccode\u003eStarter\u003c/code\u003e 类的 \u003ccode\u003emain\u003c/code\u003e 方法中,创建了 \u003ccode\u003eShizukuService\u003c/code\u003e 实例,从而完成了 ShizukuService 的启动。最后,作者总结了整个过程,强调了用户通过简单的 \u003ccode\u003eadb\u003c/code\u003e 命令如何触发一系列复杂的操作,最终实现 Shizuku 的功能。","title":"Android Shizuku源码分析 第二篇"},{"content":"前言 上一篇文章我使用了 Shizuku 去调用系统API:文章链接\n这次就来看看 Shizuku 的源码是怎么写的。\n最开始我用 Notepad++ 看,最后还是用 as 看源码好点吧。\n官方文档\n酷安下载 ShizukuManager\nWatch it on GitHub\nShizuku 是什么? Shizuku app 会引导用户使用 root 或是 adb 方式运行一个进程(Shizuku 服务进程)。\n应用进程启动时 Shizuku 服务进程发送 binder 至应用进程 应用通过该 binder 与 Shizuku 服务进程交互,Shizuku 服务进程通过 binder 与 system server 交互 正文 ShizukuBinderWrapper 首先我们看,在开发时调用 Shizuku 的代码:\nprivate static final IPackageManager PACKAGE_MANAGER = IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(\u0026#34;package\u0026#34;))); 它使我们能随意调用 Android 系统中 IPackageManager 内的方法,也就是系统隐藏的API(有 @hide 标签的方法)\n我们可以看到 ShizukuBinderWrapper 的构造方法内传入了一个 SystemServiceHelper#getSystemService 方法的返回值。\nprivate static Map\u0026lt;String, IBinder\u0026gt; systemServiceCache = new HashMap\u0026lt;\u0026gt;(); public static IBinder getSystemService(@NonNull String name) { IBinder binder = systemServiceCache.get(name); if (binder == null) { binder = ServiceManager.getService(name); systemServiceCache.put(name, binder); } return binder; } getSystemService 方法先查询了一下缓存,如果没有缓存就调用 ServiceManager 的 getService 方法。这个本地的 ServiceManager 是在 android.os 包下:\npackage android.os; public class ServiceManager { public static IBinder getService(String name) { throw new UnsupportedOperationException(); } } getService 方法会根据 name 参数返回一个 IBinder 接口,我们可以查一下 Android 源码中的 ServiceManager 类:\npublic final class ServiceManager { /** * Returns a reference to a service with the given name. * * @param name the name of the service to get * @return a reference to the service, or \u0026lt;code\u0026gt;null\u0026lt;/code\u0026gt; if the service doesn\u0026#39;t exist */ public static IBinder getService(String name) { try { IBinder service = sCache.get(name); if (service != null) { return service; } else { return Binder.allowBlocking(rawGetService(name)); } } catch (RemoteException e) { Log.e(TAG, \u0026#34;error in getService\u0026#34;, e); } return null; } } 其中 getService 不是 hide 方法可以直接调用。\n我们看看 ShizukuBinderWrapper 的构造参数:\nprivate IBinder original; // original 是 ServiceManager.getService 返回的 IBinder public ShizukuBinderWrapper(@NonNull IBinder original) { // 使用 requireNonNull 方法判断是否为 null,如果为 null 抛出空指针异常。 this.original = Objects.requireNonNull(original); } 这个构造参数对 original 进行了赋值,这是一个 IBinder。\n我们将这个 ShizukuBinderWrapper 传入Binder 的 asInterface 就能获得适用于客户端的 Binder 随意调用 IPackageiManager 内的方法,其中我在本地写的 IPackageManager 只是个空壳接口,只继承了 IInterface 声明了需要的方法:\npackage android.content.pm; // 与 Android 系统内的 IPackageManager 在同一个包下 import android.os.Binder; import android.os.IBinder; import android.os.IInterface; import android.os.RemoteException; public interface IPackageManager extends IInterface { // 我只需要这个方法,所以只声明这个方法来调用 ParceledListSlice\u0026lt;PackageInfo\u0026gt; getInstalledPackages(int flags, int userId) throws RemoteException; abstract class Stub extends Binder implements IPackageManager { public static IPackageManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } 我们查看 ShizukuBinderWrapper 的源码, ShizukuBinderWrapper 本身就实现了 IBinder 接口,所以可以直接作为上述 asInterface 方法的参数。\n这是 ShizukuBinderWrapper 中的 transact 方法的重写:\n/* ShizukuBinderWrapper#transact */ @Override public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { Parcel newData = Parcel.obtain(); try { // 使用 writeInterfaceToken 写入 ShizukuApiConstants.BINDER_DESCRIPTOR,用于后面服务端的判断 newData.writeInterfaceToken(ShizukuApiConstants.BINDER_DESCRIPTOR); // 写入了 original 这个 Binder newData.writeStrongBinder(original); newData.writeInt(code); newData.appendFrom(data, 0, data.dataSize()); ShizukuService.transactRemote(newData, reply, flags); } finally { newData.recycle(); } return true; } 我们都知道,在 aidl 中我们调用 Binder 的方法时实际上都调用了这个 transact 方法。\n例如:\nprivate static final IPackageManager PACKAGE_MANAGER = IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(\u0026#34;package\u0026#34;))); ParceledListSlice\u0026lt;PackageInfo\u0026gt; listSlice = PACKAGE_MANAGER.getInstalledPackages(flags, userId); 这里我调用 getInstalledPackages 方法时,方法的内部实际上就调用了上面 ShizukuBinderWrapper 这个 Binder 的 transact 方法。在调用 getInstalledPackages 这个系统方法时,它内部就把我们调用系统方法所传入的参数写入了 data 中再传入这个 ShizukuBinderWrapper 的 transact 方法。\nShizukuBinderWrapper 的 transact 方法又把 data 和 reply 传入了 ShizukuService 的 transactRemote 下,这个 ShizukuService 是一个普通的类里面有一些静态变量和方法。\npackage moe.shizuku.api; /* ShizukuService#transactRemote */ public static void transactRemote(@NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { // 调用 ShizukuService 的 requireService 方法 requireService().asBinder().transact(ShizukuApiConstants.BINDER_TRANSACTION_transact, data, reply, flags); } 我们可以看到,这个方法由调用了 requireService 方法取得了 Binder 再调用 Binder 的 transact 方法与服务端通信 ,再看看 requireService 方法:\nprivate static IShizukuService requireService() { if (getService() == null) { throw new IllegalStateException(\u0026#34;Binder haven\u0026#39;t received, check Shizuku and your code.\u0026#34;); } return getService(); } 没什么好解释,继续看 getService 方法。\n/* ShizukuService */ private static IShizukuService sService; public static void setBinder(IBinder binder) { sService = IShizukuService.Stub.asInterface(binder); } private static IShizukuService getService() { return sService; } 这个 getService 静态方法会返回一个 IShizukuService 类型的 mService,而这个 mService 是由 setBinder 设置的,那么是谁调用了这个 ShizukuService.setBinder 方法来设置 mService ?我们先不管它。\nIShizukuService 我们再注意一下这个 IShizukuService:\n/* IShizukuService.aidl */ package moe.shizuku.server; import moe.shizuku.server.IRemoteProcess; interface IShizukuService { int getVersion() = 2; int getUid() = 3; int checkPermission(String permission) = 4; String getToken() = 5; boolean setPidToken(in String token) = 6; IRemoteProcess newProcess(in String[] cmd, in String[] env, in String dir) = 7; String getSELinuxContext() = 8; } 这个 AIDL 的作用是让我们开发的应用与 Shhizuku 的系统进程进行 Binder 通信。\n我们之前的 transactRemote 方法已经调用了它的 Binder 的 transact 方法开始联系服务端挂起客户端了。\n我们再看与 IShizukuService 相关的类:\npackage moe.shizuku.service public class ShizukuService extends IShizukuService.Stub { ... } 它不同于我们刚才看到的 moe.shizuku.api 下的 ShizukuService ,这是 moe.shizuku.service 下的 ShizukuService。是一个真真正正的服务端,和 Android 系统中的系统服务(例如AMS, PMS)类似没有直接继承 Service 类,而是继承自一个 Binder(后面我们再说)。\n接下来我们看它重写的 onTransact 方法:\n@Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { if (code == ShizukuApiConstants.BINDER_TRANSACTION_transact) { data.enforceInterface(ShizukuApiConstants.BINDER_DESCRIPTOR); transactRemote(data, reply, flags); return true; } return super.onTransact(code, data, reply, flags); } 先用 enforceInterface 方法检查一下 Interface 是否与客户端的相同,再调用了 transactRemote 方法:\nprivate void transactRemote(Parcel data, Parcel reply, int flags) throws RemoteException { IBinder targetBinder = data.readStrongBinder(); int targetCode = data.readInt(); enforceCallingPermission(\u0026#34;transactRemote\u0026#34;, true); targetBinder.getInterfaceDescriptor(), targetCode); Parcel newData = Parcel.obtain(); try { newData.appendFrom(data, data.dataPosition(), data.dataAvail()); } catch (Throwable tr) { LOGGER.w(tr, \u0026#34;appendFrom\u0026#34;); return; } try { long id = Binder.clearCallingIdentity(); // here targetBinder.transact(targetCode, newData, reply, flags); Binder.restoreCallingIdentity(id); } finally { newData.recycle(); } } 我们可以看到 targetBinder 其实就是我们之前用 SystemServiceHelper 的 getService 方法得到的 Binder,在调用它的 transact 方法,实现 Shizuku 服务与你想要通信的系统服务进行 Binder 通信,毕竟 Shizuku 服务已经通过 adb 或 root 成为了 dalao。\nShizukuService 的启动 上面我们看到了我们用到了 ShizukuService 去完成应用与系统服务的通信,而这个静态变量 mService 是通过 ShizukuService#setBinder 方法设置的:\npackage moe.shizuku.api; public class ShizukuService { private static IShizukuService sService; public static void setBinder(IBinder binder) { sService = IShizukuService.Stub.asInterface(binder); } } 首先,Starter 类中的 main 方法会启动 ShizukuService 服务:\npublic static void main(String[] args) throws IOException, RemoteException, InterruptedException { fixFilesOwner(); waitServiceManager(); waitSystemService(\u0026#34;package\u0026#34;); waitSystemService(\u0026#34;activity\u0026#34;); waitSystemService(Context.USER_SERVICE); waitSystemService(Context.APP_OPS_SERVICE); checkManagerApp(); if (Build.VERSION.SDK_INT \u0026gt;= 28) { disableHiddenApiBlacklist(); } LOGGER.i(\u0026#34;server v3\u0026#34;); Looper.prepare(); // 这是 moe.shizuku.service 包下的继承自 Binder 的 ShizukuService ShizukuService server = new ShizukuService(getToken(args)); server.sendBinderToManager(); server.sendBinderToClients(); Looper.loop(); LOGGER.i(\u0026#34;server exit\u0026#34;); System.exit(0); } 可以看到这里直接 new 了一个 ShizukuService 实例,这和一些系统服务启动方法类似。\n其中调用了 ShizukuService 的 sendBinderToClients 方法,这中间有一大堆 dalao 操作,我们不作详细关注,泥萌可自行阅读源码。\n最后会调用 ShuzikuService 的 sendBinderToUserApp 方法,略过了一些其他的代码:\nstatic void sendBinderToUserApp(Binder binder, String packageName, int userId) { ... Bundle extra = new Bundle(); extra.putParcelable(ShizukuApiConstants.EXTRA_BINDER, new BinderContainer(binder)); Bundle reply = IContentProviderHelper.call(provider, null, name, \u0026#34;sendBinder\u0026#34;, null, extra); ... } 将 ShizukuService 这个 Binder 包装成一个 BinderContainer 类然后放入 Bundle 里。\n这个 IContentProviderHelper 的 call 方法会跨进程调用 ShizukuBinderReceiveProvider 这个 ContentProvider 的 call 方法:\n@Nullable @Override public final Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { if (extras == null) return null; Bundle reply = new Bundle(); extras.setClassLoader(BinderContainer.class.getClassLoader()); switch (method) { case METHOD_SEND_BINDER: { if (ShizukuService.pingBinder()) { Log.i(\u0026#34;ShizukuClient\u0026#34;, \u0026#34;ShizukuBinderReceiveProvider started when already a binder alive\u0026#34;); break; } BinderContainer container = extras.getParcelable(ShizukuApiConstants.EXTRA_BINDER); if (container != null \u0026amp;\u0026amp; container.binder != null) { Log.i(\u0026#34;ShizukuClient\u0026#34;, \u0026#34;binder received\u0026#34;); // 调用 ShizukuService 的 setBinder 静态方法将静态变量 mService 设置成之前在 app_process 进程启动的 ShizukuService 的 Binder ShizukuService.setBinder(container.binder); //noinspection ConstantConditions Intent intent = new Intent(ShizukuMultiProcessHelper.ACTION_BINDER_RECEIVED) .putExtra(ShizukuApiConstants.EXTRA_BINDER, container) .setPackage(getContext().getPackageName()); getContext().sendBroadcast(intent); } ... break; } ... } return reply; } 其中调用了 ShizukuService 的 setBinder 方法,将 IShuzukuService 传进去了。\n末语 实际上这只分析了一部分,关于 ShizukuService 具体的如何启动,这个 main 方法是怎么调用的,和关于 start.sh 的一些问题,或许有生之年我能水写出第二篇文章\u0026hellip;\nShizuku 内还有好多 dalao操作,我都没有去看,只把重要的一部分写了下来。\n果然只有 dalao才能这样操作,我还是继续躺着吧。\nOrz\n2020-02-04:\n时隔两天,我又写出了第二篇文章啦:https://lcblog.cn/post/android-shizuku-theory2\n尝试具体分析 Starter 类是如何启动的。\n","permalink":"https://moonlab.top/posts/2020/android-shizuku-theory/","summary":"本文分析了 Shizuku 的源码,介绍了其如何通过 Binder 实现与 Android 系统服务的交互。Shizuku 应用引导用户以 root 或 adb 方式运行服务进程,利用 ShizukuBinderWrapper 进行系统隐藏 API 的调用。文章详细探讨了 ShizukuBinderWrapper 的构造、transact 方法及其与 IShizukuService 的通信过程,最后简要提及 ShizukuService 的启动流程。作者计划进一步深入分析 Shizuku 的启动机制。","title":"Android Shizuku源码分析"},{"content":"前言 距离上一次更新也已经是六个月前了。感觉2019年过得真不咋样,什么也没干成吧,没有什么进步。\n不知不觉又是新的一年,2019是去年,2018就成了前年了。\n这个冬天我们这都没怎么下过雪,积雪的话我知记得有一次,融化得还很快。\n国内也出现了传染病,迄今为止有五千多人中枪了吧。现在也不让出去,亲戚也都没有拜完,现在天天在家窝。(不过好处就是不用上学啦)。\n今年的央视春晚也没有看,bilibili还搞了拜年祭,拜年祭真的是一年比一年棒了,可惜我也没兴趣看。\n最近在补魔法少女小圆,看得很慢。1月新番也没啥期待的,只有一部喜欢的:某科学电池炮T,毕竟是我的入坑番。还有一部是异度入侵,我不太喜欢目前的剧情,唯一喜欢的就只有OP了。\n哦其实还有Acfun独播的达尔文游戏,也还可以~~(因为我喜欢女主的性格)~~\n总结:一月新番喜欢的只有一部番和一首歌,没了。\n感觉越来越没意思了,也可能是我心态的变化,现在我都一直在看以前的番,新番我已经没啥可期待的了。\n这篇文章磨磨唧唧花费了三天,我已经把魔法少女小圆TV版看完了,太特喵好看了,还有剧场版和外传又够我看一阵子了。\n好多人都说“致郁”,其实我觉得还可以啊并不怎么致郁,相反还有些治愈(迫真脸)。\n[TOC]\n正文 回归正题,我在开发的过程中有一个小需求:需要监听每一个Activity的启动,并且需要获取Activity的ClassName。\n我这种菜鸡只能先求助百度:Android 监听第三方Activity\n方法1 首先百度到的第一个方法是搞一个Service,然后在一个循环里一直获取栈顶的Activity。\nemmmmmm\u0026hellip;.\nList\u0026lt;ActivityManager.RunningTaskInfo\u0026gt; runningTasks = am.getRunningTasks(1); ActivityManager.RunningTaskInfo runningTaskInfo = runningTasks.get(0); 这个办法肯定是不行哒,Google早已对getRunningTasks方法严加管制,需要android.permission.GET_TOP_ACTIVITY_INFO权限,但是这个权限必须要系统app才能申请。\n但或许能用shell指令 ps 来获取栈顶的 Activity,虽然我没试过。\n方法2 (???) 第二个方法是使用 UsageStatsManager,也就是应用使用情况的一个功能。\n但是只能得到应用的包名,而不是Activity的类名。\n方法3 在我百度的时候我注意到了一个接口:IActivityController和一个方法:setActivityController。\n啊,决定了,就是你啦!\n我开始了解有关IActivityController的详细内容。\n我还发现了一个比AndroidXref稍微更友好的Android源码在线查看网站:https://www.androidos.net.cn/\n本来setActivityController这个方法是在ActivityManagerNative中的,但是在Android API 26的时候似乎被遗弃了,取而代之的是IActivityManager中的setActivityController。\n在之前,启动Activity的是ActivityManagerNative,getDefault()内的startActivity方法,而ActivityManagerService继承自ActivityManagerNative,ActivityManagerNative又继承自Binder并实现iActivityManager,AMS是IActivityManager的具体实现。\nIInterface, Stub, Proxy 三个类是分离的,分别是 IActivityManager,ActivityManagerNative (extends Binder) 和 ActivityManagerProxy。\n而现在(API 26+),AMS直接继承了 IActivityManager的内部类 Stub:\n这时候 Android 源码已经有了 IActivityManager.aidl,ActivityManagerNative 这个类被弃用了\npublic class ActivityManagerService extends IActivityManager.Stub implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback { ... } 那么自然而然,setIActivityController 这个方法是在 IActivityManager.aidl 内定义的,\n而 IActivityController 是一个在 Binder 中的回调接口,可以直接下载 IActivityController.aidl 放在 android.app 这个包下。要与 Android 系统源码的位置一致。\n但是该怎么调用 系统API 呢?除非我们的应用得在系统进程。\n这时候我在酷安看到了一个叫做 Shizuku 的玩意:官方文档\n它可以帮助我们快速使用 系统API,用户只需用 adb 或者 root 激活 Shizuku,看看官方文档的解释:\nShizuku 做法 Shizuku app 会引导用户使用 root 或是 adb 方式运行一个进程(Shizuku 服务进程)。\n应用进程启动时 Shizuku 服务进程发送 binder 至应用进程 应用通过该 binder 与 Shizuku 服务进程交互,Shizuku 服务进程通过 binder 与 system server 交互 Shizuku 的优点在于:\n极小额外时间及性能消耗 与直接调用 API 体验几乎一致(应用开发者只许添加少量代码) 导入 Shizuku 的开源库,先在 android.app 包内创建一个同名文件:!ActivityManager.java\n假装他是 IActivityManager.aidl 自动生成的一个类(我们无需在下载 IActivityManager.aidl 放在项目里)。\npackage android.app; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; /* 我们自己创建的 IActivityManager */ public interface IActivityManager extends IInterface { void setActivityController(IActivityController watcher, boolean imAMonkey); abstract class Stub extends Binder implements IActivityManager { public static IActivityManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } 结构和 aidl 自动生成的类没什么两样,就是只把关键的方法写出来。\n注意哦,这个 Stub 是一个抽象类!asInterface 方法并没有具体的实现。\n之后我们再:\nprivate static final IActivityManager ACTIVITY_MANAGER = IActivityManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(\u0026#34;activity\u0026#34;))); public static void setActivityController(IActivityController activityController) { if (!ShizukuService.pingBinder()) { return; } ACTIVITY_MANAGER.setActivityController(activityController, true); } 这样我们就能直接调用 setActivityController 方法了,然后在重写 IActivityController.Stub 内的回调方法就行了。\n","permalink":"https://moonlab.top/posts/2020/android-activity-monitor/","summary":"这篇文章主要记录了作者在2019年初的个人感受和一些技术探索。作者回顾了过去一年的经历,感到没有太多进展,并提到冬季的天气和国内的传染病情况。尽管在家中待得久了,作者也提到了一些娱乐活动,比如观看动画和春晚,但整体上对新番的期待逐渐减弱。在技术方面,作者分享了在Android开发中监听Activity启动的需求,探讨了几种方法来获取Activity的类名。首先,作者提到使用\u003ccode\u003eActivityManager\u003c/code\u003e的\u003ccode\u003egetRunningTasks\u003c/code\u003e方法,但由于权限限制,这种方法不可行。接着,作者提到\u003ccode\u003eUsageStatsManager\u003c/code\u003e只能获取应用包名,而无法获取Activity类名。最终,作者发现了\u003ccode\u003eIActivityController\u003c/code\u003e接口,并通过使用Shizuku工具来调用系统API,从而实现了监听Activity启动的功能。总结来说,文章结合了个人生活感受与技术探索,展示了作者在面对生活和技术挑战时的思考与解决方案。","title":"Android 监听第三方Activity的一举一动"},{"content":"咕咕咕 基本上本文是把《Android艺术开发探索》这本书中的 View 事件分发机制 这一章节总结一下。\n很久以前的 Android 开发笔记太鸡儿水了。\n简单介绍 众所周知,一个事件序列是由 MotionEvent.ACTION_DOWN(按下) 开始,多个 MotionEvent.MOVE(移动) 和一个 MotionEvent.ACTION_UP(抬起) 结束。\n一个事件的传递顺序:Activity - \u0026gt; Window -\u0026gt; DecorView - \u0026gt; RootView (你所设置的 View)\nViewGroup 是继承自 View 的,这点也是基础中的基础了。\n事件的分发过程主要由以下三个方法来完成:\npublic boolean dispatchTouchEvent(MotionEvent ev); // 如果事件传到此 View 那么该方法一定会被调用 public boolean onInterceptTouchEvent(MotionEvent event); // 用来判断 View 是否拦截此事件,如果拦截那么在这事件序列中此方法不会再被调用 public boolean onTouchEvent(MotionEvent event); // 用来处理事件,返回是否消耗当前事件,如果不消耗那么在同一事件序列不会再让此 View 接收到。 下面我们附上一段伪代码来阐述三个方法的关系:\npublic boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; // onInterceptTouchEvent判断是否拦截此事件 if (onInterceptTouchEvent(ev)) { // 拦截的话,则调用 onTouchEvent 处理事件,返回是否消耗此事件 consume = onTouchEvent(ev); } else { // 不拦截交给子类 View consume = child.dispatchTouchEvent(ev); } // 此方法的返回值为是否拦截此事件。 return consume; } 注意调用 dispatchTouchEvent 时往往第一个是 ACTION_DOWN 事件,请搞清“拦截”和“消耗”两个概念,onInterceptTouchEvent 返回的是是否拦截,onTouchEvent 返回的是是否消耗。\n如果 onInterceptTouchEvent 返回 true ,但 onTouchEvent 返回 false,即代表拦截但不消耗事件,如果这个事件是 ACTION_DOWN,那么同一事件序列的其他事件将不会再交给此 View 了,这时会调用上一级的 onTouchEvent,如果还是 false ,那么就调用上一级的上一级的 onTouchEvent,如果都是 false,那么最终会交给 Activity 处理。\nonTouchEvent 的返回值是取决于 View 的 clickable 和 longClickable 属性的,只要其中一个为 true,那么 onTouchEvent 就会返回 true,与 enable 属性无关。\n如果 View 不消耗除 ACTION_DOWN 以外的事件,那么父 View 的 onTouchEvent 不会被调用,并且此 View 仍然可以接收到事件,不消耗的事件将直接交给 Activity 处理。\nAndroid 源码中 ViewGroup 的 onInterceptTouchEvent 默认会返回 false,而 View 则没有这个方法,会直接调用 onTouchEvent。\n如果一个 View 设置了 OnTouchListener 那么里面的 onTouch 方法则会被回调,我们都知道 onTouch 会返回一个 boolean ,onTouchEvent 方法是否被调用是取决于这个 boolean 的,如果返回 false,那么 onTouchEvent 则会被调用,我们经常设置的 OnClickListener 是在 onTouchEvent 中的。\n从上面可以看出, onTouchListener 的优先级高于 onTouchEvent,而 OnClickListener 则是优先级最低的。\n在网上翻到了一张图,总结得挺好:https://upload-images.jianshu.io/upload_images/2435754-a09ab44cb25be80d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/972/format/webp\nAndroid 源码分析 事件最开始会调用到 Activity 的 dispatchTouchEvent 方法:\npublic boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } 事件开始交给 Activity 所属的 Window 进行分发,Window 的实现类是 PhoneWindow ,我们继续来看 PhoneWindow#superDispatchTouchEvent:\nprivate DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 很明显,Window 又把事件交给了 DecorView ,就是你所设置的布局的父 View。\nDecorView#superDispatchTouchEvent\npublic boolean superDispatchTouchEvent(MotionEvent event) { // 这里又将事件传到了 ViewGroup, DecorView 是继承自 ViewGroup 的 return super.dispatchTouchEvent(event); } 我们所设置的 View 称作为根 View 或 顶级 View。\n至此,事件已经传递到我们的 View 中了。\nViewGroup 源码解析 我们来看看 ViewGroup 中的 dispatchTouchEvent 方法的一小段,因为 Android 源码实在过于复杂,我们只需要专注我们需要专注的内容就好。\n// 这个地方是 ViewGroup 是否拦截事件的一个逻辑 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 当 ViewGroup 满足 actionMasked == MotionEvent.ACTION_DOWN 或 mFirstTouchTarget != null 且没有设置标记位就会调用onInterceptTouchEvent // FLAG_DISALLOW_INTERCEPT 标记位是由 requestDisallowInterceptTouchEvent 方法设置的 final boolean disallowIntercept = (mGroupFlags \u0026amp; FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // onInterceptTouchEvent 方法在这 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } 这个 actionMasked == MotionEvent.ACTION_DOWN 不用多说,如果事件由 ViewGroup 拦截的话,那么后面的 mFirstTouchTarget != null 中的 mFirstTouchTarget 是指向子元素的,一旦事件由 ViewGroup 拦截,那么后面的 ACTION_MOVE, ACTION_UP 经过这里时, (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 会返回 false,所以 ViewGroup 的 onInterceptTouchEvent 不会再被调用,且同一事件序列的其他事件都交由它处理。\n如果 ViewGroup 不拦截,那么 mFirstTouchTarget 会指向子元素,mFirstTouchTarget != null 为 true,则会调用 onInterceptTouchEvent ,显而易见不是吗?\n我们可以仔细想想,第一个传递到这里的事件经常是 ACTION_DOWN,如果拦截那么后续的同一事件序列中的其他事件都会交给它处理,这时候 onInterceptTouchEvent 被调用。\n假如后面又来了个 ACTION_MOVE,这时候因为 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 为 false 会直接拦截,但不会调用 onInterceptTouchEvent 了。\n这就证明了:如果一个 View 一旦决定拦截,那么将不再调用 onInterceptTouchEvent 来询问是否拦截。\n你们可以看到一个名为 FLAG_DISALLOW_INTERCEPT 的标记位,如果子 View 设置了这个标记位,那么\n(!disallowIntercept) 表达式则为 false,父 View 则不会拦截此事件,当然 ACTION_DOWN 事件除外,因为 View 判断如果是 ACTION_DOWN 则会重置这个标记位。\n标记位重置代码:\n// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } 我们再看 ViewGroup 不拦截的时候:\nfinal int childrenCount = mChildrenCount; if (newTouchTarget == null \u0026amp;\u0026amp; childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList\u0026lt;View\u0026gt; preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null \u0026amp;\u0026amp; isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i \u0026gt;= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); // child final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // dispatchTransformedTouchEvent 则是调用子元素的 dispatchTouchEvent if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j \u0026lt; childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } 简单说一吧,dispatchTransformedTouchEvent 则是调用子元素的 dispatchTouchEvent,同来向子View 分发事件,我们来看 dispatchTransformedTouchEvent 方法内容:\nfinal int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { // 因为 ViewGroup extends View ,所以会调用 View 的 dispatchTouchEvent 方法 handled = super.dispatchTouchEvent(event); } else { // 调用子类的 dispatchTouchEvent handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); // 返回子 View 是否拦截 return handled; } 如果 dispatchTransformedTouchEvent 为 true,那么会调用 addTouchTarget 方法为 mFirstTouchTarget 赋值\nnewTouchTarget = addTouchTarget(child, idBitsToAssign); // 具体 addTouchTarget 里的代码就不看了 alreadyDispatchedToNewTouchTarget = true; 如果最后遍历所有子元素事件却没有被合适的处理,要么是 ViewGroup 内没有合适的可传递 View,要么是子 View 拦截并处理了事件,但 onTouchEvent 返回了 false,所以 dispatchTouchEvent 也返回了 false。\n这时 ViewGroup 就要自己处理事件了(震惊!孤寡老人竟无子可用!这究竟是\u0026hellip;):\n// Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS) } 调用了 dispatchTransformedTouchEven 并在参数 child 传入了 null,回顾我们上面贴出来的 dispatchTransformedTouchEven 方法的代码,我们可以看到如果为 child == null则会调用 ViewGroup 自己的 dispatchTouchEvent。\nViewGroup 的源码解析到此也差不多了。\nView 源码解析 接着看它的一段 dispatchTouchEvent 代码:\npublic boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; ... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags \u0026amp; ENABLED_MASK) == ENABLED \u0026amp;\u0026amp; handleScrollBarDragging(event)) { result = true; } ListenerInfo li = mListenerInfo; if (li != null \u0026amp;\u0026amp; li.mOnTouchListener != null \u0026amp;\u0026amp; (mViewFlags \u0026amp; ENABLED_MASK) == ENABLED \u0026amp;\u0026amp; li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result \u0026amp;\u0026amp; onTouchEvent(event)) { result = true; } } ... return result; } 这里的 View 只处理自己的事件,不会再向下传递事件了。\n我们看向中间第二个 if ,我们可以看到如果设置了 OnTouchListener,且 onTouch 方法返回了 true,那么 onTouchEvent 就不会执行,证明了前面所说的 OnTouchListener 优先级高于 OnClickListener。\n我们再看 onTouchEvent 的代码:\nif ((viewFlags \u0026amp; ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP \u0026amp;\u0026amp; (mPrivateFlags \u0026amp; PFLAG_PRESSED) != 0) { setPressed(false); } return (((viewFlags \u0026amp; CLICKABLE) == CLICKABLE || (viewFlags \u0026amp; LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags \u0026amp; CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } 即使 View 为 disabled 状态也能消耗事件。\nif (((viewFlags \u0026amp; CLICKABLE) == CLICKABLE || (viewFlags \u0026amp; LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags \u0026amp; CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { // 只要 clickable | longClickable 其中一个为 true 就能执行这里,onTouchEvent 就会返回 true switch (action) { case MotionEvent.ACTION_UP: // 当手指抬起时 boolean prepressed = (mPrivateFlags \u0026amp; PFLAG_PREPRESSED) != 0; if ((mPrivateFlags \u0026amp; PFLAG_PRESSED) != 0 || prepressed) { ... if (!mHasPerformedLongPress \u0026amp;\u0026amp; !mIgnoreNextUpEvent) { removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { // performClick 将会调用 onClick performClick(); } } } ... } break; ... } ... return true; } 我们接着看 performClick 方法\npublic boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null \u0026amp;\u0026amp; li.mOnClickListener != null) { // 点击音 playSoundEffect(SoundEffectConstants.CLICK); // onClick 在这 li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; } 还没结束,我们再看一看 setOnClickListener 方法\npublic void setOnClickListener(OnClickListener l) { if (!isClickable) { setClickable(true); } getListenerInfo.mOnClickListener = l; } 可以看到在设置 View 的 OnClickListener会自动改变 View 的 clickable 属性,而 setOnLongClickListener 也是一样的。\n总结 事件分发机制就是点击事件的分发,在手指接触屏幕后产生的同一个事件序列都是点击事件。 点击事件的传递顺序是由外向内。 正常情况下一个事件序列只能被一个 View 拦截且消耗。 如果 View 决定拦截事件,那么这一个事件序列都会由这个View来处理。 当子 View 拦截却不不消耗点击事件,那点击事件将交由给他的父View去处理,如果所有的 View 都没有消耗掉点击事件(onTouchEvent 返回 false),最终 Activity 会调用自己的 onTouchEvent。 onInterceptTouchEvent 方法不一定会每次都执行,一个 View 一旦决定拦截将不会调用 onInterceptTouchEvent OnTouchListener的优先级高于onTouchEvent()。这样做的好处是方便在外部处理事件。 当我们把 View 设置为不可用状态,View 依然会消耗事件。 ","permalink":"https://moonlab.top/posts/2019/android-view-motionevent/","summary":"本文总结了《Android艺术开发探索》中关于View事件分发机制的内容。事件序列从ACTION_DOWN开始,经过多个ACTION_MOVE,最后以ACTION_UP结束。事件传递顺序为Activity -\u0026gt; Window -\u0026gt; DecorView -\u0026gt; RootView。事件分发主要通过dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法实现。ViewGroup的onInterceptTouchEvent默认返回false,子View可通过OnTouchListener处理事件。总结指出,事件分发机制确保一个事件序列只能被一个View拦截和消耗,且OnTouchListener优先级高于onTouchEvent。","title":"Android笔记#1 View的事件分发机制解析"},{"content":"没想到吧!本鸽子想起自己的博客啦!\n暑假到了,感觉时间跟不要钱一样,没有了时间概念了233\n我实在没啥可写的,就随便说说这两天干了啥吧\n昨天我开始写一个第三方的知乎日报客户端\n因为太无聊了,就随便搞搞吧。用的是这个API\n简单说一下之前遇到的问题吧,以及开发的过程\n架构 用的是MVP,也就是Model, View, Presenter\n感觉有点麻烦,其实现在有点后悔了,写起来太无聊啦\n最开始当然是疯狂写写写写接口,创建Activity\n后面则是疯狂用用用用别人的轮子\n全局异常处理 自己写了个全局异常处理实现Thread.UncaughtExceptionHandler这个接口。\n然后再把自己的Application搞上去,在MyApplication中把默认的ExceptionHandler给搞下去!\n在uncaughtException方法中写入处理异常的代码,这里是不能使用AlertDialog的!!!\n我是直接跳转到专门的ExceptionActivity处理,这样用户体验就更棒了(其实没啥用)\n但但但但但是!!!\n不知道为啥在uncaughtException中Throwable的printStackTrace方法失效了,我也没有深究,也懒得管啦\nJSON解析 没啥意思,就那样JSONOBJECT, JSONARRAY,懒得用别人的轮子233\n应用中需要加载图片,我并没有考虑Glide,我使用了OkHttp框架,而Picasso也用到了OkHttp,况且Picasso更轻量,在某种情况下个人感觉Picasso比Glide更好用\n图片宽度太宽啦 这个Webview的图片超过了屏幕的宽度,我在网上查了一下 WenView 图片自适应\n找到了一个方法,就是加载JavaScript,这可能是一个本方法但管用\n像我这种五线水平,也就只能这样惹QAQ\nwebView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); String javascript = \u0026#34;javascript:function ResizeImages() {\u0026#34; + \u0026#34;var myimg,oldwidth;\u0026#34; + \u0026#34;var maxwidth = document.body.clientWidth;\u0026#34; + \u0026#34;for(i=0;i \u0026lt;document.images.length;i++){\u0026#34; + \u0026#34;myimg = document.images[i];\u0026#34; + \u0026#34;if(myimg.width \u0026gt; maxwidth){\u0026#34; + \u0026#34;oldwidth = myimg.width;\u0026#34; + \u0026#34;myimg.width = maxwidth;\u0026#34; + \u0026#34;}\u0026#34; + \u0026#34;}\u0026#34; + \u0026#34;}\u0026#34;; view.loadUrl(javascript); view.loadUrl(\u0026#34;javascript:ResizeImages();\u0026#34;); } }); Html显示 推荐一个整理json的工具:http://www.bejson.com/\n在文章接口中,它会返回一个html代码,也就是文章内容,例如\nhtml有点长,不放了 Emmmmmm,你懂我意思就行了,很多这种内容型项目的语法都是用html的,而不是markdown\n这里我遇到了一个坑,最开始我以为只需要用TextView显示html就行了\n但实际结果很不尽人意,图片显示的效果很差,图片都太小了\n这是一个很严重的问题\n我开始怀疑人生\u0026hellip;\n想到用Webview加载,但是害怕竹篮打水一场空,就分析了一下官方的知乎日报\n似乎的确是使用WebView的,这是返回的json\n{ body: \u0026#34;\u0026lt;div class=\u0026#34;main-wrap content-wrap\u0026#34;\u0026gt;...\u0026lt;/div\u0026gt;\u0026#34;, image_source: \u0026#34;Yestone.com 版权图片库\u0026#34;, title: \u0026#34;深夜惊奇 · 朋友圈错觉\u0026#34;, image: \u0026#34;http://pic3.zhimg.com/2d41a1d1ebf37fb699795e78db76b5c2.jpg\u0026#34;, share_url: \u0026#34;http://daily.zhihu.com/story/4772126\u0026#34;, js: [ ], recommenders\u0026#34;: [ { \u0026#34;avatar\u0026#34;: \u0026#34;http://pic2.zhimg.com/fcb7039c1_m.jpg\u0026#34; }, { \u0026#34;avatar\u0026#34;: \u0026#34;http://pic1.zhimg.com/29191527c_m.jpg\u0026#34; }, { \u0026#34;avatar\u0026#34;: \u0026#34;http://pic4.zhimg.com/e6637a38d22475432c76e6c9e46336fb_m.jpg\u0026#34; }, { \u0026#34;avatar\u0026#34;: \u0026#34;http://pic1.zhimg.com/bd751e76463e94aa10c7ed2529738314_m.jpg\u0026#34; }, { \u0026#34;avatar\u0026#34;: \u0026#34;http://pic1.zhimg.com/4766e0648_m.jpg\u0026#34; } ], ga_prefix: \u0026#34;050615\u0026#34;, section\u0026#34;: { \u0026#34;thumbnail\u0026#34;: \u0026#34;http://pic4.zhimg.com/6a1ddebda9e8899811c4c169b92c35b3.jpg\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;name\u0026#34;: \u0026#34;深夜惊奇\u0026#34; }, type: 0, id: 4772126, css: [ \u0026#34;http://news.at.zhihu.com/css/news_qa.auto.css?v=1edab\u0026#34; ] } 可以看到有一条 css,官方的知乎日报显示的效果,与网页不同\n看来就是加载了这个css吧,我还以为有什么其他高科技呢!\n毕竟效率第一嘛,这样做的好处也是有的\n我们只需要在 Model 层中对 javabean 的正文属性做一些修改就可以了\n最后加载css后发现整个世界都不一样了\n(事实上最后加载css后发现,知乎日报提供的css根本就不用考虑 img 标签的宽度太大,人家自懂帮忙处理好的 QAQ)\n(而且加载css还会给官方的知乎日报app留白)\n这其中不难发现 Android客户端 与负责 Web 的开发人员之间的合作,这很常见(当然也可能是一位全栈工程师吧)\nTips: 如果WebView\n但我还没感受过这种合作呢,哭惹\n现在完成了简单的加载和查看\n只做完了六分之一吧,甚至更少(哭哭)\n2020更新 基本上已经完成了,主要是写完了分页加载,垃圾代码随便看看吧:Github (https://github.com/HelloLingC/zhihu-daily-open)\n","permalink":"https://moonlab.top/posts/2019/android-zhihu-daily/","summary":"作者在暑假期间开始开发一个第三方的知乎日报客户端,使用MVP架构。遇到了一些问题,如全局异常处理和JSON解析,选择使用Picasso加载图片。为了解决WebView中图片宽度过大的问题,作者通过JavaScript调整图片大小。最终,作者完成了基本的加载和查看功能,并在GitHub上分享了代码。尽管开发过程充满挑战,作者仍然享受这个过程。","title":"知乎日报的问题"},{"content":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.\nQuick Start Create a new post $ hexo new \u0026#34;My New Post\u0026#34; More info: Writing\nRun server $ hexo server More info: Server\nGenerate static files $ hexo generate More info: Generating\nDeploy to remote sites $ hexo deploy More info: Deployment\n","permalink":"https://moonlab.top/posts/2019/hello-world/","summary":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.\nQuick Start Create a new post $ hexo new \u0026#34;My New Post\u0026#34; More info: Writing\nRun server $ hexo server More info: Server\nGenerate static files $ hexo generate More info: Generating\nDeploy to remote sites $ hexo deploy More info: Deployment","title":"Hello World"},{"content":"咸鱼不咸从今天(2019.5.25)开始使用Hexo啦\n用了一年多的 Typecho ,换了新的博客程序虽然有点不习惯,但是我觉得 Hexo 比 Typecho更加好用\n之前在 Typecho 写的文章我并不打算搬过来,评论也是如此,所以基本上相当于重新写一个博客\u0026hellip;\n之前的 咕咕咕 事故导致博客失去了许多朋友QAQ\n写博客的这一段时间,我也有许多的收获吧\n咸鱼不咸的初心只是因为无聊,而现在则是想要有一个在复杂的网络中发声的立身之地\n在茫茫海洋中,唯有这小小的避浪处能给我安心吧\n这也算是一个新的开始吧,以后更新博客的速度就随缘吧233333\n","permalink":"https://moonlab.top/posts/2019/hello-hexo/","summary":"咸鱼不咸从今天(2019.5.25)开始使用Hexo啦\n用了一年多的 Typecho ,换了新的博客程序虽然有点不习惯,但是我觉得 Hexo 比 Typecho更加好用\n之前在 Typecho 写的文章我并不打算搬过来,评论也是如此,所以基本上相当于重新写一个博客\u0026hellip;\n之前的 咕咕咕 事故导致博客失去了许多朋友QAQ\n写博客的这一段时间,我也有许多的收获吧\n咸鱼不咸的初心只是因为无聊,而现在则是想要有一个在复杂的网络中发声的立身之地\n在茫茫海洋中,唯有这小小的避浪处能给我安心吧\n这也算是一个新的开始吧,以后更新博客的速度就随缘吧233333","title":"使用Hexo"},{"content":" 从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\nAuthor Avatar LingC 8 月 7 日\n00 本文总结了16.04以上的版本运行网易云音乐会出现的问题,以及解决方法 如果你安装官方版本网易云音乐后(v1.1.0),运行网易云音乐无响应的话可以试试以下几种方法\n01 在终端中输入\n$ netease-cloud-music \u0026ndash;no-sandbox %U\n即可启动网易云音乐\n有可能会出现一些警告,稍后会打开网易云主界面\n如果出现Unable to locate theme engine in module_path: \u0026ldquo;adwaita\u0026rdquo;\n可以尝试执行这条命令:$ sudo apt install gnome-themes-standard\n如果出现failed to retrieve propertygtk-primary-button-warps-slider’ of type gboolean'\n可以尝试$ vi ~/.gtkrc-2.0\n使用vim打开这个文件,将其中的一行改为\ngtk-primary-button-warps-slider=false改为gtk-primary-button-warps-slider=0\n然后可以再试着运行一下,如果还不行就reboot\n如果出现Gtk-Message: 08:59:03.546: Failed to load module \u0026ldquo;canberra-gtk-module\u0026quot;或者没有反应,那么你可以试试第二种方法\n02 在终端中输入\n$ sudo netease-cloud-music\n使用root权限运行网易云音乐\n可能会出现一些警告和报错,其实可以无视(懒的管)\n2018-08-07 09-00-03屏幕截图.png\n03 可以试一下这个方法 https://jingyan.baidu.com/article/1e5468f956a15c484861b770.html\n04 该方法针对于ubuntu 18.04LTS 网易云音乐v1.1.0\n其他版本也可以试一下\n首先\n$ sudo aptitude install libcanberra-gtk-module\n执行此命令前提是安装了vim\n$ sudo vim /usr/share/applications/netease-cloud-music.desktop\n没有安装vim的话执行\n$ sudo nano /usr/share/applications/netease-cloud-music.desktop\n找到 exec 那一行 ,在 %U 前面加上 —no-sandbox ,按 Ctrl+X 保存\n这时候你可以先试一下\n运行网易云之后,还没出来,然后就去点一下右上角的关机\n或\n直接重启\n05 适用于17.10\nhttps://jingyan.baidu.com/article/22fe7cedf70e343002617fd5.html\n06 点击桌面图标运行网易云音乐1.1.0,正常情况下不会出来窗口,然后点击状态栏电量那里,点击关机(不要通过终端执行poweroff),然后玄学的事情发生了……网易云音乐出来了……然后就可以愉快的听音乐了。重启依旧有效。若无效可以如法炮制,屡试不爽\n07 如果以上方法都不行,你可以试着在本文评论问一下,或者去贴吧求助233\n本文参考\nhttps://blog.csdn.net/gpwner/article/details/78347516\nhttps://blog.csdn.net/yestin_L/article/details/79611082\nhttps://blog.csdn.net/weixin_31317775/article/details/78726460\nhttp://tieba.baidu.com/p/5343249977\nhttps://jingyan.baidu.com/article/1e5468f956a15c484861b770.html\nhttps://www.jianshu.com/p/d66a19f6019c\nhttp://tieba.baidu.com/p/5671164721?pid=119408155566\u0026cid;=0#119408155566\nhttps://jingyan.baidu.com/article/22fe7cedf70e343002617fd5.html\nhttps://blog.csdn.net/Csdoker/article/details/80462163\n","permalink":"https://moonlab.top/posts/2018/ubuntu-cannot-use-netease-music/","summary":"从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\nAuthor Avatar LingC 8 月 7 日\n00 本文总结了16.04以上的版本运行网易云音乐会出现的问题,以及解决方法 如果你安装官方版本网易云音乐后(v1.1.0),运行网易云音乐无响应的话可以试试以下几种方法\n01 在终端中输入\n$ netease-cloud-music \u0026ndash;no-sandbox %U\n即可启动网易云音乐\n有可能会出现一些警告,稍后会打开网易云主界面\n如果出现Unable to locate theme engine in module_path: \u0026ldquo;adwaita\u0026rdquo;\n可以尝试执行这条命令:$ sudo apt install gnome-themes-standard\n如果出现failed to retrieve propertygtk-primary-button-warps-slider’ of type gboolean'\n可以尝试$ vi ~/.gtkrc-2.0\n使用vim打开这个文件,将其中的一行改为\ngtk-primary-button-warps-slider=false改为gtk-primary-button-warps-slider=0\n然后可以再试着运行一下,如果还不行就reboot\n如果出现Gtk-Message: 08:59:03.546: Failed to load module \u0026ldquo;canberra-gtk-module\u0026quot;或者没有反应,那么你可以试试第二种方法\n02 在终端中输入\n$ sudo netease-cloud-music\n使用root权限运行网易云音乐\n可能会出现一些警告和报错,其实可以无视(懒的管)\n2018-08-07 09-00-03屏幕截图.png\n03 可以试一下这个方法 https://jingyan.baidu.com/article/1e5468f956a15c484861b770.html\n04 该方法针对于ubuntu 18.04LTS 网易云音乐v1.1.0","title":"Ubnutu 无法启动网易云音乐 - 总结"},{"content":" 从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\nAuthor Avatar LingC 8 月 4 日\n00 继上一篇电脑中常用的软件\n我在 C 盘格式化后又遇见了一些不错的软件,这些软件反正是有 Windows 版本的,其他系统我就不知道啦~~\n并且这篇文章我会为你找到软件的官网或资源下载地址,我会一并写在这篇文章中\n01 - Fances 这款软件就是让你的桌面更条例有序,给泥们放一张我桌面的截图吧 TIM截图20180804195952.png 是不是感觉很好看 o( ̄▽ ̄)o\n购买:https://www.stardock.com/products/fences/ 不过 steam 上也有卖 https://store.steampowered.com/app/607380/Fences/?l=tchinese\u0026cc;=nl 你说你没有钱买正版?没关系,我这里有破解版 不过在下载之前你要答应我,自己有能力的话一定要去买正版哦 (。・∀・) ノ 慕若曦那里就有这个破解版资源,链接:https://www.muruoxi.com/623.html\n02 - f.lux 有时候我会抱怨,电脑屏幕的亮度太高了,兼职闪瞎我的眼睛,特别是在晚上的时候,我一直找不到好的护眼软件\n然而,这款软件除了普通的护眼功能外,还可以根据你选择的模式在不同时间段进行自动调节 来张图\nflux.png\n你可以直接去官网中下载:https://justgetflux.com/\n03 - uninstaller 这其实是一个小工具,你不用安装就可以启动\n这是一款小型轻量的卸载程序,我看中它可以卸载完程序后,清理注册表里的残留,感觉挺良心的 我查了一下才发现,我一直用的是老版本的 uninstaller,这是老板本截图,新版本比这好看很多\nuninstallerold.png\n老板本的 uninstaller 不需要安装,而新版本的需要安装,但是新版本的用户体验肯定比老板本好啦\n新版本截图:\nuninstallernow.png\n虽然新版本安装时有一些肿瘤,推荐你安装同一公司的产品,以及订阅书籍,但是它在功能使用方面并不膨胀,我自己稍微使用了一下感觉还行 飞去下载地址吧!:https://iobit-uninstaller.en.softonic.com/\n04 - 火绒 这是本文唯一提到的国产软件,也是我最期待的软件\n这是一款电脑安全软件,我相信你们的电脑一般都会装一些安全软件,比如 360,百度,腾讯的那些安全卫士\n我在知道火绒之前一直是裸体的,直到火绒出现到我面前,我一直是不相信国内会有如此良心的安全软件\n首先,肯定有人会问\n啊,这是什么软件,听都没有听说过,肯定没有 360 好用啦 其实火绒的防护能力并不亚于国内的安全软件,不敢说是站在顶端,但是从制作人方面就可以看出一定不是什么小公司做开发的,我用着感觉十分的不错,纯净的超乎想象,有一次我卸载国内某款软件,最后即将完成卸载时,我不耐烦的快速按了,眼角的余光看见了\n√ 安装 鲁大师 √ 安装 … 当时其实我都忘记了还有火绒在,所以慌的一批,然后火绒就在右下角弹出 “已拦截…..”\n你经常会忽视它的存在,因为它并不会像国内软件一样,疯狂刷存在感,除更新病毒库外,它几乎不会出现任何的弹窗,这里的弹窗是指在右下角显示\n好的安全软件应 “只干活不闹事” 官网:https://www.huorong.cn/\n","permalink":"https://moonlab.top/posts/2018/windows-software-recommendation/","summary":"从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\nAuthor Avatar LingC 8 月 4 日\n00 继上一篇电脑中常用的软件\n我在 C 盘格式化后又遇见了一些不错的软件,这些软件反正是有 Windows 版本的,其他系统我就不知道啦~~\n并且这篇文章我会为你找到软件的官网或资源下载地址,我会一并写在这篇文章中\n01 - Fances 这款软件就是让你的桌面更条例有序,给泥们放一张我桌面的截图吧 TIM截图20180804195952.png 是不是感觉很好看 o( ̄▽ ̄)o\n购买:https://www.stardock.com/products/fences/ 不过 steam 上也有卖 https://store.steampowered.com/app/607380/Fences/?l=tchinese\u0026cc;=nl 你说你没有钱买正版?没关系,我这里有破解版 不过在下载之前你要答应我,自己有能力的话一定要去买正版哦 (。・∀・) ノ 慕若曦那里就有这个破解版资源,链接:https://www.muruoxi.com/623.html\n02 - f.lux 有时候我会抱怨,电脑屏幕的亮度太高了,兼职闪瞎我的眼睛,特别是在晚上的时候,我一直找不到好的护眼软件\n然而,这款软件除了普通的护眼功能外,还可以根据你选择的模式在不同时间段进行自动调节 来张图\nflux.png\n你可以直接去官网中下载:https://justgetflux.com/\n03 - uninstaller 这其实是一个小工具,你不用安装就可以启动\n这是一款小型轻量的卸载程序,我看中它可以卸载完程序后,清理注册表里的残留,感觉挺良心的 我查了一下才发现,我一直用的是老版本的 uninstaller,这是老板本截图,新版本比这好看很多\nuninstallerold.png\n老板本的 uninstaller 不需要安装,而新版本的需要安装,但是新版本的用户体验肯定比老板本好啦\n新版本截图:\nuninstallernow.png\n虽然新版本安装时有一些肿瘤,推荐你安装同一公司的产品,以及订阅书籍,但是它在功能使用方面并不膨胀,我自己稍微使用了一下感觉还行 飞去下载地址吧!:https://iobit-uninstaller.en.softonic.com/\n04 - 火绒 这是本文唯一提到的国产软件,也是我最期待的软件\n这是一款电脑安全软件,我相信你们的电脑一般都会装一些安全软件,比如 360,百度,腾讯的那些安全卫士\n我在知道火绒之前一直是裸体的,直到火绒出现到我面前,我一直是不相信国内会有如此良心的安全软件\n首先,肯定有人会问\n啊,这是什么软件,听都没有听说过,肯定没有 360 好用啦 其实火绒的防护能力并不亚于国内的安全软件,不敢说是站在顶端,但是从制作人方面就可以看出一定不是什么小公司做开发的,我用着感觉十分的不错,纯净的超乎想象,有一次我卸载国内某款软件,最后即将完成卸载时,我不耐烦的快速按了,眼角的余光看见了","title":"Windows 好软推荐 | 这一定是良心软件"},{"content":" 从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\n2018-03-27 21:28 在我从http转成https时出现一个致命问题: css与js无法加载 报错截图: 主页界面: 并且这时没有绿锁 访问了许多网站,游览了许多资料 在config.inc.php中插入\n/** 开启HTTPS */ define('__TYPECHO_SECURE__',true); 也没有任何卵用,我开始绝望\n其主要问题是 网站前台资源 通过http访问,而本站是https,所以会出现问题 且安全连接并不完全,因为有http的连接\n就在这时,我翻到一篇文章,看到一个插件:TEDUriReplace 使用方法非常简单,插件截图: 步骤: 1.将后台的网址改为https://xxx.cn\n2.在插件的规则中写入http://xxx.cn/=\u003ehttps://xxx.cn/\n3.刷新网页\n就是如此简单,这款插件真的很好用 下载链接:https://s.typechodev.com/plugins/uploads/2017/12/2122242327.zip\n","permalink":"https://moonlab.top/posts/2018/typecho-http-2-https/","summary":"从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\n2018-03-27 21:28 在我从http转成https时出现一个致命问题: css与js无法加载 报错截图: 主页界面: 并且这时没有绿锁 访问了许多网站,游览了许多资料 在config.inc.php中插入\n/** 开启HTTPS */ define('__TYPECHO_SECURE__',true); 也没有任何卵用,我开始绝望\n其主要问题是 网站前台资源 通过http访问,而本站是https,所以会出现问题 且安全连接并不完全,因为有http的连接\n就在这时,我翻到一篇文章,看到一个插件:TEDUriReplace 使用方法非常简单,插件截图: 步骤: 1.将后台的网址改为https://xxx.cn\n2.在插件的规则中写入http://xxx.cn/=\u003ehttps://xxx.cn/\n3.刷新网页\n就是如此简单,这款插件真的很好用 下载链接:https://s.typechodev.com/plugins/uploads/2017/12/2122242327.zip","title":"typecho - http转https"},{"content":" 从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\n2018-03-21 21:09 2018年3月8日,Google发布了Android P的预览版 而这次版本更新引来了很多不满,大多是对布局外观上的评价 主要都是围绕着Material Design的问题,因为Google从Android5.x开始就遵循着Material Design 而Material Design是Google提出的设计风格,虽然Google系列很多都遵循着MD 但是也有一部分偏移了MD,就比如AndroidP,我感觉更偏向于扁平化 这张图片是AndroidP的截图,感觉还可以 另外再说一下圆角元素,AndroidP大量使用了圆角元素 我个人感觉圆角元素的弧度只要把握好,真的不是什么大问题 斜刘海的问题我也不想多说 这也成为了被众人吐槽的原因之一 Android P IOS P的通知栏已经丝毫没有MD的风格了,与IOS的通知栏比起来 感觉更像了,偏向于扁平化,没有质感了 P的设置界面倒还不错,虽然与MD搭不上边 色彩的搭配很有亮点\n总的来说,并不是特别讨厌P的更新,因为时代会变化 倒不如说很喜欢这次更新,感觉是安卓目前最好看的设计 MD风格是给人的设计思路,而不是限制设计的锁链\n","permalink":"https://moonlab.top/posts/2018/comment-about-android-p/","summary":"从垃圾堆里抛出来的一篇2018的文章. \u0026ndash;2024.8.7\n2018-03-21 21:09 2018年3月8日,Google发布了Android P的预览版 而这次版本更新引来了很多不满,大多是对布局外观上的评价 主要都是围绕着Material Design的问题,因为Google从Android5.x开始就遵循着Material Design 而Material Design是Google提出的设计风格,虽然Google系列很多都遵循着MD 但是也有一部分偏移了MD,就比如AndroidP,我感觉更偏向于扁平化 这张图片是AndroidP的截图,感觉还可以 另外再说一下圆角元素,AndroidP大量使用了圆角元素 我个人感觉圆角元素的弧度只要把握好,真的不是什么大问题 斜刘海的问题我也不想多说 这也成为了被众人吐槽的原因之一 Android P IOS P的通知栏已经丝毫没有MD的风格了,与IOS的通知栏比起来 感觉更像了,偏向于扁平化,没有质感了 P的设置界面倒还不错,虽然与MD搭不上边 色彩的搭配很有亮点\n总的来说,并不是特别讨厌P的更新,因为时代会变化 倒不如说很喜欢这次更新,感觉是安卓目前最好看的设计 MD风格是给人的设计思路,而不是限制设计的锁链","title":"如何评价Android P"},{"content":"Showcase Video 关于我(LingC) An Explorer of the cold land.\nEmail: contact@moonlab.top\n关于 MoonLab Glowing guardian in the cosmic sea, always a timeless, tranquil melody.\nAll the articles in this site are licensed under the CC BY-NC 4.0 .\n友情链接 \u0026gt; 折影轻梦\n\u0026gt; Zgcwkj\n\u0026gt; 变态鹅\n\u0026gt; Ojhdt\u0026rsquo;s blog\n\u0026gt; idealclover\n","permalink":"https://moonlab.top/about/","summary":"Showcase Video 关于我(LingC) An Explorer of the cold land.\nEmail: contact@moonlab.top\n关于 MoonLab Glowing guardian in the cosmic sea, always a timeless, tranquil melody.\nAll the articles in this site are licensed under the CC BY-NC 4.0 .\n友情链接 \u0026gt; 折影轻梦\n\u0026gt; Zgcwkj\n\u0026gt; 变态鹅\n\u0026gt; Ojhdt\u0026rsquo;s blog\n\u0026gt; idealclover","title":"关于"},{"content":"由我个人维护并开发的项目\nMoon Meet - 为 Cloudflare Worker 设计的由 ECDH 密钥交换算法保证通讯安全性的在线加密聊天服务。任何人都可以免费地部署在自己的 Worker 中。使用 Vue3, Vite, itty-router 构建,并根据 GPL 协议进行源代码分发。\nMoonComm - 为 Cloudflare Worker 设计的博客评论系统。通过 D1 数据库进行数据存储,数据库仅支持 SQLite。前端使用 Google Lit, 后端使用 itty-router 构建,并根据 LGPL 协议进行源代码分发。\nMoon Counter - 网站浏览量统计器。支持通过图片和文本显示网页浏览量,允许修改 CORS。使用 Golang 开发,并根据 MIT 协议进行源代码分发。\n","permalink":"https://moonlab.top/projects/","summary":"由我个人维护并开发的项目\nMoon Meet - 为 Cloudflare Worker 设计的由 ECDH 密钥交换算法保证通讯安全性的在线加密聊天服务。任何人都可以免费地部署在自己的 Worker 中。使用 Vue3, Vite, itty-router 构建,并根据 GPL 协议进行源代码分发。\nMoonComm - 为 Cloudflare Worker 设计的博客评论系统。通过 D1 数据库进行数据存储,数据库仅支持 SQLite。前端使用 Google Lit, 后端使用 itty-router 构建,并根据 LGPL 协议进行源代码分发。\nMoon Counter - 网站浏览量统计器。支持通过图片和文本显示网页浏览量,允许修改 CORS。使用 Golang 开发,并根据 MIT 协议进行源代码分发。","title":"项目"}]