前言
阅读【Xposed】抖音短视频检测 Xposed 分析这篇文章时,突然有感,要不,也去看一下抖音的代码是如何写的,学习一下大厂是如何规范写代码的。再加上自己懂一些反编译的知识,于是就有了这篇文章。
当然这篇文章不会深入研究抖音Android版本的技术,只是从表面稍微学习一下。如果文章有错误或者描述不当,请JYM见谅。
准备工作
- adb环境变量。这个和下面的java环境变量在安装Android Studio的时候就搞定了。
- java环境变量。
- apktool下载。查看apk中的资源,这个可下载可不下载。
- dex2jar安装。用来解压dex文件。
- jd-gui安装。查看加压后的jar文件中的具体内容。
- 抖音apk文件。这篇文章所引用的
VersionCode
为270801,VersionName
='27.8.0',包名'com.ss.android.ugc.aweme'
反编译抖音apk
使用apktool解压apk文件
打开终端,执行命令行
java -jar apktool_2.9.0.jar d demo.apk -o demo
其中 demo.apk
是抖音apk文件,-o demo
是输出目标文件。
注:这一步的前提是,java环境变量的配置。
等命令行执行完毕后,打开demo文件,可以看到50个文件(可能版本不同,或者不同的apk,解压出来的文件数量是不同的)。除了assets、lib、res和AndroidManifest文件外,还有smali文件。
这里分享一篇文章Android逆向系列。同时分享另一篇帖子android app相关破解技术大揭秘咯,从这篇帖子可以知道smali文件是干什么的。
这里贴ManiFragment$6.smali的一部分代码:
ruby
.method private synthetic LIZ()V
.locals 1
.prologue
.line 65536
iget-object v0, p0, Lcom/ss/android/ugc/aweme/main/MainFragment$6;->LIZ:Lcom/ss/android/ugc/aweme/main/MainFragment;
.line 65537
.line 65538
invoke-virtual {v0}, Lcom/ss/android/ugc/aweme/main/MainFragment;->LJIILIIL()V
.line 65539
.line 65540
.line 65541
return-void
.end method
对应的代码如下:
arduino
public final void LIZ(Ve7 paramVe7) {
this.LIZ.superFinish();
}
dex2jar和jd-gui查看源码
提取apk文件中的class.dex,apk文件
可以用压缩软件打开。打开apk文件后就犯难了,要解压的dex文件居然有42个。比如,我自己写的apk,里面的class.dex一般都在3个范围内,但douyin.apk
里面居然有42个class.dex,太多了。
dex2jar可以让dex转jar,也可以让apk转jar。这个apk的dex太多了,那试试直接转apk吧。
执行命令行
d2j-dex2jar douyin.apk -o douyin.jar
报错了:
css
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.LinkedList.linkLast(Unknown Source)
at java.util.LinkedList.add(Unknown Source)
at com.googlecode.d2j.reader.DexFileReader.travelInsn(DexFileReader.java:1174)
at com.googlecode.d2j.reader.DexFileReader.findLabels(DexFileReader.java:1135)
at com.googlecode.d2j.reader.DexFileReader.acceptCode(DexFileReader.java:1412)
at com.googlecode.d2j.reader.DexFileReader.acceptMethod(DexFileReader.java:1064)
at com.googlecode.d2j.reader.DexFileReader.acceptClass(DexFileReader.java:862)
at com.googlecode.d2j.reader.DexFileReader.accept(DexFileReader.java:662)
at com.googlecode.d2j.reader.MultiDexFileReader.accept(MultiDexFileReader.java:117)
at com.googlecode.d2j.reader.MultiDexFileReader.accept(MultiDexFileReader.java:110)
at com.googlecode.d2j.dex.Dex2jar.doTranslate(Dex2jar.java:86)
at com.googlecode.d2j.dex.Dex2jar.to(Dex2jar.java:285)
at com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine(Dex2jarCmd.java:112)
at com.googlecode.dex2jar.tools.BaseCmd.doMain(BaseCmd.java:288)
at com.googlecode.dex2jar.tools.Dex2jarCmd.main(Dex2jarCmd.java:33)
有点烦,还是用死办法吧,一个一个把dex文件转成jar文件。
d2j-dex2jar classes1.dex -o class1.jar
如此,一个dex文件一个dex文件依次执行过来。最后成功把42个dex文件都转换成jar文件。
执行命令行打开gui查看代码
java -jar jd-gui-1.6.6.jar

