夜雨聆风学习资料网

ARTICLE · 1305870

抖音去水印,我把它做成了 57KB 的安卓 App

抖音去水印,我把它做成了 57KB 的安卓 App
安卓实战NO.14  /  码途日记

先把这东西干什么的说清楚。在抖音里点分享、复制链接,把那一串粘进去,它就能把无水印的视频和图集解析出来,直接存到手机上。视频能选画质,图集能整组下,也能只挑其中一张。

它原来是给 Windows 写的。双击 exe,浏览器弹出来,粘链接,解析,下载。用了一个月,唯一别扭的地方是我只能在电脑前用它。手机上刷到想留的素材,得先分享到微信,再回电脑上粘一遍。

搬这个的念头就是这么来的。整个过程我从头记一遍,从 1008 宽的窗口到 412 宽的屏,中间改的都是很具体的东西。

01它原来长这样

上面是解析出来的单条视频,下面是图集,两个窗口都是 1008 宽。界面上的东西不多:封面、标题、画质选项,再加一个下载按钮。

它不是一个命令行脚本。python3 douyin_dl.py 跑起来干两件事:起一个本地 HTTP 服务,然后用默认浏览器开一个页面。1452 行 Python 里一大半是在实现接口,界面是另外那个 52KB 的 index.html。

接口只有四个。/api/parse 解析,/api/task 轮询任务进度,/api/download 落盘,/api/stream 代理媒体流。桌面版和网页之间就靠这四个 URL 说话,别的不共享。

这一点很重要,后面所有决定都是从这个形状推出来的。

02三条路,选了最笨的那条

安卓版有三条路。

Kivy 或者 BeeWare,用 Python 直接跑在手机上。听着最省事,实际是把那 52KB 网页整个重画一遍,CSS 重写,动效重调,等于做第二个产品。

Chaquopy 把 CPython 塞进 apk,Python 代码几乎原样不动。代价是包里带一整套运行时,几十兆起步,冷启动也慢。

第三条是逻辑用 Java 重写,界面继续用那个网页。

我选了第三条。理由不复杂:接口形状不用变。安卓这边把四个 URL 的路径和参数对齐,返回的 JSON 长得一样,index.html 一行都不用改,连带上面那些动效和布局细节全都白捡。

代价是 1452 行 Python 得逐行翻成 Java,最后出来 1904 行。

03五份代码,拼成一个 App

拆下来是五个文件。

Douyin.java 707 行,管 ttwid 注册、拉作品详情,把带 playwm 的播放地址换成无水印的源地址。Api.java 578 行,四个接口的路由和 JSON 组装。HttpSrv.java 335 行,手写的 HTTP 服务,ServerSocket 加线程池。Store.java 149 行,负责落盘。MainActivity.java 135 行,WebView 壳和剪贴板桥,负责返回键。

三个地方换了写法。

剪贴板。navigator.clipboard.readText() 在 WebView 里多数机型直接 rejected,虽然 127.0.0.1 按规范算安全上下文。不跟它较劲,自己开个口子:

web.addJavascriptInterface(newClipBridge(), "AndroidClip");privateclassClipBridge {@JavascriptInterfacepublic String read() {ClipboardManagercm= (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);if (cm == null || !cm.hasPrimaryClip()) return"";ClipDataclip= cm.getPrimaryClip();if (clip == null || clip.getItemCount() == 0) return"";return clip.getItemAt(0).coerceToText(MainActivity.this).toString();    }}

请求正文。原来的写法很顺手,BufferedReader 一行行读请求头,读到空行,再按 Content-Length 去原始流读正文。

问题出在 BufferedReader 内部有 8KB 缓冲,它读头的时候顺手把正文也吸进去了。回头再去 InputStream 读,拿到空字符串。表现是「解析按钮点了没反应」,这类不报错的坑最难查。

改成一个字节一个字节读行,不多读一个:

