反编译学习:dex2jar+jd-gui--看大佬是如何代码的

前言

阅读【Xposed】抖音短视频检测 Xposed 分析这篇文章时,突然有感,要不,也去看一下抖音的代码是如何写的,学习一下大厂是如何规范写代码的。再加上自己懂一些反编译的知识,于是就有了这篇文章。

当然这篇文章不会深入研究抖音Android版本的技术,只是从表面稍微学习一下。如果文章有错误或者描述不当,请JYM见谅。

准备工作

  1. adb环境变量。这个和下面的java环境变量在安装Android Studio的时候就搞定了。
  2. java环境变量。
  3. apktool下载。查看apk中的资源,这个可下载可不下载。
  4. dex2jar安装。用来解压dex文件。
  5. jd-gui安装。查看加压后的jar文件中的具体内容。
  6. 抖音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,想来,这两个MainFragmentMainPageFragment,一个是首页,一个是推荐。

再仔细看两个类的代码, 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 ACTIVITYadb 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也看起来像数据埋点相关的内容。

ifij看起来毫无意义,推测可能是在smali里面增加代码破译的难度。

onCreate

最初看onCreate方法的时候,会疑惑为什么没有setContentView,没有布局?登录页有两个页面,第一个是专门输入手机号码的,第二个是输入验证码的。由这个点切入,感觉可能是用Fragment来实现的,发现一行代码:

ini 复制代码
uPG.LIZ(getSupportFragmentManager()); 

由这个找到uPG.classFragmentManager.classSpecialEffectsController这三个类,再往下就找不下去了,因为代码已经有点看不懂了。从登录页的逻辑来看,是输入手机号码,勾选同意按钮,发送验证码,切换Fragment。从我的理解来看,发现是用uPG.classFragmentManager.classSpecialEffectsController这三个类来操控Fragment的切换与否,但代码看了一圈,甚至连View的点击事件都没有找到。

哎,还是水平不够啊,居然在开始的时候就妄想破解抖音的客户端技术,连视频的门槛都还没见呢。

结论

目前也只是略微看了一下MainActivity.classDYLoginActivity.class,发现单单这两个文件,就有好多自己可以学习的。

比如,原本自己是知道什么是单一职责原则的,但因为向来都是在小厂里打混,代码规范什么的,也只是了解设计模式几大原则,看了阿里的Java代码规范文档,但真正执行起来,还是怎么代码舒服就怎么代码。而在MainActivity.class中,有好多部分都是严格遵守单一职责原则的。

虽然目前也只是到登录就卡住了,但我相信,随着不断尝试和努力,我最后会放弃的--从入门到放弃。


最后

感谢你看到最后,最后说一两点~

1.如果你有不同的看法,欢迎你在文章下面进行评论留言,本人一直是采取开放的态度,同不同意是你的事,采纳不采纳是我的事。

2.如果文章对你有帮助,或者你认可的话,请随手点个赞,支持一下。

3.最后不知道这篇文章能不能发出来,最后有多少人能看到,看到了又有多少能到最后的,既然看到最后了,就请顺手点个赞,给我更新文章的动力吧。

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除,同样,转载请提前告知)

相关推荐
uwvwko2 小时前
BUUCTF——web刷题第一页题解
android·前端·数据库·php·web·ctf
fzxwl2 小时前
隆重推荐(Android 和 iOS)UI 自动化工具—Maestro
android·ui·ios
LittleLoveBoy5 小时前
踩坑:uiautomatorviewer.bat 打不开
android
居然是阿宋5 小时前
Android核心系统服务:AMS、WMS、PMS 与 system_server 进程解析
android
CGG928 小时前
【单例模式】
android·java·单例模式
kp000008 小时前
PHP弱类型安全漏洞解析与防范指南
android·开发语言·安全·web安全·php·漏洞
编程乐学(Arfan开发工程师)13 小时前
06、基础入门-SpringBoot-依赖管理特性
android·spring boot·后端
androidwork14 小时前
使用 Kotlin 和 Jetpack Compose 开发 Wear OS 应用的完整指南
android·kotlin
繁依Fanyi15 小时前
Animaster:一次由 CodeBuddy 主导的 CSS 动画编辑器诞生记
android·前端·css·编辑器·codebuddy首席试玩官
奔跑吧 android17 小时前
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
android·bluetooth·bt·aosp13·storagemodule