这个是classes1.jar文件的目录
把42个classes1.dex~classes42.dex全部拖入可视化gui界面。
那接下来从何入手呢?
我决定从AndroidManifest入手。这个文件容易拿到,前面提到的apktool可以获取到,甚至直接用解压工具都可以获取到这个文件,也可以直接把apk拖入Android Studio,也能看得到AndroidManifest.xml文件。
打开AndroidMainfest.xml文件
,找到启动页,发现居然还真是SplashActivity
ini
<activity-alias android:name="com.ss.android.ugc.aweme.splash.SplashActivity"
android:screenOrientation="portrait"
android:targetActivity="com.ss.android.ugc.aweme.main.MainActivity"
android:theme="@style/APKTOOL_DUPLICATE_style_0x7f120274">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
打开抖音,这是用adb查看当前Activity
adb shell dumpsys activity activities
这句命令行可以查看当前所有活动中的Activity。
ini
TaskRecord{8b667e0 #3 A=com.ss.android.ugc.aweme U=0 StackId=2 sz=1}
userId=0 effectiveUid=u0a116 mCallingUid=u0a24 mUserSetupComplete=true mCallingPackage=com.miui.home
affinity=com.ss.android.ugc.aweme
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.ugc.aweme/.main.MainActivity}
发现当前播放视频的Activity是MainActivity(命名还是很标准的)。
根据路径com.ss.android.ugc.aweme.main
还是笨办法,在42个classes.jar文件中找这个目录下的文件。
在classes7.jar中找到main文件夹,不过只有MainFragment$6
类,不是目标。
在classes9.jar中找到main文件夹,不过只有TabAlphaController
类,不是目标。
在classes11.jar中找到main文件夹,不过只有MainPageFragment
类,不是目标。
在classes14.jar中找到main文件夹,不过只有IMainPageMobHolper
类,不是目标。
终于,在classes21.jar中找到MainActivity文件