privatestatic String readLine(InputStream in)throws IOException {ByteArrayOutputStreambuf=newByteArrayOutputStream();int c;while ((c = in.read()) != -1) {if (c == '\n') {byte[] b = buf.toByteArray();intlen= b.length;if (len > 0 && b[len - 1] == '\r') len--;returnnewString(b, 0, len, "ISO-8859-1");        }        buf.write(c);    }returnnull;}

改完专门测了一遍,发 4012 个字符的 JSON 过去,服务端收到的长度一个不差。

落盘。桌面版先写 .part 临时文件,写完 os.replace 改名,靠这个保证「你看到的文件一定是完整的」。安卓上换一套:API 29 以后要往 MediaStore 插一条记录,系统给 content URI,你往里写,写完这条记录才成为一个真文件。好处是文件直接出现在系统「下载」里,文件管理器和相册都能引用。

三个地方改完,第一版打出来 53KB,装到手机上长这样:

界面跟桌面版一模一样。复用的好处就在这儿,一行 CSS 没动。

但屏幕从 1008 宽变成 412 宽,同一份 CSS 在两个尺寸上算出来的东西不一样。下面三处是装上去之后一处一处改的。

04不用 Gradle,五条命令

打包没走 Gradle。

这台机器没装 Android Studio,dl.google.com 十分钟连不上。后来从国内镜像单独拉了 build-tools 和 platform 两个压缩包,解到 D:/develop/android-sdk,干脆不要 Gradle,直接调底层:

aapt2 compile            编译资源aapt2 link               生成 R.java 和 base.apkjavac                    编译 Java(--release 8)d8                       转 classes.dexzipalign + apksigner     对齐、签名

一条 shell 脚本串起来,十几秒跑完。往 apk 里塞 classes.dex 那一步卡了一下:习惯性写了 zip -qf,这台机器没装 zip 命令,最后用 Python 的 zipfile 三行补上。

05第一个改的是「目录」

第一版留了个「打开文件夹」按钮,ACTION_VIEW 指向 content://media/external/downloads。点下去弹的是这个:

夸克、阿里云盘、百度网盘,一个文件管理器都没有。

