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 的时候肯定会有相当深刻的理解。
网友评论