MainActivity.class
随便找其中一个方法
enterAuthPage
csharp
private void enterAuthPage(Intent paramIntent) {
if (paramIntent != null && paramIntent.getExtras() != null && paramIntent.getExtras().containsKey("openplatform_after_switch_account"))
try {
SmartRouter.buildRoute((Context)this, "aweme://authorizedy/").withParam(paramIntent.getExtras()).open();
if (!"pc_auth".equals(paramIntent.getExtras().getString("source")))
finish();
return;
} catch (Exception exception) {
GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(exception);
}
}
发现抖音Android端是用自己的路由跳转代码SmartRouter
,既不是Arouter,也不是TheRouter。aweme://authorizedy/
这个看起来像是路径,不过没找到对应的Fragment或者Activity。
在
java
public boolean onFeedPage() {
if (getTabChangeManager().LIZJ() instanceof MainFragment) {
ewF ewF = f9k.LIZ.LJIIIZ();
if (ewF != null && ewF.LJIIJJI())
return true;
}
return false;
}
这串代码中,有MainFragment
,与其同一个文件夹下还有个类MainPageFragment
,想来,这两个MainFragment
、MainPageFragment
,一个是首页,一个是推荐。
再仔细看两个类的代码, MainFragment
中有一段代码
arduino
private void LIZ(String paramString1, String paramString2) {
boolean bool;
if (getActivity() != null) {
bool = (wob.LIZ.LIZLLL(getActivity())).LIZ;
} else {
bool = false;
}
EventMapBuilder eventMapBuilder = EventMapBuilder.newBuilder();
eventMapBuilder.appendParam("click_method", paramString1);
eventMapBuilder.appendParam("city_info", mfK.LIZ());
eventMapBuilder.appendParam("display", paramString2);
eventMapBuilder.appendParam("is_reshape", Boolean.valueOf(bool));
QiB.LIZ("homepage_fresh_click", eventMapBuilder.builder(), "com.ss.android.ugc.aweme.main.MainFragment");
}
MainPageFragment
中有一段代码
less
private void changeNearByTabName(String paramString, boolean paramBoolean) {
if (isViewValid() && !TextUtils.isEmpty(paramString) && eyN.LJIIJJI() == 1) {
if (TextUtils.equals(paramString, f7v.LIZ.LJFF("NEARBY")))
return;
if (wob.LIZ.LJJIIZ()) {
f7v.LIZ.LIZIZ("NEARBY", getString(2131831656));
return;
}
if (!SimpleLocationHelper.LIZJ() && fwj.LJ() == null) {
f7v.LIZ.LIZIZ("NEARBY", getString(2131831656));
return;
}
f7v.LIZ.LIZ("NEARBY", paramString, paramBoolean);
}
}
猜测MainPageFragment
应该是首页,MainFragment
应该是推荐。
enterLiveRoom
下面还有private void enterLiveRoom(String paramString)
方法,这个是用来进入直播间的;
kotlin
private void enterLiveRoom(String paramString) {
try {
StringBuilder stringBuilder = StringBuilderCache.get();
stringBuilder.append("sslocal://webcast_room/?");
stringBuilder.append(paramString);
SmartRouter.buildRoute((Context)this, StringBuilderCache.release(stringBuilder)).open();
return;
} catch (Exception exception) {
GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(exception);
return;
}
}
"sslocal://webcast_room/?"
这个路径找不到,不过找到一个类StartLiveActivity
,猜测这个可能就是enterLiveRoom这个方法跳转的类。
onCreate
scss
public static void com_ss_android_ugc_aweme_main_MainActivity_androidx_fragment_app_FixSpecialEffectControllerLancet_onCreate(MainActivity paramMainActivity, Bundle paramBundle) {
AppTrace.b(1898);
String str = paramMainActivity.getClass().getName();
List list = L48.LIZ.LIZ();
if (list != null && !list.isEmpty() && list.contains(str))
uPG.LIZ(paramMainActivity.getSupportFragmentManager());
paramMainActivity.com_ss_android_ugc_aweme_main_MainActivity__onCreate$___twin___(paramBundle);
AppTrace.e(1898);
}
这是onCreate方法,可以看到第七行,实现了onCreate_twin,在onCreate_twin,实现了enterAuthPage
和其他的初始化操作。
其他
private final boolean filterMoveEvent(MotionEvent paramMotionEvent)
,这个方法从名字来看,应该是重写了触摸反馈;
addTestInfo()
细看这个方法,除了ViewGroup是用xml文件写的,其他的TextInfo都是通过new
方法生成的。怪不得,当初跑无障碍服务的时候,快手可以用id来确定按钮,而抖音的控件id是动态的。当时还很疑惑,现在一看,原来这么回事啊。
多class.dex解压
回到dex2jar解压dex文件,因为报错OOM,所以只能使用最笨的办法,一个一个解压文件,代码块散乱在不同的class.dex文件中,虽然这样也能读,但好麻烦啊。
LoginActivity
举一个栗子,假如要找登录LoginActivity, 在AndroidMainfest文件中查找LoginActivity,发现有四个LoginActivity文件,
com.ss.android.ugc.aweme.account.business.login.DYLoginActivity
com.ss.android.ugc.aweme.commerce.sdk.login.ShadowLoginActivity
com.ss.android.ugc.aweme.im.sdk.chat.ChatCheckLoginActivity
com.ss.android.ugc.aweme.login.PushLoginActivity
而四个不同的类还都在不同的文件夹下面,加上一共42个class.dex,这得找到什么时候啊。不行,这个问题得解决,不就是oom吗,这个我熟。
必应一下,找到一个解决方案OutOfMemoryError #518
OutOfMemoryError
找到dex-tool文件夹,打开d2j_invoke.bat
文件,将java -Xms512m -Xmx2048m -cp "%CP%" %*
修改为java -Xms512m -Xmx10240m -cp "%CP%" %*
,将内存改到10G,新启一个终端,重新输入执行命令行
d2j-dex2jar douyin.apk douyin.jar
发现还是报错OOM,10G的内存大小对dex-tool加压apk文件还是不够,再改,一口气加到15G(内存也才16G),终于成功加压出jar文件。
用jd-gui文件打开jar文件,终于,可以一口气看所有的代码文件了。
查看当前活动
回到LoginActivity文件,此时找4个LoginActivity容易多了,但到底是哪个?
此时想到了两个方法,因为App的入口Activity是只有一个的,根据Intent-filter标签确定入口Splash文件,然后一步一步进入LoginActivity。
第二个方法就是adb命令行。
adb shell dumpsys activity activities
这个命令行是查看所有活动中的Activity,文章上面提到过。不过一下子把所有的Activity都列出来,每个TaskRecord的内容都太多了,看得眼花缭乱的,换一个命令行。
adb shell dumpsys activity top | findstr ACTIVITY
或adb shell dumpsys window | findstr mCurrentFocus
这两个命令行都是当前activity的,不过一个是查看最顶部的Activity,一个是查看当前正在获取焦点的Activity的。
执行上面的命令行,可以发现,登录Activity是DYLoginActivity。
ini
mCurrentFocus=Window{c1303d u0 com.ss.android.ugc.aweme/com.ss.android.ugc.aweme.account.business.login.DYLoginActivity}
DYLoginActivity.class
java
package com.ss.android.ugc.aweme.account.business.login;
import X.1W7;
import X.4YA;
import X.C11;
import X.L48;
import X.W0Z;
import X.dI3;
import X.sdz;
import X.uPG;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import com.bytedance.android.btm.api.BtmSDK;
import com.bytedance.apm.agent.v2.instrumentation.ActivityAgent;
import com.bytedance.covode.number.Covode;
import com.bytedance.ies.abmock.ABManager;
import com.bytedance.jarvis.trace.apptrace.AppTrace;
import com.bytedance.sysoptimizer.EnterTransitionCrashOptimizer;
import com.ss.android.ugc.aweme.ml.api.MLCommonService;
import com.ss.android.ugc.aweme.pad_impl.common.PadCommonServiceImpl;
import com.ss.android.ugc.playerkit.exp.PlayerSettingCenter;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public final class DYLoginActivity extends W0Z {
public Map<Integer, View> LJ;
static {
Covode.recordClassIndex(697363);
}
public final View LIZ(int paramInt) {
Map<Integer, View> map = this.LJ;
View view2 = map.get(Integer.valueOf(paramInt));
View view1 = view2;
if (view2 == null) {
view1 = findViewById(paramInt);
if (view1 != null) {
map.put(Integer.valueOf(paramInt), view1);
return view1;
}
view1 = null;
}
return view1;
}
public final void onCreate(Bundle paramBundle) {
AppTrace.bc(15600);
String str = getClass().getName();
List list = L48.LIZ.LIZ();
if (list != null && !list.isEmpty() && list.contains(str))
uPG.LIZ(getSupportFragmentManager());
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onCreate", true);
if (1W7.LIZIZ()) {
WeakReference<DYLoginActivity> weakReference = new WeakReference<DYLoginActivity>(this);
dI3.LIZ.LIZ(weakReference);
}
super.onCreate(paramBundle);
if (!getIntent().getBooleanExtra("use_one_key_login_half_screen_force", false) && !getIntent().getBooleanExtra("login_activity_without_anim", false)) {
overridePendingTransition(2130772644, 2130772061);
} else {
overridePendingTransition(0, 0);
}
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onCreate", false);
AppTrace.ec(15600);
}
public final void onPause() {
AppTrace.bc(15601);
super.onPause();
PadCommonServiceImpl.LIZ(false).LIZIZ();
AppTrace.ec(15601);
}
public final void onResume() {
AppTrace.bc(15603);
BtmSDK.INSTANCE.getService().getActivityLifeCycleAopListener().onActivityPreResumeAop((Activity)this);
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onResume", true);
super.onResume();
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onResume", false);
AppTrace.ec(15603);
}
public final void onSaveInstanceState(Bundle paramBundle) {
4YA 4YA = (4YA)ABManager.getInstance().getValueSafely(true, "transaction_too_large_opt", 31744, 4YA.class, C11.LIZ);
if (4YA != null && 4YA.LIZ && Build.VERSION.SDK_INT >= 29 && 4YA.LJIIIZ && 4YA.LIZJ != null) {
String str = getClass().getName();
Iterator iterator = 4YA.LIZJ.iterator();
while (iterator.hasNext()) {
if (str.equals(iterator.next())) {
System.out.println("skipOnSaveInstanceState");
return;
}
}
}
super.onSaveInstanceState(paramBundle);
}
public final void onStart() {
AppTrace.bc(15602);
BtmSDK.INSTANCE.getService().getActivityLifeCycleAopListener().onActivityPreStartAop((Activity)this);
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onStart", true);
super.onStart();
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onStart", false);
AppTrace.ec(15602);
}
public final void onStop() {
AppTrace.bc(15607);
super.onStop();
if (EnterTransitionCrashOptimizer.getContext() != null)
int j = Build.VERSION.SDK_INT;
int i = Build.VERSION.SDK_INT;
try {
return;
} finally {
Exception exception = null;
AppTrace.cc(15607);
AppTrace.ec(15607);
}
}
public final void onWindowFocusChanged(boolean paramBoolean) {
AppTrace.bc(15605);
if (PlayerSettingCenter.INSTANCE.getENABLE_ADJUST_BRIGHT_STRATEGY())
sdz.LIZ().LIZ((Activity)this, paramBoolean);
MLCommonService.instance().LIZ((Activity)this, paramBoolean);
ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onWindowFocusChanged", true);
super.onWindowFocusChanged(paramBoolean);
AppTrace.ec(15605);
}
}
虽然混淆了一部分代码,但大部分的代码都是能读的,而且也能根据命名知道方法是干什么的。
解读onStop
ini
public final void onStop() {
AppTrace.bc(15607);
super.onStop();
if (EnterTransitionCrashOptimizer.getContext() != null)
int j = Build.VERSION.SDK_INT;
int i = Build.VERSION.SDK_INT;
try {
return;
} finally {
Exception exception = null;
AppTrace.cc(15607);
AppTrace.ec(15607);
}
}
AppTrace
应该是字节的数据埋点,猜测的,包括ActivityAgent
也看起来像数据埋点相关的内容。
if
、i
、j
看起来毫无意义,推测可能是在smali里面增加代码破译的难度。
onCreate
最初看onCreate方法的时候,会疑惑为什么没有setContentView
,没有布局?登录页有两个页面,第一个是专门输入手机号码的,第二个是输入验证码的。由这个点切入,感觉可能是用Fragment来实现的,发现一行代码:
ini
uPG.LIZ(getSupportFragmentManager());
由这个找到uPG.class
、FragmentManager.class
、SpecialEffectsController
这三个类,再往下就找不下去了,因为代码已经有点看不懂了。从登录页的逻辑来看,是输入手机号码,勾选同意按钮,发送验证码,切换Fragment。从我的理解来看,发现是用uPG.class
、FragmentManager.class
、SpecialEffectsController
这三个类来操控Fragment的切换与否,但代码看了一圈,甚至连View的点击事件都没有找到。
哎,还是水平不够啊,居然在开始的时候就妄想破解抖音的客户端技术,连视频的门槛都还没见呢。
结论
目前也只是略微看了一下MainActivity.class
和DYLoginActivity.class
,发现单单这两个文件,就有好多自己可以学习的。
比如,原本自己是知道什么是单一职责原则的,但因为向来都是在小厂里打混,代码规范什么的,也只是了解设计模式几大原则,看了阿里的Java代码规范文档,但真正执行起来,还是怎么代码舒服就怎么代码。而在MainActivity.class
中,有好多部分都是严格遵守单一职责原则的。
虽然目前也只是到登录就卡住了,但我相信,随着不断尝试和努力,我最后会放弃的--从入门到放弃。
最后
感谢你看到最后,最后说一两点~
1.如果你有不同的看法,欢迎你在文章下面进行评论留言,本人一直是采取开放的态度,同不同意是你的事,采纳不采纳是我的事。
2.如果文章对你有帮助,或者你认可的话,请随手点个赞,支持一下。
3.最后不知道这篇文章能不能发出来,最后有多少人能看到,看到了又有多少能到最后的,既然看到最后了,就请顺手点个赞,给我更新文章的动力吧。
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除,同样,转载请提前告知)