目次: Android
昨日(2017年11月8日の日記参照)の続きです。
本来見たかった道をざっくりまとめておくと、
これが2017年11月6日の日記の前半で分かった部分。
これが2017年11月6日の日記の後半で分かった部分。
これが2017年11月7日の日記で分かった部分。
これが2017年11月8日の日記で分かった部分です。そのあとはlooperとは何ぞや?という点を追いかけていましたが、まだわからない状態です。
肝心のACodec::BaseState::mCodecに何が入っているのか?についてはUninitializedStateを手掛かりに見ていきます。
//android/frameworks/av/media/libstagefright/ACodec.cpp
struct ACodec::BaseState : public AState {
BaseState(ACodec *codec, const sp<AState> &parentState = NULL);
...
ACodec *mCodec; //★★これが知りたい★★
//★★UninitializedStateを手掛かりに見てみる★★
struct ACodec::UninitializedState : public ACodec::BaseState {
...
ACodec::UninitializedState::UninitializedState(ACodec *codec)
: BaseState(codec) { //★★BaseStateに丸投げ★★
}
//★★BaseStateを見てみる★★
struct ACodec::BaseState : public AState {
BaseState(ACodec *codec, const sp<AState> &parentState = NULL);
...
ACodec::BaseState::BaseState(ACodec *codec, const sp<AState> &parentState)
: AState(parentState),
mCodec(codec) { //★★引数をそのまま設定しているだけ★★
}
//★★UninitializedStateの生成個所を探す★★
ACodec::ACodec()
: mQuirks(0),
...
mDescribeHDRStaticInfoIndex((OMX_INDEXTYPE)0) {
mUninitializedState = new UninitializedState(this); //★★thisが指すものはACodec★★
mLoadedState = new LoadedState(this);
つまりACodec::BaseState::mCodecは、UninitializeStateを生成したACodecです。もう一つの謎getLooper()が何を返すのか?も見てみます。
//android/frameworks/av/include/media/stagefright/foundation/AHandler.h
struct AHandler : public RefBase {
...
wp<ALooper> getLooper() const {
return mLooper; //★★mLooperを返すだけ★★
}
...
inline void setID(ALooper::handler_id id, wp<ALooper> looper) {
mID = id;
mLooper = looper; //★★mLooperはsetIDの引数そのまま★★
}
//android/frameworks/av/include/media/libstagefright/foundation/ALooperRoster.cpp
ALooper::handler_id ALooperRoster::registerHandler(
const sp<ALooper> looper, const sp<AHandler> &handler) {
Mutex::Autolock autoLock(mLock);
if (handler->id() != 0) {
CHECK(!"A handler must only be registered once.");
return INVALID_OPERATION;
}
HandlerInfo info;
info.mLooper = looper;
info.mHandler = handler;
ALooper::handler_id handlerID = mNextHandlerID++;
mHandlers.add(handlerID, info);
handler->setID(handlerID, looper); //★★setIDを呼んでいる個所はここだけ★★
return handlerID;
}
//media/libstagefright/foundation/ALooper.cpp
ALooperRoster gLooperRoster;
...
ALooper::handler_id ALooper::registerHandler(const sp<AHandler> &handler) {
return gLooperRoster.registerHandler(this, handler);
}
ALooper::registerHandlerはALooperをAHandlerに登録する仕組み、AHandler::getLooper()はAHandlerに登録されたALooperを返す仕組みのようです。取得/設定が一致しないのでややこしいです。設計を失敗したのかなあ?
例えばAHandler *hogeとALooper *fugaがあってfuga->registerHandler(hoge)としたならば、hoge->getLooper()は先ほど登録したfugaを返します。
ちなみにACodecはAHandlerを継承しているのでgetLooper()関数を持っています。
ここまで分かればALooper::registerHandler()を呼んでいる個所を見て、引数がACodecオブジェクトであろう場所を見つければ、looperが指すのはどのALooperなのか?がやっと判明します。
しかしregisterHandler()の呼び出し箇所は非常に多くて、追いきれません。うーん、別のアプローチが必要でしょうか……?
この記事にコメントする
目次: Android
昨日(2017年11月7日の日記参照)の続きです。
どうもAndroidのメッセージシステムのたらい回しが激しすぎて、話が一向に進みません。本来見たかった道をざっくりまとめておくと、
これが2017年11月6日の日記の前半部分です。post()によってメッセージがキューに追加されます。
これが2017年11月6日の日記の後半部分です。キューに追加されたメッセージは別スレッドで処理され、mObserverなるものに渡されていました。
そして2017年11月7日の日記を丸々使い、OMXNodeInstance::mObserverの正体がCodecObserverだと思われるところまで来ました。
//android/frameworks/av/media/libstagefright/ACodec.cpp
struct CodecObserver : public BnOMXObserver {
...
// from IOMXObserver
virtual void onMessages(const std::list<omx_message> &messages) {
...
sp<AMessage> notify = mNotify->dup();
bool first = true;
sp<MessageList> msgList = new MessageList();
for (std::list<omx_message>::const_iterator it = messages.cbegin();
it != messages.cend(); ++it) {
const omx_message &omx_msg = *it;
if (first) {
notify->setInt32("node", omx_msg.node);
first = false;
}
sp<AMessage> msg = new AMessage;
//★★omx_msg.typeはOMX::OnFillBufferDone() にてFILL_BUFFER_DONEに設定★★
msg->setInt32("type", omx_msg.type);
switch (omx_msg.type) {
...
case omx_message::FILL_BUFFER_DONE:
{
//★★omx_messageからAMessageに変換している★★
msg->setInt32(
"buffer", omx_msg.u.extended_buffer_data.buffer);
msg->setInt32(
"range_offset",
omx_msg.u.extended_buffer_data.range_offset);
msg->setInt32(
"range_length",
omx_msg.u.extended_buffer_data.range_length);
msg->setInt32(
"flags",
omx_msg.u.extended_buffer_data.flags);
msg->setInt64(
"timestamp",
omx_msg.u.extended_buffer_data.timestamp);
msg->setInt32(
"fence_fd", omx_msg.fenceFd);
break;
}
...
}
msgList->getList().push_back(msg);
}
notify->setObject("messages", msgList);
notify->post(); //★★notifyとは??★★
}
また変なものが出てきました。notify = mNotify->dup()なので、次にmNotifyが何者かを見ていきます。
//android/frameworks/av/media/libstagefright/ACodec.cpp
struct CodecObserver : public BnOMXObserver {
...
void setNotificationMessage(const sp<AMessage> &msg) {
mNotify = msg;
}
bool ACodec::UninitializedState::onAllocateComponent(const sp<AMessage> &msg) {
...
sp<CodecObserver> observer = new CodecObserver;
IOMX::node_id node = 0;
...
status_t err = NAME_NOT_FOUND;
for (size_t matchIndex = 0; matchIndex < matchingCodecs.size();
++matchIndex) {
componentName = matchingCodecs[matchIndex];
quirks = MediaCodecList::getQuirksFor(componentName.c_str());
pid_t tid = gettid();
int prevPriority = androidGetThreadPriority(tid);
androidSetThreadPriority(tid, ANDROID_PRIORITY_FOREGROUND);
err = omx->allocateNode(componentName.c_str(), observer, &mCodec->mNodeBinder, &node); //★★11月7日の日記参照★★
...
notify = new AMessage(kWhatOMXMessageList, mCodec);
observer->setNotificationMessage(notify); //★★ここで設定している★★
従ってmNotifyはAMessage(kWhatOMXMessageList, mCodec)です。dup()は複製しているだけでしょうから、notify->post()はAMessage::post()が呼ばれるのでしょう。
//android/frameworks/av/media/libstagefright/foundation/AMessage.cpp
status_t AMessage::post(int64_t delayUs) {
sp<ALooper> looper = mLooper.promote();
if (looper == NULL) {
ALOGW("failed to post message as target looper for handler %d is gone.", mTarget);
return -ENOENT;
}
looper->post(this, delayUs); //★★たらい回し再び、mLooperとは?★★
return OK;
}
...
AMessage::AMessage(uint32_t what, const sp<const AHandler> &handler)
: mWhat(what),
mNumItems(0) {
setTarget(handler); //★★mLooperはここから設定★★
}
...
void AMessage::setTarget(const sp<const AHandler> &handler) {
if (handler == NULL) {
mTarget = 0;
mHandler.clear();
mLooper.clear();
} else {
mTarget = handler->id();
mHandler = handler->getHandler();
mLooper = handler->getLooper(); //★★mLooperはAMessageコンストラクタの2番目の引数のgetLooper() が返す値★★
}
}
うーん、また訳の分からないものが出てきましたね…。
この記事にコメントする
目次: Android
昨日(2017年11月6日の日記参照)の続きです。
メッセージがOMXNodeInstance::onMessages() 関数にたどり着き、次にOMXNodeInstance::mObserverに渡されていることはわかりましたが、これは一体何者でしょうか?
//android/frameworks/av/media/libstagefright/include/OMXNodeInstance.h
struct OMXNodeInstance {
...
private:
...
sp<IOMXObserver> mObserver;
//android/frameworks/av/media/libstagefright/omx/OMXNodeInstance.cpp
OMXNodeInstance::OMXNodeInstance(
OMX *owner, const sp<IOMXObserver> &observer, const char *name)
: mOwner(owner),
mNodeID(0),
mHandle(NULL),
mObserver(observer), //★★コンストラクタの2番目の引数observerで初期化している★★
mDying(false),
mSailed(false),
mQueriedProhibitedExtensions(false),
mBufferIDCount(0)
{
//android/frameworks/av/media/libstagefright/omx/OMX.cpp
status_t OMX::allocateNode(
const char *name, const sp<IOMXObserver> &observer,
sp<IBinder> *nodeBinder, node_id *node) {
...
OMXNodeInstance *instance = new OMXNodeInstance(this, observer, name); //★★allocateNodeの2番目の引数observerを渡している★★
残念ながらallocateNode() の引数がわからないため、observerに何が指定されているかわかりません。
//android/frameworks/av/media/libstagefright/ACodec.cpp
bool ACodec::UninitializedState::onAllocateComponent(const sp<AMessage> &msg) {
...
OMXClient client; //★★binderのクライアント★★
if (client.connect() != OK) { //★★デコーダは別プロセスで実行されているので、接続する★★
mCodec->signalError(OMX_ErrorUndefined, NO_INIT);
return false;
}
...
sp<IOMX> omx = client.interface(); //★★binderを使って通信するためのインタフェース★★
//android/frameworks/av/media/libstagefright/OMXClient.cpp
class OMXClient {
public:
OMXClient();
status_t connect();
void disconnect();
sp<IOMX> interface() {
return mOMX; //★★インタフェースはこれ★★
}
//android/frameworks/av/media/libstagefright/OMXClient.cpp
status_t OMXClient::connect() {
sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> playerbinder = sm->getService(String16("media.player"));
sp<IMediaPlayerService> mediaservice = interface_cast<IMediaPlayerService>(playerbinder);
...
sp<IOMX> mediaServerOMX = mediaservice->getOMX();
...
sp<IBinder> codecbinder = sm->getService(String16("media.codec"));
sp<IMediaCodecService> codecservice = interface_cast<IMediaCodecService>(codecbinder);
...
sp<IOMX> mediaCodecOMX = codecservice->getOMX();
...
mOMX = new MuxOMX(mediaServerOMX, mediaCodecOMX); //★★インタフェースはここで設定している★★
return OK;
}
なかなか複雑ですね。このインタフェースとやらの実体はMuxOMXだと思われます。
//android/frameworks/av/media/libstagefright/ACodec.cpp
bool ACodec::UninitializedState::onAllocateComponent(const sp<AMessage> &msg) {
...
sp<IOMX> omx = client.interface(); //★★MuxOMXのオブジェクトのはず★★
...
sp<CodecObserver> observer = new CodecObserver; //★★たぶんこれがobserver★★
IOMX::node_id node = 0;
status_t err = NAME_NOT_FOUND;
for (size_t matchIndex = 0; matchIndex < matchingCodecs.size();
++matchIndex) {
componentName = matchingCodecs[matchIndex];
quirks = MediaCodecList::getQuirksFor(componentName.c_str());
pid_t tid = gettid();
int prevPriority = androidGetThreadPriority(tid);
androidSetThreadPriority(tid, ANDROID_PRIORITY_FOREGROUND);
err = omx->allocateNode(componentName.c_str(), observer, &mCodec->mNodeBinder, &node); //★★ここでobserverをMuxOMX::allocateNodeに渡す★★
//android/frameworks/av/media/libstagefright/OMXClient.cpp
status_t MuxOMX::allocateNode(
const char *name, const sp<IOMXObserver> &observer,
sp<IBinder> *nodeBinder,
node_id *node) {
...
sp<IOMX> omx;
node_location loc = getPreferredCodecLocation(name);
if (loc == CODECPROCESS) {
omx = mMediaCodecOMX;
} else if (loc == MEDIAPROCESS) {
omx = mMediaServerOMX;
} else {
if (mLocalOMX == NULL) {
mLocalOMX = new OMX;
}
omx = mLocalOMX;
}
status_t err = omx->allocateNode(name, observer, nodeBinder, node); //★★OMX::allocateNode() などに渡す★★
ALOGV("allocated node_id %x on %s OMX", *node, omx == mMediaCodecOMX ? "codecprocess" :
omx == mMediaServerOMX ? "mediaserver" : "local");
突然、ここで三択(mMediaCodecOMXとmMediaServerOMXとmLocalOMX)になりますが、いずれの選択肢を選んでも、渡すobserverは変わらずCodecObserverのはずです。それさえわかれば、とりあえずOKです。
この記事にコメントする
目次: Android
昨日(2017年11月5日の日記参照)の続きです。
OpenMAXの解説をしていると日が暮れるのでやめます。とにかくデコードされた画素データはFillBufferDoneで返ってくることがわかっていれば、コードを追いかけられるはずです。
見ているコードはAndroid 7.1です。タグで言えばandroid-7.1.2_r33辺りです。
FillBufferDoneはコールバックであることは説明しました。OpenMAXの規格では、コンポーネントがコールバックする関数は、コンポーネントを生成する際に指定します。コールバックされる関数を探すには、コンポーネントを生成していそうな個所を探せばわかるはずです。
//android/frameworks/av/media/libstagefright/omx/OMX.cpp
status_t OMX::allocateNode(
const char *name, const sp<IOMXObserver> &observer,
sp<IBinder> *nodeBinder, node_id *node) {
...
OMXNodeInstance *instance = new OMXNodeInstance(this, observer, name); //★★1番目の引数がownerなので、thisつまりこのオブジェクトが指定される★★
OMX_COMPONENTTYPE *handle;
OMX_ERRORTYPE err = mMaster->makeComponentInstance(
name, &OMXNodeInstance::kCallbacks,
instance, &handle); //★★2番目の引数kCallbacksがコールバック関数の指定。3番目の引数instanceがFillBufferDoneのpAppDataに渡される★★
//android/frameworks/av/media/libstagefright/omx/OMXNodeInstance.cpp
// static
OMX_CALLBACKTYPE OMXNodeInstance::kCallbacks = {
&OnEvent, &OnEmptyBufferDone, &OnFillBufferDone
};
かなり端折ってますが、FillBufferDoneのコールバック関数にはOMXNodeInstance::OnFillBufferDoneを指定しているようです。従ってデコードが終わると、画素データが入ったバッファがOMXNodeInstance::OnFillBufferDone関数に渡されます。
//android/frameworks/av/media/libstagefright/omx/OMXNodeInstance.cpp
// static
OMX_ERRORTYPE OMXNodeInstance::OnFillBufferDone(
OMX_IN OMX_HANDLETYPE /* hComponent */,
OMX_IN OMX_PTR pAppData,
OMX_IN OMX_BUFFERHEADERTYPE* pBuffer) {
...
OMXNodeInstance *instance = static_cast<OMXNodeInstance *>(pAppData); //★★makeComponentInstanceの3番目の引数に渡した値★★
if (instance->mDying) {
return OMX_ErrorNone;
}
int fenceFd = instance->retrieveFenceFromMeta_l(pBuffer, kPortIndexOutput);
return instance->owner()->OnFillBufferDone(instance->nodeID(),
instance->findBufferID(pBuffer), pBuffer, fenceFd); //★★ownerはOMX型のオブジェクトなのでOMX::OnFillBufferを見る★★
}
//android/frameworks/av/media/libstagefright/omx/OMX.cpp
OMX_ERRORTYPE OMX::OnFillBufferDone(
node_id node, buffer_id buffer, OMX_IN OMX_BUFFERHEADERTYPE *pBuffer, int fenceFd) {
ALOGV("OnFillBufferDone buffer=%p", pBuffer);
omx_message msg;
msg.type = omx_message::FILL_BUFFER_DONE;
msg.node = node;
msg.fenceFd = fenceFd;
msg.u.extended_buffer_data.buffer = buffer;
msg.u.extended_buffer_data.range_offset = pBuffer->nOffset;
msg.u.extended_buffer_data.range_length = pBuffer->nFilledLen;
msg.u.extended_buffer_data.flags = pBuffer->nFlags;
msg.u.extended_buffer_data.timestamp = pBuffer->nTimeStamp;
findDispatcher(node)->post(msg); //★★post() とは何だろうか??★★
return OMX_ErrorNone;
}
sp<OMX::CallbackDispatcher> OMX::findDispatcher(node_id node) {
Mutex::Autolock autoLock(mLock);
ssize_t index = mDispatchers.indexOfKey(node);
return index < 0 ? NULL : mDispatchers.valueAt(index); //★★mDispatchersとは?★★
}
謎の関数CallbackDispatcher::post() が出てきました。名前からするとメッセージパッシングを行うための関数ではないかと予想されます。この場所に限らずstagefrightではあらゆる場所でメッセージパッシングが使用されており、とても読みづらいです……。
CallbackDispatcherというクラスが出てきましたので、見てみます。
//android/frameworks/av/media/libstagefright/include/OMX.h
class OMX : public BnOMX,
public IBinder::DeathRecipient {
...
KeyedVector<node_id, sp<CallbackDispatcher> > mDispatchers; //★★mDispatchersの定義★★
//android/frameworks/av/media/libstagefright/omx/OMX.cpp
struct OMX::CallbackDispatcher : public RefBase {
CallbackDispatcher(OMXNodeInstance *owner);
// Posts |msg| to the listener's queue. If |realTime| is true, the listener thread is notified
// that a new message is available on the queue. Otherwise, the message stays on the queue, but
// the listener is not notified of it. It will process this message when a subsequent message
// is posted with |realTime| set to true.
void post(const omx_message &msg, bool realTime = true);
...
private:
...
std::list<omx_message> mQueue;
void OMX::CallbackDispatcher::post(const omx_message &msg, bool realTime) {
Mutex::Autolock autoLock(mLock);
mQueue.push_back(msg); //★★メッセージをキューに追加★★
if (realTime) {
mQueueChanged.signal();
}
}
引数のnode_id nodeから、適切なCallbackDispatcherを探して、内部キューmQueueにメッセージを追加しています。mQueueを手掛かりにメッセージを処理する側を探すと、どうやらCallbackDispatcherThreadが処理しているようです。
//android/frameworks/av/media/libstagefright/omx/OMX.cpp
bool OMX::CallbackDispatcherThread::threadLoop() {
return mDispatcher->loop();
}
bool OMX::CallbackDispatcher::loop() {
for (;;) {
std::list<omx_message> messages;
{
Mutex::Autolock autoLock(mLock);
while (!mDone && mQueue.empty()) {
mQueueChanged.wait(mLock);
}
if (mDone) {
break;
}
messages.swap(mQueue); //★★mQueueのロック時間を短くするため、別のリストに全てのメッセージを移動させる★★
}
dispatch(messages); //★★メッセージ処理★★
}
return false;
}
void OMX::CallbackDispatcher::dispatch(std::list<omx_message> &messages) {
if (mOwner == NULL) {
ALOGV("Would have dispatched a message to a node that's already gone.");
return;
}
mOwner->onMessages(messages); //★★メッセージ送信先のmOwnerとは?★★
}
OMX::CallbackDispatcher::CallbackDispatcher(OMXNodeInstance *owner)
: mOwner(owner), //★★CallbackDispatcherの生成時に渡された引数で初期化されている★★
mDone(false) {
mThread = new CallbackDispatcherThread(this);
mThread->run("OMXCallbackDisp", ANDROID_PRIORITY_FOREGROUND);
}
ここまででわかったことは、
困ったことに、肝心のメッセージがどこに行くか?がいまだに不明です。mOwnerとはどこで指定されているのでしょう?
OMX::mDispatchersを操作している箇所を探すと、1箇所見つかります。先程も出てきたOMX::allocateNode() です。
//android/frameworks/av/media/libstagefright/omx/OMX.cpp
status_t OMX::allocateNode(
const char *name, const sp<IOMXObserver> &observer,
sp<IBinder> *nodeBinder, node_id *node) {
...
OMXNodeInstance *instance = new OMXNodeInstance(this, observer, name);
OMX_COMPONENTTYPE *handle;
OMX_ERRORTYPE err = mMaster->makeComponentInstance(
name, &OMXNodeInstance::kCallbacks,
instance, &handle); //★★3番目の引数、つまりinstanceがFillBufferDoneのpAppDataに渡される★★
if (err != OMX_ErrorNone) {
ALOGE("FAILED to allocate omx component '%s' err=%s(%#x)", name, asString(err), err);
instance->onGetHandleFailed();
return StatusFromOMXError(err);
}
*node = makeNodeID_l(instance);
mDispatchers.add(*node, new CallbackDispatcher(instance)); //★★メッセージの送信先を登録する★★
どうやらOMXNodeInstanceにメッセージを送っているようです。従ってmOwner->onMessages(messages) はここに辿り着きます。
void OMXNodeInstance::onMessages(std::list<omx_message> &messages) {
for (std::list<omx_message>::iterator it = messages.begin(); it != messages.end(); ) {
if (handleMessage(*it)) {
messages.erase(it++); //★★デコードに付随する情報をメッセージに載せる★★
} else {
++it;
}
}
if (!messages.empty()) {
mObserver->onMessages(messages); //★★mObserverとは?★★
}
}
ここで終わりかと思いきや、まだです。mObserverとは何者でしょうか?メッセージの冒険は続きます。
この記事にコメントする
目次: Android
たまにはAndroidの話でも。Androidのメディア再生のデコード完了から出画までを見てみました。
Media | Android Open Source Project 辺りにあるように、Androidはlibstagefrightにメディアの処理を任せています。
図からはちょっと読み取りづらいですが、libstagefrightは動画、音声のデコードにOpenMAXというAPIを用います。図だとOMX Coreと書かれている部分です。
OpenMAXの各種デコーダ(※)は「コンポーネント」と呼ばれる部品になっています。
OpenMAXではデータの入力はEmptyThisBufferと呼びます。データの入力は非同期に行うことができます。コンポーネントは入力を処理し終えたら完了通知をコールバック(EmptyBufferDone)する仕組みになっています。
データの出力はFillThisBufferと呼びます。出力も非同期に行うことができます。コンポーネントはデータ出力の完了通知(FillBufferDone)をする仕組みになっています。
libstagefrightはOpenMAXのコンポーネントに対して、下記の処理を行います。他にも設定、フラッシュ、などややこしい処理がありますが、省略。
入力側はこんな感じです。
出力側もほぼ同じです。
バッファが2つある場合も基本的には同じです。OpenMAXの特徴はバッファ1とバッファ2がお互いを気にしなくて良いことです。バッファ2が返ってきていようが返ってきていまいが、バッファ1はコンポーネントに渡して構いません。
例えばバッファ2にすごく時間が掛かって、こんな順になっても構いません(コロンの右側はコンポーネントに渡したが返ってきていないバッファの一覧)。
特にデコーダの場合は、この例のように渡した順番と返ってくる順番が違う場合がほとんどです。
(※)OpenMAXの規格が定義するコンポーネントの機能は、デコーダだけではありません。しかしAndroidはデコーダコンポーネントしか使いません。
この記事にコメントする
目次: Kindle
Kindleは書籍をAmazonから本体にダウンロードしてから読みます。漫画や雑誌のように図画の多い書籍は1冊100MB近いサイズとなるため、ダウンロードに時間がかかります。
時間帯やネットワーク環境にもよりますが、DTIに乗り換えてからはネットワーク環境が改善したので、早くて2, 3分、遅くても10分程度の短いものですが、ただボーっと待っているには長い時間です。
幸いにもKindle Fireはダウンロードしながら読むことができる機能があります。
しかし、この機能はまともに動かなくて、本当は何か書いてあるはずのページなのに、真っ白なページが表示されることが多いです。一度ページが白くなってしまうと、何度見ても白いままです。
ページが白くなってしまった場合は、書籍を本体から削除し、ダウンロードしなおすと直ります。元から白いページもあるのでややこしいですけど、その辺は本を読んでいる人ならわかりますよね。
きちんと動作すれば便利な機能だと思うのですが、今は使い物にならないです。それどころか、間違えてこの機能を発動させると本が歯抜けになってしまって迷惑なので、ダウンロード中は絶対に本を開かないようにしています。
Kindle Fireは色々残念なところが多いですね……。
この記事にコメントする
目次: マンガ紹介
少し前にアニメ化されて盛り上がって(おそらく負の方向に…)いた「異世界はスマートフォンとともに」ですが、いわゆる異世界転生物かつ最強系という作品です。
異世界転生物は現代社会において事故で死亡、もしくは強制的に転送されるなどして、現代社会ではない世界、仮想世界などに転生するところから始まる作品です。
最強系は転生前は平凡だった主人公が、異世界に行くだけで突如強くなって、異世界で良い思いしたり、トントン拍子に上手く行く作品です。もう一つの特徴として、男性1人が主人公です。見た目ではなく価値観や行動基準が青年〜中年男性です。
私の購入履歴からざっと拾ってみたら20作品ほどありました。独断と偏見で分類しています。
注意としては、全て漫画版の話です。原作小説がある作品がほとんどですが、一切読んでいませんし、内容も知りません。
「無敵」と「最強」の違いはざっくりいうと、こんな感じで分類しています。
今のところ判別できそうなシーンが無い(1巻しか発売されていないなど)場合もありますが、周りと異様にかけ離れて強いかどうかでとりあえず分類しています。ただ、いずれの作品も連載中ですから、お話が進むにつれてこの分類とは違う展開になるかもしれません。
異世界&最強系の特徴として、主人公の価値観や行動基準が青年〜中年男性です。気持ち悪い言い方をすると、主人公が男でも女でも怪物でも必ず「心にオジサンがインストール」されています。先ほど挙げた20作品の中でこの点を外している作品は「私、能力は平均値で〜」だけです。
主人公を褒めたたえる周りのキャラクター達には、若い美女か地位の高い人が多いです。これは主人公(の中のオジサン)の願望がほぼ「ハーレム」か「成り上がり」に集約されるためだと思われます。
ちなみに先ほど「無敵」と「最強」に作品を分類しましたが、これは「ハーレム」と「成り上がり」傾向とそれなりに一致します。つまり「無敵」なら「ハーレム」傾向、「最強」なら「成り上がり」傾向が強いです。不思議ですね。
ハーレムの傾向が強い作品は好みが分かれると思いますから、私のお勧めは「最強」の作品、特に「最強、努力も継続」に挙げた作品です。
表現上、努力と書きましたが、何も悩むことはありません。全戦、全勝、とにかく楽勝。成り上がりはトントン拍子、全てが上手く行きます。気分爽快ですよね!
ちなみに私は普段からこんな面倒くさいことを考えて読んでいる訳じゃありません。こんなん考えていたら集中できませんし、作品が全く面白くなくなります……。
この記事にコメントする
| < | 2017 | > | ||||
| << | < | 11 | > | >> | ||
| 日 | 月 | 火 | 水 | 木 | 金 | 土 |
| - | - | - | 1 | 2 | 3 | 4 |
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | - | - |
25年10月6日
25年10月6日
25年9月29日
25年9月29日
20年8月24日
20年8月24日
16年2月14日
16年2月14日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
20年8月16日
20年8月16日
20年8月16日
20年8月16日
24年6月17日
24年6月17日
wiki
Linux JM
Java API
2002年
2003年
2004年
2005年
2006年
2007年
2008年
2009年
2010年
2011年
2012年
2013年
2014年
2015年
2016年
2017年
2018年
2019年
2020年
2021年
2022年
2023年
2024年
2025年
過去日記について
アクセス統計
サーバ一覧
サイトの情報合計:
本日: