Jun_22.md

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,953评论 25 707
  • 12I'm trying to install glue 0.3 for OXS Mountain Lion an...
    逆行风阅读 521评论 0 0
  • 事情靠幻想不行,必须要去找实践性的证据去证明,证明自己的判断,看看有什么特别的错误,不可挽回,而后进行修正,很多...
    平常非常阅读 235评论 0 0
  • 午加餐:红薯晚水果:苹果 参考目标: 1份肉2份豆制品3份“新鲜”水果4份谷物/薯5份蔬菜,深绿色叶菜最好6杯水 ...
    静趣_儿童心理师阅读 118评论 0 1
  • 每个人手上都握着一把打开奇迹之门的钥匙,只是,明白这个道理的人少之又少。改变命运的奇迹,从不以匆匆的姿态出现。只要...
    jeffleefree阅读 521评论 1 2