原因是那个 URI 没带 MIME 类型。系统拿不到类型,就按 */* 去匹配所有声明能收 content URI 的应用,而真正在这条路径上注册过的恰好是那堆网盘。查文档才明白这是按类型匹配的必然结果,没有哪个文件管理器会主动声明自己能收裸的 content://。

那就别调系统了,App 里自己列。API 29 以上按相对路径查,只捞自己下载的:

Stringsel= MediaStore.MediaColumns.RELATIVE_PATH + " LIKE ?";resolver.query(MediaStore.Downloads.EXTERNAL_CONTENT_URI, cols, sel,newString[]{"%" + Environment.DIRECTORY_DOWNLOADS + "/DouyinDL%"}, null);

29 以下退回遍历目录。两套都留着,minSdk 定的是 21。

列表页第一版只把结果存内存 Map,App 一重启就空了。改成每次打开都去 MediaStore 现查一遍,查一次几十毫秒,比维护缓存省心。

列表用 iframe 浮层盖在主页上,不整页跳转,这样解析结果不会丢。分享走 ACTION_SEND 带 MIME,落到系统分享面板,微信、抖音、文件管理都在里面。

0631 张图,和那行 aspect-ratio

抖音图集到 31 张的时候,缩略图开始互相压:

上面那张是改之前的,第二行盖在第一行上,点第二行实际点到的是第一行那张。下面是 23 张的同一套布局。

格子按竖版 3:4,CSS 写得很顺手:

.img-cell { aspect-ratio: 3 / 4; }.img-cellimg { width: 100%; height: 100%; object-fit: cover; }

aspect-ratio 和 height: 100% 放一起会形成循环依赖。图片高度依赖容器高度,容器高度由 grid 行高决定,行高又希望由内容撑开。桌面 Chrome 恰好能算出一个对的结果,WebView 算错了。

改成用 padding 撑比例,图片绝对定位铺满:

.img-cell { position: relative; padding-top: 133.333%; overflow: hidden; }.img-cellimg {position: absolute; top: 0; left: 0;width: 100%; height: 100%; object-fit: cover;}

133.333% 就是 4/3,撑出一个竖版盒子。绝对定位的百分比参照的是 padding box,循环断了。

顺带一个触屏的事。缩略图上的下载图标原来是 opacity: 0 靠 hover 显形,这是桌面版的习惯。手机上根本没有 hover,那排按钮永远看不见。窄屏下改成常显,代价是图上有几个小方块,比点不到强。

有没有真的对齐,别用眼睛判断。在 WebView 里注入一段脚本,把所有格子的 getBoundingClientRect() 打出来,一行行看顶边和底边。我那次发现第七格的顶是 145,第一行底是 139,差 6 像素,肉眼看不出,手指点下去就是错的。

07大图底下那个按钮,改了三版

点开大图要能单独存这一张。左边是刚装上那会儿,底下只有页码;右边是最后的样子,按钮改了三版:

第一版是个 position: absolute,贴在大图下方。缩略图挤在一起的时候,人点开大图就是因为小图上点不准,结果按钮还叠在图片上,被盖掉一半。

第二版把它挪到浮层底部,加 padding、加 z-index,又把兼容性加固一遍(inset: 0 要 Chrome 87 以上,改成写四个边)。装上还是看不到。

第三版不缝了,改结构。

原来那个浮层是 position: fixed 的全屏层,图、左右箭头、关闭按钮全靠绝对定位在一个坐标系里往上叠,图一高就把按钮顶出去。改成两段:上面一段放图和箭头,下面一段放按钮和页码,两段都在正常文档流里,用 flex 分高度。

.lightbox { display: flex; flex-direction: column; }.lb-stage { flex: 11 auto; min-height: 0; position: relative; }.lb-bottom { flex: 00 auto; }

min-height: 0 不能省。不加这行,flex 子项的最小高度会按内容算,图一高照样把下面那段挤出去。

按钮本身做成三态,可下载、下载中、已下载。判断依据不是点没点过,是服务端返回的 files 字段,也就是真落盘的记录,缩略图上那个绿对勾用的是同一份数据。轮询到文件出现,按钮才转绿;点完到转绿之间写「下载中」,按不动,防连点。

中间有个小坑。服务端每轮询返回的都是新数组,items = data.items 一整个替换,灯箱里存的那个对象引用就变成旧的了,读不到新 files。解法是每次按 index 从当前数组重新取,别信引用。

0857KB

53KB 到 57KB,多出来的 4KB 是一个文件列表页 3.9KB、大图浮层的样式重写、下载状态机那几十行 JS。

这个体量下,每加一个功能都能在体积上看见。也正因为小,改起来才敢一次一次试,改完到打好包十几秒,装上就能测。

要是走的是 Chaquopy 那条路,包几十兆,装一次要等,我大概改到第二版就懒得动了。


抖音去水印下载器 v1.3 现在能用了,安卓手机直接装,不用 root,不用装任何框架。

拿的方式:公众号后台回「去水印」,自动回你下载链接。网盘也存了一份,密码在自动回复里。

装完先看一眼首页标题下面那行小字,写着 v1.3 就是最新的。旧版要先卸载再装,签名换了,覆盖装会失败。

这个工具不收费,也不打算收费。用着不顺的地方回我,能改的我都改。

你手上有没有哪个只在电脑上用着顺的小工具,想搬到手机上?

评论区聊聊,我一条条回

关注「码途日记」,写点技术人真实做过的事

已关注的设个星标,不然容易收不到

     #安卓  #APK  #抖音去水印  #WebView

    #程序员  #移动开发  #独立开发  

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料