Jun_22.md

作者: 深蓝Yearth | 来源:发表于2018-06-22 16:50 被阅读0次

    0x00 如何从零开始分析一个 app

    在接触到这个任务之前,我从没分析过 app,写过任何一个 Android 应用程序,也没接触过 java,甚至连面向对象编程这个概念都不熟悉。

    那么在这样的状况下,要如何开始分析一个 app 呢?

    搞清楚需求很重要。

    那么任务是什么呢?

    梳理三星音乐内的不同模块的源码,找到支撑 APK 功能的主要代码模块,例如下载,播放,搜索,http 链接,文件打开等等,梳理函数之间的调用关系,形成较为完整的模块逻辑,例如播放中有哪几个功能函数,调用逻辑关系,最终以文档形式反馈。

    总结一下,我需要完成的任务有:

    • 找到完成主要功能的函数,分析其实现
    • 分析函数之间的调用关系

    既然这个任务拆分成了相对比较明确的问题,就可以着手开始尝试解决这些相对明确的问题了。

    0x01 反汇编

    首先我要能够阅读这款 app 中的代码,然而这是一个打包好的 app,并没有源码,应该怎么办?

    答案显而易见,使用反汇编工具对 app 进行反汇编,然后阅读反汇编之后的代码。

    一开始我使用的是 APK改之理,这个工具能够对 app 进行反汇编,修改,重新打包等等,功能非常详尽,不过它反汇编出来的是 smail 汇编代码,对于这么大一款 app,如果强行分析汇编代码一定会十分耗时,并且不怎么见得到收益,所以扁哥给我推荐了一款反汇编工具:jadx

    目前看来,jadx 完全满足我当前的需求,由于目标 app 并没有加壳和混淆,反汇编出来的代码看起来甚至和源码差不多,安装上有一些小坑,不过问题不大,谷歌一下就能解决。

    0x02 定位入口

    现在的情况相当于是把一款 app 的源码给我,让我对其分析,然而刚开始的时候,我甚至连语法都搞不懂,直接开始盲目的分析显然也不太现实,所以在豪哥的帮助下开始尝试定位一个入口,从这个入口开始分析。

    抓包定位

    一开始的时候想的是首先分析下载这个功能。那么如何定位到下载呢?尝试全局搜索 Downloader 并没有什么比较理想的结果。

    于是豪哥提出了一个思路:(虽然后面证明这个思路没什么卵用,但确实给了无从下手的我一个比较明确的方法)

    通过抓包的手段,抓到这款 app 在下载文件的时候的链接,找到其中特征的部分,在代码中全局搜索,尝试定位到 Downloader 模块。

    那么问题就再次细化,当前关心的问题就是:

    • 如何对手机进行抓包
    • 如何抓取下载的数据

    Fiddler

    结合前辈们的经验来看,对手机 app 进行抓包,Fiddler 是个不错的选择。

    安装上 Fiddler 没有什么坑,选择好安装路径即可。

    在抓包的时候记得先验证手机和运行 Fiddler 的机器在同一网段,能够互相 ping 通即可,最后记得关闭 Win10 的防火墙。

    一番折腾之后,成功抓到了 Samsung Music 下载音乐时候的包:

    Request Headers
    GET /content/01/100/1100050-MP3-128K-FTD.mp3?sign=CQqSIbb5AME7PJqxVitA+/Gax0hhPTEwMDM5ODY3Jms9QUtJRE1Fdm53SXdwNFlqUlU1NHhxd3VLQlRYMExOOWdJVFNRJmU9MTUyODk4Mzg2NiZ0PTE1Mjg5NjIyNjYmcj0xOTM3Nzg3NDQ2JmY9L2NvbnRlbnQvMDEvMTAwLzExMDAwNTAtTVAzLTEyOEstRlRELm1wMyZiPXVsdGltYXRl&transDeliveryCode=SS@4@1528962266@D@20BFE04507592793 HTTP/1.1
    

    然而对这串数据不管怎么删减,都没法搜索到相关的代码。

    然后,虽然不太重要,不过验证可知:/content/01/100/1100050-MP3-128K-FTD.mp3 对于同一目标音乐是不变的,并不是随机生成的。

    定位 Activity

    抓包定位 Downloader 模块的思路失败了,于是豪哥又提出了一个新的思路:

    定位目标 app 的某一个存在 Download 按钮的 activity,分析这个 activity,尝试找到下载模块。

    有现成的 adb 命令能够实现这个思路:

    adb shell dumpsys activity | grep "mFocusedActivity"
    

    然而测试发现,这个命令并不能在目标手机 Samsung S8 中生效,导出 dumpsys activity 分析发现并没有 mFocusedActivity 关键字,于是想了个笨办法:

    对比上面的命令能够生效的 dumpsys activity 和 Samsung S8 的 dumpsys activity,找到对于顶层 activity 描述的关键词。

    方法虽然笨,效果还不错,最终发现 Samsung S8 的关键字是 mResumedActivity,这里仅记录一下测试过的手机能使用的命令:

    adb shell dumpsys activity | grep "mResumedActivity" # 三星 S8
    adb shell dumpsys activity | grep "mFocusedActivity" # 坚果 Pro2
    

    分析历程(持续更新ing...)

    通过上面的方法,定位到了第一个开始分析的 activity:com.samsung.android.app.music.common.player.PlayerController

    首先来看看主类的声明 :(当然一开始我并不知道什么是主类)

    public class PlayController implements OnMediaChangeObserver, com.samsung.android.app.music.core.service.mediacenter.OnMediaChangeObserver
    

    可以看出它继承了两个接口:

    public interface OnMediaChangeObserver {
        void onExtraChanged(String str, Bundle bundle);
    
        void onMetaChanged(Meta meta, PlayState playState);
    
        void onPlayStateChanged(PlayState playState);
    }
    
    public interface OnMediaChangeObserver {
        void onExtrasChanged(String str, Bundle bundle);
    
        void onMetadataChanged(MusicMetadata musicMetadata);
    
        void onPlaybackStateChanged(MusicPlaybackState musicPlaybackState);
    
        void onQueueChanged(List<QueueItem> list, Bundle bundle);
    }
    

    总共实现了四个方法:

    onExtraChanged

    public void onExtraChanged(String action, Bundle data) {
            if ("com.samsung.musicplus.action.DRM_REQUEST".equals(action)) {
                updatePlayState(this.mPlayerController.isPlaying());
            }
        }
    
    private void updatePlayState(boolean isPlaying) {
            TalkBackUtils.setContentDescriptionAll(this.mContext, this.mPlay, isPlaying ? R.string.tts_pause : R.string.tts_play);
            if (this.mPlay.isActivated() != isPlaying) {
                this.mPlay.setActivated(isPlaying);
                if (this.mPlayToPauseAnimationResId != -1 && this.mPauseToPlayAnimationResId != -1) {
                    ImageView iv = (ImageView) this.mPlay.findViewById(R.id.play_pause_icon);
                    if (isPlaying) {
                        iv.setImageResource(this.mPlayToPauseAnimationResId);
                    } else {
                        iv.setImageResource(this.mPauseToPlayAnimationResId);
                    }
                    AnimationDrawable d = (AnimationDrawable) iv.getDrawable();
                    if (this.mPlay.isLaidOut()) {
                        d.start();
                    } else {
                        iv.setImageDrawable(d.getFrame(d.getNumberOfFrames() - 1));
                    }
                }
            }
        }
    
    public static void setContentDescriptionAll(Context context, View v, int stringResId) {
            v.setContentDescription(getButtonDescription(context, stringResId));
            if (DefaultUiUtils.isHoverUiEnabled(context)) {
                HoverPopupWindowCompat.setContent(v, context.getString(stringResId));
            }
        }
    
    • 这里最主要的实现的功能是更新 播放 - 暂停 按钮的状态。

    onMetaChanged

    public void onMetaChanged(Meta m, PlayState s) {
            updatePlayState(s.isPlaying);
            setPrevNextEnabled(m.listCount != 0);
        }
    
    private void setPrevNextEnabled(boolean enabled) {
            float prevNextAlpha = enabled ? 1.0f : 0.37f;
            if (this.mPrev != null) {
                this.mPrev.setEnabled(enabled);
                this.mPrev.setAlpha(prevNextAlpha);
            }
            if (this.mNext != null) {
                this.mNext.setEnabled(enabled);
                this.mNext.setAlpha(prevNextAlpha);
            }
        }
    
    • 这里最主要的功能是设置 前后 按钮是否可按,如果可以颜色深度是 100%,否则是 37%。

    onPlaybackStateChanged

    public void onPlaybackStateChanged(MusicPlaybackState s) {
            updatePlayState(s.isSupposedToPlaying());
        }
    
    • 因为上面分析过 updatePlayState,所以这里很容易知道功能是更新 播放 - 暂停 按钮的状态

    onQueueChanged

    public void onQueueChanged(List<QueueItem> list, Bundle extras) {
        }
    
    • 这是个空方法,并没有实际实现

    经过上面的分析,大致了解一点点关于 Android 编程的内容,不再像一开始那样连一两行代码看起来都头疼,紧接着又分析了一些简单的 activity:

    OnAirViewPopupListener

    public interface OnAirViewPopupListener {
            View getAirView(View view);
        }
    
    public View getAirView(View v) {
                Context context = this.mActivity.getApplicationContext();
                switch (v.getId()) {
                    case R.id.next_btn:
                        String nextTitle = UiUtils.getTitle(context, this.mPlayerController.getNextUri());
                        if (nextTitle == null) {
                            nextTitle = TalkBackUtils.getButtonDescription(context, (int) R.string.tts_next);
                        }
                        return UiUtils.getAirTextView(this.mActivity, nextTitle);
                    case R.id.prev_btn:
                        String prevTitle = UiUtils.getTitle(context, this.mPlayerController.getPrevUri());
                        if (prevTitle == null) {
                            prevTitle = TalkBackUtils.getButtonDescription(context, (int) R.string.tts_previous);
                        }
                        return UiUtils.getAirTextView(this.mActivity, prevTitle);
                    default:
                        return null;
                }
            }
    
    • 根据分析和一定的猜测,这里应该是获取播放的音乐的标题

    PlayController

    public PlayController(Activity activity, View view, IPlayerController playerController, ForwardRewindInputListener forwardRewindInputListener, MediaChangeObservable mediaChangeObservable, com.samsung.android.app.music.core.service.mediacenter.MediaChangeObservable coreMediaChangeObservable) {
            this.mContext = activity.getApplicationContext();
            this.mPlayerController = playerController;
            this.mPrev = view.findViewById(R.id.prev_btn);
            this.mNext = view.findViewById(R.id.next_btn);
            this.mPlay = view.findViewById(R.id.play_pause_btn);
            ConvertTouchEventListener convertTouchEventListener = new ConvertTouchEventListener();
            if (this.mPrev != null) {
                this.mPrev.setOnKeyListener(convertTouchEventListener);
                this.mPrev.setOnTouchListener(forwardRewindInputListener);
                TalkBackUtils.setContentDescriptionAll(this.mContext, this.mPrev, R.string.tts_previous);
            }
            if (this.mNext != null) {
                this.mNext.setOnKeyListener(convertTouchEventListener);
                this.mNext.setOnTouchListener(forwardRewindInputListener);
                TalkBackUtils.setContentDescriptionAll(this.mContext, this.mNext, R.string.tts_next);
            }
            this.mPlay.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    PlayController.this.mPlayerController.togglePlay();
                    if (PlayController.this.mOnPlayClickListener != null) {
                        PlayController.this.mOnPlayClickListener.onClick(v);
                    }
                }
            });
            setAirView(activity);
            mediaChangeObservable.registerMediaChangeObserver(this);
        }
    
    • 设置按钮事件以及备注(前,后,播放)

    随后接触到了 Binder 通信机制,这里问了东哥很多问题,对 Binder 通信机制大致有了一个认识:

    getBuffering

    public int getBuffering() {
            return ServiceUtils.getBuffering();
        }
    
    
    public static int getBuffering() {
            try {
                if (sService != null) {
                    return sService.buffering();
                }
                return -1;
            } catch (RemoteException e) {
                e.printStackTrace();
                return -1;
            }
        }
    
    public int buffering() throws RemoteException {
                    Parcel _data = Parcel.obtain();
                    Parcel _reply = Parcel.obtain();
                    try {
                        _data.writeInterfaceToken(Stub.DESCRIPTOR);
                        this.mRemote.transact(23, _data, _reply, 0);
                        _reply.readException();
                        int _result = _reply.readInt();
                        return _result;
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
    
    • 实现进程之间的通信,使用的方法号为 23:static final int TRANSACTION_buffering = 23

    getNextUri

    public String getNextUri() {
            return ServiceUtils.getNextUri();
        }
    
    public static String getNextUri() {
            if (sService == null) {
                return null;
            }
            String uri = null;
            try {
                return sService.getNextUri();
            } catch (RemoteException e) {
                e.printStackTrace();
                return uri;
            }
        }
    
    public String getNextUri() throws RemoteException {
                    Parcel _data = Parcel.obtain();
                    Parcel _reply = Parcel.obtain();
                    try {
                        _data.writeInterfaceToken(Stub.DESCRIPTOR);
                        this.mRemote.transact(51, _data, _reply, 0);
                        _reply.readException();
                        String _result = _reply.readString();
                        return _result;
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
    
    • 使用 52 号方法进行通信:static final int TRANSACTION_getNextUri = 51

    getPosition

    public long getPosition() {
            return ServiceUtils.getPosition();
        }
    
    public static long getPosition() {
            try {
                if (sService != null) {
                    return sService.position();
                }
                return -1;
            } catch (RemoteException e) {
                e.printStackTrace();
                return -1;
            }
        }
    
    public long position() throws RemoteException {
                    Parcel _data = Parcel.obtain();
                    Parcel _reply = Parcel.obtain();
                    try {
                        _data.writeInterfaceToken(Stub.DESCRIPTOR);
                        this.mRemote.transact(22, _data, _reply, 0);
                        _reply.readException();
                        long _result = _reply.readLong();
                        return _result;
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
    
    • 使用 22 号方法进行通信:static final int TRANSACTION_position = 22

    getPrevUri

    public String getPrevUri() {
            return ServiceUtils.getPrevUri();
        }
    
    public static String getPrevUri() {
            if (sService == null) {
                return null;
            }
            String uri = null;
            try {
                return sService.getPrevUri();
            } catch (RemoteException e) {
                e.printStackTrace();
                return uri;
            }
        }
    
    public String getPrevUri() throws RemoteException {
                    Parcel _data = Parcel.obtain();
                    Parcel _reply = Parcel.obtain();
                    try {
                        _data.writeInterfaceToken(Stub.DESCRIPTOR);
                        this.mRemote.transact(52, _data, _reply, 0);
                        _reply.readException();
                        String _result = _reply.readString();
                        return _result;
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
    
    • 使用 52 号方法进行通信:static final int TRANSACTION_getPrevUri = 52

    在分析了这样的方法之后,通过搜索关键字的手段找到了本地实现播放的一些方法:

    play

    private void play() {
            play(false);
        }
    
    
    private void play(boolean applyFadeUp) {
            iLog.d(TAG, "play() - mPlayerState : " + this.mPlayerState + ", applyFadeUp : " + applyFadeUp);
            if (!CscFeatures.SUPPORT_MUSIC_PLAYBACK_DURING_CALL && !CallStateChecker.isCallIdle(this.mContext)) {
                iLog.d(TAG, "play() - Can't play during call.");
                this.mOnSoundPlayerChangedListener.onError(1);
            } else if (this.mAudioManager.requestAudioFocus(this.mAudioFocusChangeListener, 3, 1) == 0) {
                iLog.d(TAG, "play() - Can't play because audio focus request is failed.");
            } else if (!isPlaying()) {
                if (canPlayState()) {
                    if (applyFadeUp) {
                        this.mPlayerHandler.sendEmptyMessageDelayed(0, 20);
                    } else {
                        this.mPlayer.setVolume(this.mMaxVolume, this.mMaxVolume);
                    }
                    this.mPlayer.start();
                    this.mPlayerState = 4;
                    this.mOnSoundPlayerStateListener.onPlayStateChanged(this);
                    setPlaybackState(getPosition());
                    setBatteryTemperatureCheck(true);
                    if (USAFeatures.REGIONAL_USA_GATE_ENABLED) {
                        GateMessageUtils.printMessage("AUDI_PLAYING", this.mUri.getPath());
                        return;
                    }
                    return;
                }
                setDataSource(this.mUri, true);
                this.mOnSoundPlayerStateListener.onMetaChanged(this);
            }
        }
    
    • 判断是否支持打电话的时候播放音乐,并且判断是否正在打电话,如果二者同时满足则打印 log 并且返回一个错误
    • 获取音频焦点,获取失败则退出
    • 判断是否正在播放,如果正在播放则退出
    • 通过 mPlayerState 来判断是否满足播放,如果不满足则退出
    • applyFadeUp 暂时没能搞清楚是判断什么,如果为真则延迟发送一些消息,否则设置音量
    • 开始播放并且设置播放状态
    • 更新播放按钮的状态
    • 获取当前播放点
    • 监控电池状态
    • setDataSource(this.mUri, true) 用于设置音频文件路径
    • this.mOnSoundPlayerStateListener.onMetaChanged(this) 暂且意义不明

    pause

    public void pause() {
            iLog.d(TAG, "pause() - mPlayerState : " + this.mPlayerState);
            if (this.mPlayerState == 4) {
                this.mPlayerHandler.removeMessages(0);
                this.mPlayer.pause();
                this.mPlayerState = 5;
                this.mOnSoundPlayerStateListener.onPlayStateChanged(this);
                setPlaybackState(getPosition());
                setBatteryTemperatureCheck(false);
            }
        }
    
    • 判断播放状态,如果正在播放则状态值为 4
    • 取消 sendEmptyMessageDelayed
    • 暂停,设置暂停状态,暂停状态值为 5
    • 更新播放按钮状态
    • 获取当前播放点
    • 监控电池状态

    stop

    public void stop() {
            iLog.d(TAG, "stop() - mPlayerState : " + this.mPlayerState);
            this.mPlayerHandler.removeMessages(0);
            if (isPlaying()) {
                this.mPlayer.pause();
                this.mPlayerState = 5;
            }
            seek(0);
            this.mPlayer.stop();
            this.mPlayerState = 6;
            this.mOnSoundPlayerStateListener.onPlayStateChanged(this);
            this.mOnSoundPlayerStateListener.onSeekComplete(this);
            reset();
        }
    
    • 取消 sendEmptyMessageDelayed
    • 如果正在播放,则先暂停播放,并且设置播放状态为暂停
    • seek 暂且意义不明
    • 停止,设置播放状态为停止,停止状态值为 6
    • 更新播放按钮状态
    • this.mOnSoundPlayerStateListener.onSeekComplete(this) 暂且意义不明
    • 重置 MediaPlayer 对象

    reset

    private void reset() {
            iLog.d(TAG, "reset()");
            this.mPlayer.reset();
            this.mPlayerState = 0;
    }
    
    • 重置 MediaPlayer 对象
    • 设置播放状态为初始状态,初始状态值为 0

    seek

    public void seek(long position) {
            if (this.mPlayerState > 2) {
                setPlaybackState(position);
                this.mPlayer.seekTo((int) position);
            }
        }
    
    • 更新播放状态

    前面的分析虽然找到了很多方法的实现,不过实际上有些杂乱无章,不过这帮助我逐渐找到了分析的节奏和方法,后面逐渐发现,以类为目标来分析是比较清晰的,这里找到了比较关键的一个类:PlayerListManager,顾名思义,播放列表管理器

    PlayerListManager

    这是一个纯自定义的类,没有继承其他任何类或者接口。

    OnListChangeListener

    interface OnListChangeListener {
            void onListChanged(boolean z, int i);
        }
    
    • 这个接口的作用是监听播放列表

    getNowPlayingListPosition

    public int getNowPlayingListPosition() {
            if (this.mShuffleMode == 1) {
                synchronized (this.mShuffleList) {
                    if (!this.mShuffleList.isEmpty() && this.mShufflePlayPos > -1) {
                        this.mPlayPos = ((Integer) this.mShuffleList.get(this.mShufflePlayPos)).intValue();
                    }
                }
            }
            Log.d("SMUSIC-SV-List", "getCurrentListPosition : " + this.mPlayPos);
            return this.mPlayPos;
        }
    
    • 获取当前播放列表的位置

    getNextPosition

    private int[] getNextPosition(int position, int shufflePosition) {
            if (this.mShuffleMode == 1) {
                if (shufflePosition < this.mShuffleList.size() - 1) {
                    shufflePosition++;
                } else {
                    shufflePosition = 0;
                }
                if (this.mShuffleList.isEmpty()) {
                    position = 0;
                } else {
                    position = ((Integer) this.mShuffleList.get(shufflePosition)).intValue();
                }
            } else if (position < this.mPlayListLength - 1) {
                position++;
            } else {
                position = 0;
            }
            return new int[]{position, shufflePosition};
        }
    
    • 获取下一首在播放列表中的位置

    getNextPositionMediaUri

    public Uri getNextMediaUri() {
            int position = getNextPosition(this.mPlayPos, this.mShufflePlayPos, false);
            if (this.mPlayList == null || this.mPlayListLength == 0) {
                return null;
            }
            Uri uri = appendWithBaseUri(this.mPlayList[position]);
            iLog.d("SV-List", "getNextMediaUri() Uri : " + uri);
            return uri;
        }
    
    private Uri appendWithBaseUri(long audioId) {
            if (audioId > -1) {
                return ContentUris.withAppendedId(getCurrentBaseUri(), audioId);
            }
            return null;
        }
    
    private Uri getCurrentBaseUri() {
            if (this.mBaseUri == null) {
                changeBaseUri(Tracks.CONTENT_URI);
            }
            return this.mBaseUri;
        }
    
    private void changeBaseUri(Uri uri) {
            registerContentObserver(uri);
            this.mBaseUri = uri;
        }
    
    private void registerContentObserver(Uri uri) {
            if (this.mObserver != null) {
                unregisterContentObserver();
                this.mContentResoler.registerContentObserver(uri, false, this.mObserver);
                this.mIsRegistered = true;
            }
        }
    
    • 获取下一首音乐对应的 Uri

    当前分析到这里.......持续分析 ing,之后的分析计划以类为对象进行分析,搞清楚每个类的功能,具体怎么实现的,然后找到其调用关系,应该就能大致理解这个 app 的实现了。

    另外通过这两天无数次的使用搜索引擎搜索相关功能发现,如果有过 Music App 的开发经验,在分析相关 app 的时候肯定会有相当深刻的理解。

    相关文章

      网友评论

          本文标题:Jun_22.md

          本文链接:https://www.haomeiwen.com/subject/tgakyftx.html